阐述如何利用 Storybook 或类似工具为 Vue 组件库构建交互式文档和测试平台。

各位靓仔靓女们,大家好!今天咱们不聊风花雪月,就来唠唠嗑,关于如何给Vue组件库打造一个集美貌与智慧于一身的交互式文档和测试平台。就拿Storybook来说吧,这玩意儿简直就是组件的专属“展示厅+游乐场”,能让你的组件亮瞎别人的眼,还能让开发测试过程变得像玩游戏一样轻松愉快。

第一幕:Storybook“粉墨登场”——安装与配置

好,闲话少说,咱们先让Storybook登场。安装过程嘛,就像装软件一样简单粗暴,一条命令搞定:

npx storybook init

这条命令会分析你的项目,自动安装相应的依赖,并创建一个.storybook的文件夹,里面住着Storybook的配置文件。如果你的项目比较特殊,或者你想手动配置,可以这样:

  1. 安装依赖:

    npm install -D @storybook/vue3 @storybook/addon-essentials @storybook/addon-links @storybook/addon-interactions @storybook/testing-library
    • @storybook/vue3: Storybook对Vue 3的支持。如果是Vue 2,请换成@storybook/vue
    • @storybook/addon-essentials: 一些常用的插件,比如控制面板、文档生成等。
    • @storybook/addon-links: 允许你在 Storybook 故事之间创建链接。
    • @storybook/addon-interactions: 允许你创建可交互的故事,并使用 Play 函数进行测试。
    • @storybook/testing-library: 方便你在 Play 函数中使用 Testing Library 进行断言。
  2. 配置.storybook/main.js

    module.exports = {
      stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
      addons: [
        '@storybook/addon-essentials',
        '@storybook/addon-links',
        '@storybook/addon-interactions',
      ],
      framework: {
        name: '@storybook/vue3',
        options: {},
      },
      docs: {
        autodocs: true,
      },
    };
    • stories: 定义Storybook去哪里找你的故事文件(后面会讲到)。
    • addons: 引入你需要的插件。
    • framework: 声明你使用的框架。
    • docs: 启用自动文档生成。
  3. 配置.storybook/preview.js (可选):

    import '../src/assets/main.css'; // 引入你的全局样式(如果需要)
    
    export const parameters = {
      actions: { argTypesRegex: "^on[A-Z].*" },
      controls: {
        matchers: {
          color: /(background|color)$/i,
          date: /Date$/,
        },
      },
    };
    • 这里可以配置一些全局的参数,比如事件处理函数的命名规则、控制器的匹配规则等。你还可以在这里引入你的全局样式、注册全局组件等等。

第二幕:编写你的第一个故事——组件的“个人秀”

好,有了舞台,接下来就要让你的组件登台表演了。Storybook里,每个组件的“表演剧本”就叫做“故事”(Story)。咱们来创建一个简单的按钮组件的故事:

  1. 创建src/components/MyButton.vue

    <template>
      <button :class="classes" @click="$emit('click')">
        {{ label }}
      </button>
    </template>
    
    <script>
    export default {
      props: {
        label: {
          type: String,
          default: 'Button',
        },
        primary: {
          type: Boolean,
          default: false,
        },
        size: {
          type: String,
          default: 'medium',
          validator: (value) => ['small', 'medium', 'large'].includes(value),
        },
      },
      computed: {
        classes() {
          return [
            'my-button',
            `my-button--${this.size}`,
            { 'my-button--primary': this.primary },
          ];
        },
      },
      emits: ['click'],
    };
    </script>
    
    <style scoped>
    .my-button {
      border: none;
      padding: 10px 20px;
      border-radius: 5px;
      cursor: pointer;
    }
    
    .my-button--primary {
      background-color: #007bff;
      color: white;
    }
    
    .my-button--small {
      padding: 5px 10px;
      font-size: 0.8em;
    }
    
    .my-button--medium {
      font-size: 1em;
    }
    
    .my-button--large {
      padding: 15px 30px;
      font-size: 1.2em;
    }
    </style>
  2. 创建src/components/MyButton.stories.js

    import MyButton from './MyButton.vue';
    
    export default {
      title: 'Components/MyButton', // Storybook里的分类
      component: MyButton,
      argTypes: {
        label: { control: 'text', description: '按钮的文本' },
        primary: { control: 'boolean', description: '是否是主要按钮' },
        size: { control: 'select', options: ['small', 'medium', 'large'], description: '按钮的大小' },
        onClick: { action: 'clicked', description: '按钮点击事件' },
      },
    };
    
    const Template = (args) => ({
      components: { MyButton },
      setup() {
        return { args };
      },
      template: '<MyButton v-bind="args" @click="args.onClick" />',
    });
    
    export const Primary = Template.bind({});
    Primary.args = {
      primary: true,
      label: 'Primary Button',
    };
    Primary.parameters = {
      docs: {
        source: {
          code: '<MyButton primary label="Primary Button" />',
        },
      },
    };
    
    export const Secondary = Template.bind({});
    Secondary.args = {
      label: 'Secondary Button',
    };
    
    export const Small = Template.bind({});
    Small.args = {
      size: 'small',
      label: 'Small Button',
    };
    
    export const Large = Template.bind({});
    Large.args = {
      size: 'large',
      label: 'Large Button',
    };
    • title: 定义这个故事在Storybook里的分类,方便你组织组件。
    • component: 指定这个故事是关于哪个组件的。
    • argTypes: 定义组件的属性(props),以及它们在Storybook里的控制方式。control指定了控制器的类型,options指定了可选值,description添加了属性的描述,action可以让Storybook监听事件,并在界面上显示事件触发的信息。
    • Template: 一个函数,用于渲染组件。它接收args作为参数,args包含了你配置的属性值。
    • Primary, Secondary, Small, Large: 不同的故事,代表组件的不同状态。
    • args: 每个故事的属性值。
    • parameters: 可以为每个 story 单独设置参数,比如自定义文档源码。

第三幕:启动Storybook——“验收成果”的时刻

一切就绪,是时候启动Storybook,看看我们的“作品”了:

npm run storybook

或者

yarn storybook

打开浏览器,访问Storybook的地址(通常是http://localhost:6006),你就能看到你的组件在Storybook里闪耀登场了!你可以在控制面板里调整属性值,实时看到组件的变化,还可以点击按钮,看到事件触发的信息。

第四幕:高级技巧——让你的Storybook更上一层楼

  1. 使用MDX编写文档:

    除了用JavaScript编写故事,你还可以用MDX(Markdown + JSX)来编写更丰富的文档。MDX允许你在Markdown文档里嵌入JSX代码,让你可以用更灵活的方式展示组件的用法和示例。

    {/* src/components/MyButton.stories.mdx */}
    import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
    import MyButton from './MyButton.vue';
    
    <Meta title="Components/MyButton" component={MyButton} />
    
    # MyButton
    
    这是一个按钮组件,可以自定义文本、大小和颜色。
    
    <Canvas>
      <Story name="Primary">
        <MyButton primary label="Primary Button" />
      </Story>
    </Canvas>
    
    <ArgsTable of={MyButton} />
    • Meta: 定义故事的元数据,比如标题和组件。
    • Story: 定义一个故事。
    • Canvas: 创建一个画布,用于渲染故事。
    • ArgsTable: 自动生成属性表格。
  2. 使用play函数进行交互测试:

    @storybook/addon-interactions 允许你编写 play 函数,模拟用户与组件的交互,并使用 Testing Library 进行断言。

    import MyButton from './MyButton.vue';
    import { fireEvent, within } from '@storybook/testing-library';
    import { expect } from '@storybook/jest';
    
    export default {
      title: 'Components/MyButton',
      component: MyButton,
      argTypes: {
        onClick: { action: 'clicked' },
      },
    };
    
    const Template = (args) => ({
      components: { MyButton },
      setup() {
        return { args };
      },
      template: '<MyButton v-bind="args" @click="args.onClick" />',
    });
    
    export const Interaction = Template.bind({});
    Interaction.args = {
      label: 'Click Me',
    };
    Interaction.play = async ({ canvasElement }) => {
      const canvas = within(canvasElement);
      const button = await canvas.getByRole('button', { name: 'Click Me' });
      await fireEvent.click(button);
      await expect(button).toBeInTheDocument(); // 一个简单的断言
    };
    • play: 一个异步函数,它接收一个对象作为参数,这个对象包含了当前故事的上下文信息,比如画布元素。
    • within: 一个函数,它可以让你在指定的元素范围内查找元素。
    • fireEvent: 一个函数,它可以让你模拟用户与元素的交互,比如点击、输入等。
    • expect: 一个函数,它可以让你进行断言,验证组件的行为是否符合预期。
  3. 使用 ThemeProvider 集成主题:

    如果你希望 Storybook 中的组件展示不同的主题,你可以使用 Vue 的 provide/inject 特性,结合 Storybook 的 decorators 来实现。

    {/* src/components/ThemeProvider.vue */}
    <template>
      <slot />
    </template>
    
    <script>
    import { provide } from 'vue';
    
    export default {
      props: {
        theme: {
          type: Object,
          required: true,
        },
      },
      setup(props) {
        provide('theme', props.theme);
        return {};
      },
    };
    </script>
    {/* src/components/MyComponent.vue */}
    <template>
      <div :style="themedStyles">
        {{ message }}
      </div>
    </template>
    
    <script>
    import { inject, computed } from 'vue';
    
    export default {
      props: {
        message: {
          type: String,
          default: 'Hello World',
        },
      },
      setup() {
        const theme = inject('theme');
    
        const themedStyles = computed(() => ({
          backgroundColor: theme.value.background,
          color: theme.value.text,
          padding: '10px',
        }));
    
        return { themedStyles };
      },
    };
    </script>
    {/* src/components/MyComponent.stories.js */}
    import MyComponent from './MyComponent.vue';
    import ThemeProvider from './ThemeProvider.vue';
    
    export default {
      title: 'Components/MyComponent',
      component: MyComponent,
      decorators: [
        (story, context) => {
          const theme = {
            background: context.globals.backgrounds?.value || 'white',
            text: context.globals.textColor?.value || 'black',
          };
    
          return {
            components: { ThemeProvider, story },
            template: `<ThemeProvider :theme="theme"><story /></ThemeProvider>`,
          };
        },
      ],
      parameters: {
        backgrounds: {
          values: [
            { name: 'light', value: 'white' },
            { name: 'dark', value: 'black' },
          ],
        },
        textColor: {
          values: [
            { name: 'light', value: 'black' },
            { name: 'dark', value: 'white' },
          ],
        },
      },
      globals: {
        backgrounds: { value: 'light' },
        textColor: { value: 'dark' },
      },
    };
    
    const Template = (args) => ({
      components: { MyComponent },
      setup() {
        return { args };
      },
      template: '<MyComponent v-bind="args" />',
    });
    
    export const Default = Template.bind({});
    Default.args = {
      message: 'Themed Component',
    };

    在这个例子中,ThemeProvider 组件接收一个 theme prop,并通过 provide 将其注入到组件树中。MyComponent 组件通过 inject 接收 theme,并根据主题动态地计算样式。在 Storybook 中,我们使用 decoratorsThemeProvider 包裹在每个故事周围,并根据 Storybook 的全局参数动态地设置主题。

第五幕:组件库文档和测试平台的终极目标

功能 描述
组件展示 清晰地展示组件的各种状态和用法,包括属性、事件、插槽等。
交互式演示 允许用户在浏览器中实时调整组件的属性,查看组件的变化,模拟用户的交互行为。
自动生成文档 根据组件的源代码和故事文件,自动生成API文档,包括属性的类型、默认值、描述等。
自动化测试 通过编写 play 函数,模拟用户与组件的交互,并使用 Testing Library 进行断言,验证组件的行为是否符合预期。
主题定制 允许用户自定义 Storybook 的主题,使其与组件库的风格保持一致。
版本控制 将 Storybook 集成到版本控制系统中,方便团队协作和版本管理。
部署与分享 将 Storybook 部署到服务器上,方便团队成员和用户访问。

总结

有了Storybook,你的Vue组件库就能拥有一个强大的“大脑”和“心脏”,不仅能让组件的开发和测试效率大大提高,还能让你的组件库更容易被理解和使用。所以,赶紧行动起来,让你的组件库也拥有一个属于自己的Storybook吧!

好了,今天的“讲座”就到这里,希望对大家有所帮助!记住,编程的乐趣在于不断学习和尝试,遇到问题不要怕,大胆地去探索,你会发现编程的世界充满了惊喜!下次有机会再和大家分享其他的技术心得,拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注