Under the hood
Guillaume Chau
@Akryum
Vue.js Core Team
@Akryum@m.webtoo.ls
A story is a small number of components mounted in an isolated environment
Story:
Why stories?
Organize and document components for other developers
Showcase features and components
Develop components in isolation
Tests components
Visual regression
⚡ Dynamic source
🍱 Variant grids
📖 Markdown docs
🌔 Dark theme
📱 Responsive testing
🎹 Flexible controls
📷 Visual regression testing
🎨 Automatic design tokens
🔍 Fast fuzzy/full-text search
More to come...
Packed with features!
Writing stories
Writing stories
should look similar to writing component code
<!-- Cars.story.vue -->
<template>
<Story title="Cars">
<Variant title="default">
🚗
</Variant>
<Variant title="Fast">
🏎️
</Variant>
<Variant title="Slow">
🚜
</Variant>
</Story>
</template>
How Vite powers Histoire
Vite Native
Reuse the same build pipeline
Less time and effort setting up
Fast boot and instant HMR
Virtual modules
histoire dev
Story FS watcher
.md FS watcher
Vite dev server
(HTTP server)
vite-node server
Generate docs-only stories
<!-- Cars.story.vue -->
<template>
<Story title="Cars">
<Variant title="default">
🚗
</Variant>
<Variant title="Fast">
🏎️
</Variant>
<Variant title="Slow">
🚜
</Variant>
</Story>
</template>
-
Docs of Story 1
-
Docs page 1
-
Docs page 2
-
Story 1
-
Story 2
-
Story 3
-
...
Collect
Story FS watcher
.md FS watcher
Vite dev server
(HTTP server)
vite-node server
-
Story 1
-
Story 2
-
Story 3
-
...
-
Docs of Story 1
-
Docs page 1
-
Docs page 2
Vite dev server
(HTTP server)
Virtual modules
Browser
import {
files, tree, onUpdate
} from 'virtual:$histoire-stories'
export let files = [{ ... }]
export let tree = { ... }
const handlers = []
export function onUpdate (cb) {
handlers.push(cb)
}
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
files = newModule.files
tree = newModule.tree
handlers.forEach(h => {
h(newModule.files, newModule.tree)
newModule.onUpdate(h)
})
})
}
HMR
async resolveId (id) {
if (id === 'virtual:$histoire-stories') {
return '\0virtual:$histoire-stories'
}
}
async load (id) {
if (id === '\0virtual:$histoire-stories') {
return `export let files = [${JSON.stringify(files)}]
export let tree = ${JSON.stringify(tree)}
const handlers = []
export function onUpdate (cb) {
handlers.push(cb)
}
if (import.meta.hot) {
// Handle HMR...
}`
}
}
Vite dev server
(HTTP server)
Virtual modules
Browser
HMR
stories data & tree
resolved config
theme CSS variables
setup code
support plugins
markdown files
full-text search indexes
Vite dev server
(HTTP server)
Browser
View: Story 1
stories data & tree
setup code
support plugin
markdown files
Mount: Story 1
Story1.story.vue
Render: Story 1: Content
Render: Story 1: Controls
support plugin
support plugin
Load
- Props
- Slots
- Render functions
Browser
View: Story 1
stories data & tree
support plugin
Mount: Story 1
Story1Component.vue
Load
Render: Story 1: Content
Render: Story 1: Controls
support plugin
support plugin
Sync state
can be in an iframe
Browser
View: Story 1
stories data & tree
support plugin
Mount: Story 1
Story1Component.vue
Load
Render: Story 1: Content
support plugin
Auto-CodeGen: Story 1
support plugin
slot
<button
color="primary"
disabled
>
Click me!
</button>
Sync state
Browser
Virtual modules
stories data & tree
resolved config
theme CSS variables
setup code
support plugins
markdown files
full-text search indexes
Static code
Vite build
HMR API
export const count = 1
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
if (newModule) {
// newModule is undefined when SyntaxError happened
console.log('updated: count is now ', newModule.count)
}
})
}
Accept an update for current module
import { foo } from './foo.js'
foo()
if (import.meta.hot) {
import.meta.hot.accept('./foo.js', (newFoo) => {
// the callback receives the updated './foo.js' module
newFoo?.foo()
})
}
Accept an update for a dependency
if (import.meta.hot) {
// When current module is replaced
import.meta.hot.dispose((data) => {
// cleanup side effect
})
// when the module is no longer used in the page
import.meta.hot.prune((data) => {
// cleanup side effect
})
}
Cleanup module side-effects
if (import.meta.hot) {
import.meta.hot.accept((module) => {
// You may use the new module instance
// to decide whether to invalidate.
if (cannotHandleUpdate(module)) {
import.meta.hot.invalidate()
}
})
}
Bail out of HMR and propagate the update in parent modules
Client-Dev server communication
Using HMR API
vitePlugins.push({
name: 'histoire:example',
configureServer (server) {
server.ws.on('some-event', (payload) => {
server.ws.send('some-other-event', somePayload)
})
},
})
if (import.meta.hot) {
import.meta.hot.send('some-event', { message: 'Hello Amsterdam' })
import.meta.hot.on('some-other-event', (somePayload) => {
console.log(somePayload)
})
}
Browser
Vite server
Thank you!
Histoire: A deep dive (Vue Amsterdam 2023)
By Guillaume Chau
Histoire: A deep dive (Vue Amsterdam 2023)
A look at the internals of Histoire, a tool to generate component stories, and how Vite makes it possible.
- 2,658