加入直播会议:美国东部时间周四上午 11 点,Storybook 9 版本发布及 AMA

Storybook Addon Preview 可以在 Storybook 中以各种框架代码显示用户选择的 knobs

在 Github 上查看

Storybook Addon Preview

npm version

Storybook Addon Preview 可以在 Storybook 中以各种框架代码显示用户选择的 controls(args)knobs

入门

  • 需要 Storybook 6 或更高版本。
  • 如果您使用 Storybook 5,请使用版本 1.x。
npm i storybook-addon-preview --dev

.storybook/main.js

module.exports = {
    addons: [
        "storybook-addon-preview/register"
    ],
};

现在,编写带有预览的 stories。

如何与 controls(args) 一起使用

import { previewTemplate, DEFAULT_VANILLA_CODESANDBOX } from "storybook-addon-preview";
// CSF https://storybook.org.cn/docs/react/api/csf

export default {
    title: "Example",
}
export const example = e => {
    e.opt1;
    e.num1;
    return ....;
}
example.parameters = {
    preview: [
        {
            tab: "Vanilla",
            template: previewTemplate`
const inst = new Instance({
    opt1: ${"opt1"},
    num1: ${"num1"},
});
            `,
            language: "ts",
            copy: true,
            codesandbox: DEFAULT_VANILLA_CODESANDBOX(["@egjs/infinitegrid"]),
        },
    ],
};
example.args = {
    opt1: false,
    num1: 0,
};
example.argTypes = {
    opt1: {
        control: { type: "boolean" },
        defaultValue: false,
    },
    num1: {
        control: { type: "number" },
        defaultValue: 0,
    },
};

如何与 knobs 一起使用(已弃用)

import { withPreview, previewTemplate, DEFAULT_VANILLA_CODESANDBOX } from "storybook-addon-preview";
import { withKnobs, boolean, number } from "@storybook/addon-knobs";

const stories = storiesOf("Example", module);

stories.addDecorator(withKnobs).addDecorator(withPreview);

stories.add("example", e => {
    const opt1Value = boolean("opt1", false);
    const num1Value = number("num1", 0);

    return ....;
}, {
    preview: [
        {
            tab: "Vanilla",
            template: previewTemplate`
const inst = new Instance({
    opt1: ${"opt1"},
    num1: ${"num1"},
});
            `,
            language: "ts",
            copy: true,
            codesandbox: DEFAULT_VANILLA_CODESANDBOX(["@egjs/infinitegrid"]),
        },
    ]
});
// CSF https://storybook.org.cn/docs/react/api/csf

export default {
    title: "Example",
    decorators: [withKnobs, withPreview],
    parameters: {
        preview: [
            {
                tab: "Vanilla",
                template: previewTemplate`
    const inst = new Instance({
        opt1: ${"opt1"},
        num1: ${"num1"},
    });
                `,
                language: "ts",
                copy: true,
                codesandbox: DEFAULT_VANILLA_CODESANDBOX(["@egjs/infinitegrid"]),
            },
        ],
    },
}
export const example = e => {
    const opt1Value = boolean("opt1", false);
    const num1Value = number("num1", 0);

    return ....;
}

InfiniteGrid 的 Storybook 示例

属性

export interface PreviewParameter {
    /**
     * The name of the tab to appear in the preview panel
     */
    tab?: string;
    /**
     * Code to appear in the corresponding tab of the preview panel.
     */
    template?: string
    | ((props: Record<string, any>, globals: Record<string, any>) => any)
    | ReturnType<typeof previewTemplate>;
    /**
     * Description of the corresponding template code
     */
    description?: string;
    /**
     * Custom args or knobs that can be used in the preview template.
     */
    knobs?: Record<string, any>;
    /**
     * Custom args or knobs that can be used in the preview template.
     */
    args?: Record<string, any>;
    /**
     * Whether to display the copy button
     */
    copy?: boolean;
    /**
     * Language to highlight its code ("html", "css", "jsx", "tsx", "ts", "js")
     */
    language?: string;
    /**
     * Language presets to link to codesandbox
     * @see {@link https://github.com/naver/storybook-addon-preview/blob/master/README.md}
     */
    codesandbox?: CodeSandboxValue
    | ((previewMap: Record<string, string[]>) => CodeSandboxValue);
    /**
     * Whether to share line numbers when tab names are the same
     */
    continue?: boolean;
    /**
     * Formatting type for that code if you want formatting
     * Only "html" is supported as built-in support.
     * If you want to use custom formatter, use `previewFormatter` config in manager.js
     * @see {@link https://github.com/naver/storybook-addon-preview/blob/master/README.md}
     */
    format?: string | boolean;
}

格式化

Storybook 基本上使用独立的 prettierparser-html。因此 PreviewParameter 格式仅支持 "html"。

如果您使用自定义格式化程序,请注意文件大小可能会增加。

请参阅:https://prettier.node.org.cn/docs/en/browser.html

// .storybook/manager.js
import { addons } from "@storybook/addons";
import * as prettier from "prettier/standalone";
import * as htmlParser from "prettier/parser-html";
import * as babelParser from "prettier/parser-babel";
import * as postCSSParser from "prettier/parser-postcss";

addons.setConfig({
    previewFormatter: (format, code) => {
        if (format === "tsx") {
            return prettier.format(code, {
                parser: "babel-ts",
                plugins: [
                    htmlParser,
                    babelParser,
                    postCSSParser,
                ],
            });
        } else if (format === "vue") {
            return prettier.format(code, {
                parser: "vue",
                plugins: [
                    htmlParser,
                    babelParser,
                    postCSSParser,
                ],
            });
        }
        return code;
    },
});
// stories.js

export const Story = {
    parameters: {
        preview: [
            {
                tab: "Vanilla",
                template: `
const inst = new Instance({
    opt1: "opt1",
    num1: "num1",
});
                `,
                language: "ts",
                format: "tsx",
            },
        ],
    }
}

模板

  • 如果模板是不使用 knobs 的代码,您可以直接将其写成 string 类型。
{
    template: `
const inst = new Instance({
    opt1: 1,
    num1: 1,
});
`,
}
  • 如果您只是想原样表达 knobs,请使用 previewTemplate 函数
import { previewTemplate } from "storybook-addon-preview";

{
    args: {
        args1: true,
    },
    template: previewTemplate`
const inst = new Instance({
    opt1: ${"opt1"},
    num1: ${"num1"},
    args1: ${"args1"},
});
`,
}
  • 如果您想处理变量,请使用函数
{
    args: {
        args1: true,
    },
    template: (props, globals) => `
const inst = new Instance({
    opt1: ${props.opt1},
    num1: ${props.num1},
    args1: ${props.args1},
});
`,
}

高亮

  • 如果您想高亮您的代码,请添加 [highlight] 注释。
[
    {
        template: previewTemplate`
    const inst = new Instance({
        /* [highlight] highlight opt1 */
        opt1: ${"opt1"},
        num1: ${"num1"},
    });
        `,
        language: "js",
    },
    {
        template: previewTemplate`
<!-- [highlight] highlight html -->
<div style="width: ${"width"}px;"></div>
        `,
        language: "html",
    },
]
  • 如果您想高亮您的代码区域,请添加 [highlight-start][highlight-end] 注释。
[
    {
        template: previewTemplate`
    const inst = new Instance({
        /* [highlight-start] highlight options */
        opt1: ${"opt1"},
        num1: ${"num1"},
        /* [highlight-end] */
    });
    `,
    },
    {
        template: previewTemplate`
<!-- [highlight-start] highlight html -->
<div style="width: ${"width"}px;"></div>
<!-- [highlight-end] -->
        `,
        language: "html",
    },
]

Props

轻松使用选项或 props,或者当您有很多选项时使用 props 模板

export interface PropsOptions {
    indent?: number;
    wrap?: string;
    prefix?: string;
}
  • DEFAULT_PROPS_TEMPLATE(names: string[], options: PropsOptions)
import { previewTemplate, DEFAULT_PROPS_TEMPLATE } from "storybook-addon-preview";

{
    template: previewTemplate`
/* [highlight] You can see opt1, num1 options. */
const inst = new Instance({
${DEFAULT_PROPS_TEMPLATE(["opt1", "num1"], { indent: 4 })}
});
`,
}
  • JSX_PROPS_TEMPLATE(names: string[], options: PropsOptions)
import { previewTemplate, JSX_PROPS_TEMPLATE } from "storybook-addon-preview";

{
    template: previewTemplate`
/* [highlight] You can see opt1, num1 options. */
<Instance
${JSX_PROPS_TEMPLATE(["opt1", "num1"], { indent: 4 })}
    />
`,
    language: "jsx",
}
  • ANGULAR_PROPS_TEMPLATE(names: string[], options: PropsOptions)
import { previewTemplate, ANGULAR_PROPS_TEMPLATE } from "storybook-addon-preview";

{
    template: previewTemplate`
/* [highlight] You can see opt1, num1 options. */
<ngx-instance
${ANGULAR_PROPS_TEMPLATE(["opt1", "num1"], { indent: 4 })}
    ></ngx-instance>
`,
    language: "html",
}
  • VUE_PROPS_TEMPLATE(names: string[], options: PropsOptions)
import { previewTemplate, VUE_PROPS_TEMPLATE } from "storybook-addon-preview";

{
    template: previewTemplate`
/* [highlight] You can see opt1, num1 options. */
<vue-instance
${VUE_PROPS_TEMPLATE(["opt1", "num1"], { indent: 4 })}
    ></vue-instance>
`,
    language: "html",
}
  • LIT_PROPS_TEMPLATE(names: string[], options: PropsOptions)
import { previewTemplate, LIT_PROPS_TEMPLATE } from "storybook-addon-preview";

{
    template: previewTemplate`
/* [highlight] You can see opt1, num1 options. */
html${"`"}<lit-instance
${LIT_PROPS_TEMPLATE(["opt1", "num1"], { indent: 4 })}
    ></lit-instance>${"`"};
`,
    language: "js",
}

CodeSandBox

将您使用的代码链接到 codesandbox。有用于链接 codesandbox 的依赖项和初始设置文件。我们支持的框架有 react, angular, svelte, lit, preact, 和 vue。

const CodeSandboxValue = {
    // https://github.com/codesandbox/codesandbox-importers/blob/master/packages/import-utils/src/create-sandbox/templates.ts#L63
    template: "vue-cli",
    files: {
        "src/main.js": `
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

new Vue({
render: h => h(App)
}).$mount("#app");
    `,
        "src/App.vue": {
            tab: "Vue",
        },
    },
    dependencies: {
        "vue": "^2.6.0",
    },
    userDependencies: ["@egjs/vue-infinitegrid@^3"],
});

您可以使用默认的 codesandbox 预设。

  • 代码中使用的框架模块以外的外部模块
// DEFAULT_(VANILLA)_CODESANDBOX
// DEFAULT_(REACT)_CODESANDBOX
// DEFAULT_(ANGULAR)_CODESANDBOX
type DEFAULT_FRAMEWORK_CODESANDBOX: CodeSandboxTemplate = (userDependencies?: string[], files?: FilesParam) => CodeSandboxValue;
  • 预览中提供的 codesandbox 预设有 vanilla, react, angular, vue, preact, lit 和 svelte。
名称 默认标签页名称 代码
DEFAULT_VANILLAJS_CODESANDBOX(JS) HTML, VANILLA, CSS(可选) 查看代码
DEFAULT_VANILLA_CODESANDBOX(TS) HTML, VANILLA, CSS(可选) 查看代码
DEFAULT_REACT_CODESANDBOX(TS) React, CSS(可选) 查看代码
DEFAULT_REACTJS_CODESANDBOX(TS) ReactJS, CSS(可选) 查看代码
DEFAULT_ANGULAR_CODESANDBOX Angular(html, component, module), CSS(可选) 查看代码
DEFAULT_VUE_CODESANDBOX Vue 查看代码
DEFAULT_VUE3_CODESANDBOX Vue3 查看代码
DEFAULT_SVELTE_CODESANDBOX Svelte 查看代码
DEFAULT_LIT_CODESANDBOX Lit, CSS(可选) 查看代码

以下说明如何使用默认 codesandbox 预设。

import {
    DEFAULT_VANILLA_CODESANDBOX,
    DEFAULT_REACT_CODESANDBOX,
    DEFAULT_ANGULAR_CODESANDBOX,
} from "storybook-addon-preview";

{
    preview: [
        {
            // { tab: "HTML" }
            tab: "HTML",
            template: ...,
        },
        {
            // { tab: "CSS" }
            tab: "CSS",
            template: ...,
        },
        {
            // { tab: "Vanilla" }
            tab: "Vanilla",
            template: ...,
            codesandbox: DEFAULT_VANILLA_CODESANDBOX(["@egjs/infinitegrid"]),
        },
        {
            // { tab: "React" }
            tab: "React",
            template: ...,
            codesandbox: DEFAULT_REACT_CODESANDBOX(["@egjs/react-infinitegrid"]),
        },
        {
            // { tab: "Angular", index: 0 }
            tab: "Angular",
            description: "app.component.html",
            template: ...,
            language: "markup",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"]),
        },
        {
            // { tab: "Angular", index: 1 }
            tab: "Angular",
            description: "app.component.ts",
            template: ...,
            language: "tsx",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"]),
        },
        {
            // { tab: "Angular", index: 2 }
            tab: "Angular",
            description: "app.module.ts",
            template: ...,
            language: "typescript",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"]),
        },
    ],
}

更改标签页名称

检查 codesandbox 预设代码以获取文件名。

import {
    DEFAULT_VANILLA_CODESANDBOX,
    DEFAULT_REACT_CODESANDBOX,
    DEFAULT_ANGULAR_CODESANDBOX,
} from "storybook-addon-preview";

{
    preview: [
        {
            tab: "Custom HTML",
            template: ...,
        },
        {
            tab: "Custom CSS",
            template: ...,
        },
        {
            tab: "Vanilla",
            template: ...,
            codesandbox: DEFAULT_VANILLA_CODESANDBOX(["@egjs/infinitegrid"], {
                "index.html": {
                    tab: "Custom HTML",
                    template: "html",
                    values: {
                        cssFiles: ["src/styles.css"],
                        jsFiles: ["src/index.ts"],
                    },
                },
                "src/styles.css" : { tab: "Custom CSS" },
            }),
        },
        {
            tab: "Angular Component HTML",
            description: "app.component.html",
            template: ...,
            language: "markup",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"], {
                "src/app/app.component.html": { tab: "Angular Component HTML" },
                "src/app/app.component.ts": { tab: "Angular Component" },
                "src/app/app.module.ts": { tab: "Angular Module" },
            }),
        },
        {
            tab: "Angular Component",
            description: "app.component.ts",
            template: ...,
            language: "tsx",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"], {
                "src/app/app.component.html": { tab: "Angular Component HTML" },
                "src/app/app.component.ts": { tab: "Angular Component" },
                "src/app/app.module.ts": { tab: "Angular Module" },
            }),
        },
        {
            tab: "Angular Module",
            description: "app.module.ts",
            template: ...,
            language: "typescript",
            codesandbox: DEFAULT_ANGULAR_CODESANDBOX(["@egjs/ngx-infinitegrid"], {
                "src/app/app.component.html": { tab: "Angular Component HTML" },
                "src/app/app.component.ts": { tab: "Angular Component" },
                "src/app/app.module.ts": { tab: "Angular Module" },
            }),
        },
    ],
};

制作自定义 CodeSandbox

  • template 基于 逻辑。
  • dependencies, devDependencies, scripts 基于 package.jsondependencies, devDependencies, scripts
  • userDependencies 是数组类型的依赖项。([vue@^2.6.0])
  • files 具有 string、CodeFileTab(object) 和 null 类型。
    • CodeFileTab: 将预览标签页作为字符串值返回。
    • null: 删除现有文件。

CodeSandbox 支持各种模板。要使用模板,您需要自己定义基本文件。请参考 CodeSandbox 中的模板。

export const DEFAULT_VUE_CODESANDBOX: CodeSandboxTemplate = (userDependencies = [], files = {}) => {
    return {
        template: "vue-cli",
        files: {
            "src/main.js": `
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

new Vue({
    render: h => h(App)
}).$mount("#app");
        `,
            "src/App.vue": {
                tab: "Vue",
            },
            ...files,
        },
        dependencies: {
            "vue": "^2.6.0",
        },
        userDependencies,
    };
};

贡献

请参阅 CONTRIBUTING.md

许可协议

storybook-addon-preview 根据 MIT 许可协议发布。

Copyright (c) 2020-present NAVER Corp.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.