框架
Storybook 的架构旨在支持多种多样的 Web 框架,包括 React、Vue、Angular、Web Components、Svelte 以及十多种其他框架。本指南帮助你开始为 Storybook 添加新的框架支持。
构建新框架
首先要做的是在你自己的仓库中构建框架支持。
我们建议采用与 Storybook monorepo 相同的项目结构。该结构包含框架包 (app/<framework>
) 和示例应用程序 (examples/<framework>-kitchen-sink
),以及根据需要关联的其他文档和配置。
这看起来可能比实际需要更多的层级结构。但由于该结构模仿了 Storybook monorepo 的构建方式,你可以重用 Storybook 的工具。如果以后需要将框架移入 Storybook monorepo,这也会更容易。
我们建议使用 @storybook/html
作为入门框架,因为它最简单,且不包含任何框架特有的特性。你可以在此处找到入门样板。
框架架构
在 Storybook 中支持新框架通常包含两个主要方面:
-
配置服务器。在 Storybook 中,服务器是指运行
storybook dev
或storybook build
时运行的 Node 进程。配置服务器通常意味着以框架特定的方式配置 babel 和 webpack。 -
配置客户端。客户端是指在浏览器中运行的代码,配置客户端意味着提供一个框架特定的故事渲染函数。
配置服务器
Storybook 具有预设 (presets) 的概念,它们通常是用于文件加载的 babel/webpack 配置。如果你的框架有自己的文件格式(例如 “.vue”),你可能需要在加载时将其转换为 JavaScript 文件。如果你认为你的框架的每个用户都需要这样做,你应该将其添加到框架中。到目前为止,所有添加到 Storybook 的框架都这样做了,因为 Storybook 的核心配置非常简洁。
包结构
在添加框架预设之前,了解 Storybook 的包结构很有帮助。每个框架通常在其 package.json
中暴露两个可执行文件:
{
"bin": {
"storybook": "./bin/index.js",
"build-storybook": "./bin/build.js"
}
}
这些脚本会将一个 options
对象传递给 storybook/internal/server
,这是一个抽象了 Storybook 所有独立于框架代码的库。
例如,这是使用 storybook dev
启动开发服务器的样板代码:
import { buildDev } from '@storybook/core/server';
import options from './options';
buildDev(options);
因此,添加框架预设的本质就是填充该 options 对象。
服务器选项
如上所述,服务器 options
对象负责完成大部分服务器配置工作。
我们来看看 @storybook/vue
的选项定义:
import { sync } from 'read-pkg-up';
export default {
packageJson: sync({ cwd: __dirname }).packageJson,
framework: 'vue',
frameworkPresets: [require.resolve('./framework-preset-vue.js')],
};
framework
选项的值(即 ‘vue’)会传递给插件,以便它们能够执行与你的框架相关的特定任务。
此文件的核心是框架预设,它们是标准的Storybook 预设 —— 你可以查看 Storybook monorepo 中的框架包(例如 React、Vue、Web Components)以查看框架特定自定义的示例。
在开发非 Storybook 维护的自定义框架时,你可以使用 frameworkPath
键指定位置文件的路径:
import { sync } from 'read-pkg-up';
export default {
packageJson: sync({ cwd: __dirname }).packageJson,
framework: 'my-framework',
frameworkPath: '@my-framework/storybook',
frameworkPresets: [require.resolve('./framework-preset-my-framework.js')],
};
你可以为 frameworkPath
添加相对路径。别忘了它们默认从 Storybook 配置目录(即 .storybook
)解析。
确保 frameworkPath
指向你的框架应用程序中的 dist/client/index.js
文件。
配置客户端
要配置客户端,你必须提供一个框架特定的渲染函数。在深入细节之前,了解用户编写的故事如何与屏幕上渲染的内容相关联是至关重要的。
可渲染对象
Storybook 故事是返回“可渲染对象”的 ES6 对象。
考虑以下 React 故事:
import { Button } from './Button';
export default {
component: Button,
};
export const Sample = {
render: () => <Button label="hello button" />,
};
在这种情况下,可渲染对象是 React 元素 <Button .../>
。
在大多数其他框架中,可渲染对象实际上是一个普通的 JavaScript 对象。
考虑以下假设示例:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Sample: Story = {
render: () => ({
template: '<button :label=label />',
data: {
label: 'hello button',
},
}),
};
这种“可渲染对象”的设计是框架特定的,理想情况下应与该框架的惯用法相匹配。
渲染函数
框架的渲染函数是将可渲染对象转换为 DOM 节点的实体。它通常具有以下形式:
const rootElement = document.getElementById('root');
export default function renderMain({ storyFn }: RenderMainArgs) {
const storyObj = storyFn();
const html = fn(storyObj);
rootElement.innerHTML = html;
}
包结构
在客户端,关键文件是src/client/preview.js
import { start } from 'storybook/preview-api';
import './globals';
import render from './render';
const api = start(render);
// the boilerplate code
globals 文件通常会设置一个全局变量,客户端代码(例如插件提供的装饰器)可以在需要时引用该变量,以了解正在运行的是哪个框架。
import { global } from '@storybook/global';
const { window: globalWindow } = global;
globalWindow.STORYBOOK_ENV = 'vue';
start
函数抽象了 Storybook 所有独立于框架的客户端(浏览器)代码,并接受我们上面定义的渲染函数。有关渲染函数的示例,请参见 Storybook monorepo 中的React、Vue、Angular 和 Web Components。