JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第61章—实战篇-Storybook与UI开发

首次发表于 2024-07-29, 更新于 2024-07-29

前言

本篇我们介绍 Storybook,根据官网的介绍,它是:

Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.

Storybook is the most popular UI component development tool for React, Vue, and Angular. It helps you develop and design UI components outside your app in an isolated environment.

简单总结一下就是:Storybook 是一个支持 React、Vue、Angular 的 UI 组件开发工具,独立于开发环境,常被用于 UI 开发、组件测试和编写组件文档

之所以出现 Storybook,是因为传统开发的 UI 组件,随着不断迭代越发复杂,组件所拥有的状态不再容易追溯。当你需要查看组件的某种状态时,可能需要麻烦的运行项目、Mock 接口数据亦或者手动修改组件 props 才能查看效果,查看完后还要再更改回来……这就造成了组件开发和使用的繁琐。而 Storybook 可以独立于开发环境,让你能够快捷查看到组件的不同状态。

目前 Storybook GitHub 83k Stars,Npm 周均下载量 2400W,算是这个细分领域最主流的技术选型了。

只谈论背景和功能还是比较抽象,我们以 Next.js 项目为例,具体讲解 Storybook 如何使用。

Storybook

1. 初始化

使用 Next.js 官方脚手架创建一个新项目:

npx create-next-app@latest

初始化 Storybook:

npx storybook@latest init

Storybook 会自动探测项目类型,根据 Next.js 项目类型安装依赖项,创建 .storybookstories两个文件夹,其中:

  1. .storybook下的文件是配置文件,声明了 Storybook 的运行规则,比如读取哪些文件作为 story
  2. stories 下的文件是组件文件,初始化创建的组件文件只是用作示例代码,展示 story 的写法和使用方式

同时package.json 也会写入了两个脚本命令:

{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  }
}

安装完成后,会自动运行 npm run storybook,自动打开 http://localhost:6006/

image.png

2. 支持 Tailwind.css

如果我们书写自己的组件,很有可能用到 Tailwind.css。为了让 Tailwind.css 在 Storybook 中生效,我们还需要额外做一点修改。

修改 tailwind.config.js,代码如下:

// 添加 stories 目录
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./stories/**/*.{js,ts,jsx,tsx}"
  ],
  theme: {
    extend: {
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
        "gradient-conic":
          "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
      },
    },
  },
  plugins: [],
};

修改 next-storybook/.storybook/preview.js,添加代码如下:

// 引入全局 CSS
import "../app/globals.css";

const preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

3. 组件与 Story

为了由浅入深的讲解如何写 Storybook 组件,我们先删除掉 stories 文件夹下的内容,从头开始实现。

我们以 Ant-Design 的 <Button> 组件为例。根据 type 属性的值不同,Ant-Design 提供了五种基本类型:

image.png

我们尝试模仿下这个效果。但为了避免复杂,我们就先完成主按钮、次按钮类型。

新建 stories/Button.jsx,代码如下:

import PropTypes from "prop-types";

const Button = ({ label, type = "default", ...props }) => {

  let classnames = "bg-white text-black border border-gray-300 border-solid hover:border-sky-600 hover:text-sky-600";
  if (type == 'primary') {
    classnames = "bg-sky-600 text-white "
  }

  return (
    <button type="button" className={`inline-flex items-center gap-2 rounded-md py-1.5 px-3 text-sm/6 shadow-inner shadow-white/10 ${classnames}`} {...props}>
      { label }
    </button>
  );
};

Button.propTypes = {
  /**
   * 按钮文案
   */
  label: PropTypes.string,
  /**
   * 按钮类型
   */
  type: PropTypes.string,
  /**
   * 可选点击事件
   */
  onClick: PropTypes.func,
};

export default Button;

新建 stories/Button.stories.js,代码如下:

import Button from './Button';

export default {
  title: 'Button',
  component: Button,
};

export const Primary = () => <Button type="primary" label="Primary Button" />;

export const Default = () => <Button type="default" label="Default Button" />;

在这段代码中,我们引入了 Button 组件的代码,导出了两种状态的组件。

此时打开 http://localhost:6006/

9.gif

在这个页面,我们可以预览组件的两种状态。这就是 Storybook 最基本的两个组织级别:组件和它的子故事(stroies)。也就是说,每一个组件都有多个 story,共同组成了“storybook”。

注意下方的 Controls 面板,里面的值(比如 label、type、onClick)可以理解,是根据 Button.propTypes 生成,可是当组件切换的时候,这些值的内容并未发生改变。如果你需要定义这些值,并希望能够随时修改,修改 stories/Button.stories.js,代码如下:

import Button from './Button';

export default {
  title: 'Button',
  component: Button
};

export const Primary = {
  args: {
    type: "primary",
    label: "Primary Button",
  },
};

export const Default = {
  args: {
    type: "default",
    label: "Default Button",
  },
};

此时浏览器效果如下:

10.gif

我们甚至可以在面板中修改传入的值,并实时查看效果。

4. 组件设置

其实 Storybook 提供了非常丰富的设置选项,继续修改 stories/Button.stories.js,代码如下:

import { fn } from '@storybook/test';
import Button from './Button';

export default {
  title: 'Button',
  component: Button,
  parameters: {
    // 调整组件在 Canvas 的位置: https://storybook.js.org/docs/configure/story-layout
    layout: 'centered',
  },
  // 自动生成组件文档: https://storybook.js.org/docs/writing-docs/autodocs
  tags: ['autodocs'],
  // 组件参数类型: https://storybook.js.org/docs/api/argtypes
  argTypes: {
    type: { control: 'radio', options: ['primary', 'default'] }
  },
  // 组件默认传入参数:https://storybook.js.org/docs/essentials/actions#action-args
  args: { onClick: fn() },
};

export const Primary = {
  args: {
    type: "primary",
    label: "Primary Button"
  },
};

export const Default = {
  args: {
    type: "default",
    label: "Default Button"
  },
};

其中,parameters.layout 用于设置组件的位置,tags: ['autodocs']用于自动生成组件文档,查看效果时你会发现组件下多了一个 Docs 文档。argTypes用于设置组件的类型,之前都是输入框类型,在这里可以设置更多样的类型,就比如单选框。args 设置组件的默认传入参数。

此时效果如下:

11.gif

5. Story 设置

我们也可以设置 Story,继续修改 stories/Button.stories.js,代码如下:

import { fn, within, userEvent, expect } from '@storybook/test';
import Button from './Button';

export default {
  title: 'Button',
  component: Button,
  parameters: {
    // 调整组件在 Canvas 的位置: https://storybook.js.org/docs/configure/story-layout
    layout: 'centered',
  },
  // 组件自动生成文档: https://storybook.js.org/docs/writing-docs/autodocs
  tags: ['autodocs'],
  // 组件参数类型: https://storybook.js.org/docs/api/argtypes
  argTypes: {
    type: { control: 'radio', options: ['primary', 'default'] }
  },
  // 组件默认传入参数:https://storybook.js.org/docs/essentials/actions#action-args
  args: { onClick: fn() },
};

export const Primary = {
  args: {
    type: "primary",
    label: "Like"
  },
  render(args, { loaded: { todo } }) {
    return (
      <div>
        <span>{todo.title} 点赞: </span>
        <Button {...args} />
      </div>
    )
  },
  loaders: [
    async () => ({
      todo: await (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
    }),
  ],
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: /Like/i });
    await expect(button).toBeInTheDocument();
    await userEvent.click(button);
    await expect(button.textContent).toEqual('Like')
  }
};

export const Default = {
  args: {
    type: "default",
    label: "Default Button"
  },
};

其中,render 函数用于展示组件的最终渲染结果,loaders 用于渲染组件前加载数据,play 用于触发事件,常用于测试。

浏览器效果如下:

12.gif

6. 添加 MDX 文档

上节讲到可以自动生成组件文档,你也可以完全自定义组件文档,Storybook 支持 MDX 文档格式。

举个例子,先注释掉 stories/Button.stories.js 中的 tags: ['autodocs'] 这行,因为它会与你自定义的文档造成冲突。

然后新建 next-/stories/Button.mdx,代码如下:

import { Canvas, Meta } from '@storybook/blocks';

import * as ButtonStories from './Button.stories';

<Meta of={ButtonStories} />

# Button

这里展示了 Button 组件的不同样式

主按钮样式:

<Canvas of={ButtonStories.Primary} />

默认按钮样式:

<Canvas of={ButtonStories.Default} />

浏览器效果如下:

image.png

组件驱动开发

至此我们就展示了 Storybook 的基本功能:1. 展示 UI 2. 组件测试 3. 编写组件文档

其实使用 Storybook 的大前提是遵循组件驱动开发(Component Driven Development)的编程方式。

所谓组件驱动开发,将复杂的页面拆解为简单的组件,每个组件都有一个定义良好的 API 和一系列被模拟的固定状态。开发者就可以通过重组来构建不同的 UI。

那组件驱动开发的流程是什么样的呢?

  1. 编写单个组件:单独构建每个组件并定义其相关状态,比如 Avatar、Button、Input、Tooltip
  2. 组合复杂组件:将单个组件进行组合构建更复杂的组件,比如 Form、Header、List、Table
  3. 组装页面:通过组合复杂组件构建页面,使用模拟数据模拟边缘 Case,比如主页、设置页面、个人信息页面
  4. 集成到项目:将页面添加到项目中,与后端 API 和服务进行连接,比如 Web 应用、文档网站、商城网站

我们在运行 Storybook 初始化命令的时候,示例代码包含三个组件,分别是 Button、Header、Page,其实就代表了这样一个开发流程。

类似工具

类似于 Storybook 的组件文档工具还有 Docz(23.5k Star)、dumi(3.5k Star):

image.png

总的来说,Storybook 无论是在实现功能、支持的库、下载使用量、更新维护速度等多个方面都遥遥领先。虽然 Docz、dumi 也有其特点和优势,但就这一细分领域,Storybook 是毫无疑问的一家独大。

参考文档:

  1. https://storybook.js.org/docs/get-started/nextjs
  2. https://storybook.js.org/docs/api
  3. https://github.com/chromaui/intro-storybook-react-template/blob/master/src/stories/Button.stories.js
  4. https://www.componentdriven.org/
  5. https://npmtrends.com/docz-vs-dumi-vs-storybook
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru