大数跨境

React 和 TypeScript 组件库,第 5 部分:使用 Storybook 编写组件文档

React 和 TypeScript 组件库,第 5 部分:使用 Storybook 编写组件文档 索引目录
2025-05-30
1
导读:关注【索引目录】服务号,更多精彩内容等你来探索!设置首先,通过在终端中运行以下命令来完成初始故事书配置的设置:npx storybook init。

关注【索引目录】服务号,更多精彩内容等你来探索!

设置

首先,通过在终端中运行以下命令来完成初始故事书配置的设置:npx storybook init
此命令将检测项目依赖项并显示三个步骤:

  • storybook
    指示将添加哪个版本
  • 询问项目是否使用webpackvite
  • 询问是否storybook用于文档或测试

关于第一点,请继续。对于第二点,该项目使用rollup,因此实际上两个选项都不直接适用,但由于storybook它没有与 直接集成rollupvite因此将选择 选项,该选项将用于运行storybook。对于最后一个问题,请指定 storybook 将用于文档。
完成此过程后,将自动添加允许运行 storybook 的库。
在撰写本文时,它生成了以下版本:

"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-onboarding": "^8.6.12",
"@storybook/blocks": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
"@storybook/test": "^8.6.12",
"eslint-plugin-storybook": "^0.12.0",
"storybook": "^8.6.12",

并且在 中package.json会自动添加两个脚本:

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

第一个脚本在本地运行故事书,第二个脚本构建故事书。

它还会生成一个.storybook文件夹,其中包含两个允许 Storybook 运行的配置文件:main.tspreview.ts
此外,stories还会创建一个文件夹,其中包含一些示例文档。由于我们不会使用它,因此将删除此文件夹。

这样,如果应用使用 Vite,storybook 就已经成功配置了。但是,由于它使用了 Rollup,因此需要添加两个库才能使其成功运行:

yarn add vite @storybook/builder-vite --dev

在撰写本文时,已生成以下版本:

"@storybook/builder-vite": "^8.6.12",
"vite": "^6.3.5",

组件修改

在继续文档之前,我们将对库组件进行调整,定义默认属性并进行相应的调整。
从以下内容开始Tag.tsx

  • 定义默认 props 值并删除textWeight(因为已经有一个属性可以自定义控制文本的字体粗细:)textFontWeight
// ...
const Tag = ({
  text = "Tag",
  type = "default",
  textColor = "#fff",
  textFontWeight = 600,
  textFontSize,
  textFontFamily,
  backgroundColor,
  format = "semiRounded",
  borderRadius,
  size = "medium",
  padding,
}: TagProps) => {
  • 调整组件的类型定义,因为textWeight不再是组件中的 props,默认format是 semiRounded:
// ...
export interface TagProps {
  type?: "default" | "success" | "alert" | "error";
  text: string;
  textColor?: string;
  textFontWeight?: number;
  textFontSize?: string;
  textFontFamily?: string;
  backgroundColor?: string;
  format?: "square" | "semiRounded" | "rounded";
  borderRadius?: string;
  size?: "small" | "medium" | "large";
  padding?: string;
}

export interface StyledTagProps {
  $type?: "default" | "success" | "alert" | "error";
  $textColor?: string;
  $textFontWeight?: number;
  $textFontSize?: string;
  $textFontFamily?: string;
  $backgroundColor?: string;
  $format?: "square" | "semiRounded" | "rounded";
  $borderRadius?: string;
  $size?: "small" | "medium" | "large";
  $padding?: string;
}
  • 使用 styled-components 调整属性定义,以更清楚地考虑 small size、 defaulttype和 square format
// ...
export const StyledTag = styled.div<StyledTagProps>`
  border: none;
  padding: ${(props) =>
    props.$padding
      ? props.$padding
      : props.$size === "large"
        ? "15px 20px"
        : props.$size === "medium"
          ? "10px 12px"
          : props.$size === "small" && "7px"};
  background-color: ${(props) =>
    props.$backgroundColor
      ? props.$backgroundColor
      : props.$type === "error"
        ? "#e97451"
        : props.$type === "alert"
          ? "#f8de7e"
          : props.$type === "success"
            ? "#50c878"
            : props.$type === "default" && "#d3d3d3"};
  pointer-events: none;
  border-radius: ${(props) =>
    props.$borderRadius
      ? props.$borderRadius
      : props.$format === "rounded"
        ? "30px"
        : props.$format === "semiRounded"
          ? "5px"
          : props.$format === "square" && "0"};
  width: fit-content;
`;
  • 在组件的渲染中进行调整,根据textFontSizeprops或者sizeprops设置文本字体大小:
// ...
}: TagProps) => {
  const fontSize = textFontSize
    ? textFontSize
    : size === "large"
      ? "18px"
      : size === "small"
        ? "14px"
        : "16px";

  return (
    <StyledTag
      data-testid="tag"
      $type={type}
      $backgroundColor={backgroundColor}
      $format={format}
      $borderRadius={borderRadius}
      $size={size}
      $padding={padding}
    >
      <Text
        color={textColor}
        fontWeight={textFontWeight}
        fontSize={fontSize}
        fontFamily={textFontFamily}
      >
        {text}
      </Text>
    </StyledTag>
  );
};

由于组件的默认属性Tag已更改,因此需要更新其测试文件Tag.test.tsx

  • 调整expect第一个测试中的语句以验证默认属性:
it("should render component with default properties", () => {
  render(<Tag text="Tag" />);

  const element = screen.getByTestId("tag");

  expect(element).toBeInTheDocument();
  expect(element).toHaveStyleRule("background-color", "#d3d3d3");
  expect(element).toHaveStyleRule("border-radius", "0");
  expect(element).toHaveStyleRule("padding", "7px");    
  expect(within(element).getByText("Tag")).toHaveStyleRule("color", "#fff");
});
  • 删除测试should render component with bold text font weight,因为它所依赖的 props 已被删除。

现在关于Text.tsx

  • 默认 prop 值的定义:
// ...
const Text = ({
  children,
  color="#000",
  weight="normal",
  fontWeight,
  fontSize="16px",
  fontFamily,
}: TextProps) => (
  • 使用 styled-components 调整属性定义,以便在某些情况下仅当提供相关 props 时才设置属性:
// ...
export const StyledText = styled.span<StyledTextProps>`
  ${(props) => props.$color && `color: ${props.$color};`}
  ${(props) => props.$fontSize && `font-size: ${props.$fontSize};`}
  font-weight: ${(props) =>
    props.$fontWeight
      ? props.$fontWeight
      : props.$weight
        ? props.$weight
        : "normal"};
  ${(props) => props.$fontFamily && `font-family: ${props.$fontFamily};`}
`;

组件文档

对于组件文档,将为每个组件创建两种类型的文件:

  • 故事:定义文档中显示的定制场景
  • mdx:组件文档,其中将写入文本并嵌入故事文件中的场景

在进入文档之前,我们将创建一个组件,用于集中文档中渲染的各个组件,并在它们之间添加一个小空间。组件StorybookContainer内将创建一个名为 的文件夹src/components

  • 文件StorybookContainer.tsx
import React from "react";
import styled from "styled-components";

export interface StorybookContainerProps {
  children: React.ReactNode;
}

export const StyledStorybookContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
`;

const StorybookContainer = ({ children }: StorybookContainerProps) => (
  <StyledStorybookContainer>
    {children}
  </StyledStorybookContainer>
);

export default StorybookContainer;
  • 文件index.ts
export { default } from "./StorybookContainer";

现在,我们可以通过在文件夹中创建以下文件来开始记录 Tag 组件src/components/Tag

  • 标签.stories.tsx
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import Tag from "./Tag";
import StorybookContainer from "../StorybookContainer/StorybookContainer";

const meta: Meta<typeof Tag> = {
  title: "Tag",
  component: Tag,
  argTypes: {},
};

export default meta;

type Story = StoryObj<typeof Tag>;

export const Default: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
    </StorybookContainer>
  ),
};

export const PredefinedType: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} type="success" />
      <Tag {...args} type="alert" />
      <Tag {...args} type="error" />
    </StorybookContainer>
  ),
};

export const PredefinedFormat: Story = {
  args: {
    format: "square",
  },
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} format="semiRounded" />
      <Tag {...args} format="rounded" />
    </StorybookContainer>
  ),
};

export const PredefinedSize: Story = {
  args: {
    size: "small",
  },
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} size="medium" />
      <Tag {...args} size="large" />
    </StorybookContainer>
  ),
};

export const Text: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} text="Example" />
    </StorybookContainer>
  ),
};

export const BackgroundColor: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} backgroundColor="#3b82f6" />
    </StorybookContainer>
  ),
};

export const CustomFormat: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} borderRadius="15px 2px" />
    </StorybookContainer>
  ),
};

export const CustomSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} padding="20px 40px" />
    </StorybookContainer>
  ),
};

export const TextColor: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textColor="#000" />
    </StorybookContainer>
  ),
};

export const TextFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontWeight={100} />
    </StorybookContainer>
  ),
};

export const TextFontFamily: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontFamily="Arial" />
    </StorybookContainer>
  ),
};

export const TextFontSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontSize="20px" />
    </StorybookContainer>
  ),
};

在其中的 下Meta,定义了要记录的组件,以及将出现在 Storybook 侧边栏菜单中的标题。每个标题都export const定义了一个将包含在文档中的场景。引用两个示例场景,第一个场景Default将使用其默认属性渲染组件。第二个场景PredefinedType将展示组件如何根据type属性的变化进行操作。

  • 标签.mdx
import { Canvas, Controls, Meta } from "@storybook/blocks";
import * as Stories from "./Tag.stories";

<Meta of={Stories} />

# Tag

Tag base component.

<Canvas of={Stories.Default} withToolbar />

<Controls of={Stories.Default} />

## Predefined properties

### Type

There are four type predefined properties: default, success, alert and error.

<Canvas of={Stories.PredefinedType} withToolbar />

### Format

There are three format predefined properties: square, semiRounded (default) and rounded.

<Canvas of={Stories.PredefinedFormat} withToolbar />

### Size

There are three size predefined properties: small, medium (default) and large.

<Canvas of={Stories.PredefinedSize} withToolbar />

## Custom properties

### Text

Tag text can be modified.

<Canvas of={Stories.Text} withToolbar />

### Background Color

Tag background color can be modified.

<Canvas of={Stories.BackgroundColor} withToolbar />

### Format

Tag format can be modified by border radius definition.

<Canvas of={Stories.CustomFormat} withToolbar />

### Size

Tag size can be modified by padding definition.

<Canvas of={Stories.CustomSize} withToolbar />

### Text Color

Tag text color can be modified.

<Canvas of={Stories.TextColor} withToolbar />

### Text Font Weight

Tag text font weight can be modified.

<Canvas of={Stories.TextFontWeight} withToolbar />

### Text Font Family

Tag text font family can be modified.

<Canvas of={Stories.TextFontFamily} withToolbar />

### Text Font Size

Tag text font size can be modified.

<Canvas of={Stories.TextFontSize} withToolbar />

在上述文件中,<Canvas of={Stories.Default} withToolbar />使用其场景渲染组件Default。下方,<Controls of={Stories.Default} />显示一个表格,其中包含组件所有可供自定义的 props。通过调整这些 props,可以实时查看它们如何影响表格上方的组件。
在 中Predefined properties,显示了基于修改后的 props,应用程序本身定义关联的 CSS 的场景。
在中Custom properties,显示了修改后的 props 直接影响已定义的 CSS 的场景。

使用以下方式在本地运行故事书yarn storybook



现在,通过在文件夹中添加以下文件来创建文本组件的文档src/components/Text

  • 文本.故事.tsx
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import Text from "./Text";
import StorybookContainer from "../StorybookContainer/StorybookContainer";

const meta: Meta<typeof Text> = {
  title: "Text",
  component: Text,
  argTypes: {},
};

export default meta;

type Story = StoryObj<typeof Text>;

export const Default: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
    </StorybookContainer>
  ),
};

export const PredefinedFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} weight="bold">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const Color: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} color="#800080">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const CustomFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontWeight={900}>
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const FontSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontSize="30px">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const FontFamily: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontFamily="Arial">
        Text
      </Text>
    </StorybookContainer>
  ),
};
  • 文本.mdx
import { Canvas, Controls, Meta } from "@storybook/blocks";
import * as Stories from "./Text.stories";

<Meta of={Stories} />

# Text

Text base component.

<Canvas of={Stories.Default} withToolbar />

<Controls of={Stories.Default} />

## Predefined properties

### Font Weight

There are two font weight predefined properties: normal(default) and bold.

<Canvas of={Stories.PredefinedFontWeight} withToolbar />

## Custom properties

### Color

Text color can be modified.

<Canvas of={Stories.Color} withToolbar />

### Font Weight

Text font weight can be modified.

<Canvas of={Stories.CustomFontWeight} withToolbar />

### Font Size

Text font size can be modified.

<Canvas of={Stories.FontSize} withToolbar />

### Font Family

Text font family can be modified.

<Canvas of={Stories.FontFamily} withToolbar />

它看起来如下:



部署故事书

现在,我们已经为组件编写了文档,Storybook 也已在本地成功运行。但我们的目标并非是让文档仅供库的开发人员使用——它应该能够向考虑添加库的用户展示库提供的组件。为了实现这一点,我们将使用 Chromatic 来部署文档并使其易于访问,这是 Storybook 维护人员提供的一项发布服务。首先,我们将添加库:

yarn add chromatic --dev

在撰写本文时,它生成了以下版本:

"chromatic": "^12.0.0",

现在,按照以下步骤进行部署:

  • 前往注册页面
  • 选择Connect with GitHub(假设您的存储库托管在那里)
  • 注册后,选择Choose GitHub repo
  • 从您的 github 帐户中选择您的库的存储库。
  • 复制并保存下面显示的命令Publish your Storybook。此时,如果不在终端中运行此命令,您将无法继续,但在此之前,让我们先构建故事书
  • 在终端中运行yarn build-storybook
  • 然后,在终端中,运行 chromatic 提供的命令(每次需要部署时,都必须在构建后执行此命令)

一旦该过程成功完成,终端将确认 storybook 已发布,并在 下显示一个 URL View your Storybook at "url"
通过访问此 URL,您将能够查看组件文档。

自述文件.md

现在故事书已经发布,它所在的 URL 将被添加到应用程序的 README 中(我将在此处包含我生成的 URL,对于您来说,您应该在该位置插入您的 URL):



.gitignore 文件

在 .gitignore 文件中,storybook-static将添加要忽略的文件夹,因为它是在运行时生成的yarn build-storybook

dist
node_modules

*storybook.log
storybook-static

包.json

package.json由于新版本的库即将发布,因此里面的版本将更改为 0.5.0:

{
  "name": "react-example-lib",
  "version": "0.5.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/griseduardo/react-example-lib.git"
  },
  "scripts": {
    "build": "rollup -c --bundleConfigAsCjs",
    "lint-src": "eslint src",
    "lint-src-fix": "eslint src --fix",
    "lint-fix": "eslint --fix",
    "format-src": "prettier src --check",
    "format-src-fix": "prettier src --write",
    "format-fix": "prettier --write",
    "test": "jest",
    "prepare": "husky",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "lint-staged": {
    "src/components/**/*.{ts,tsx}": [
      "yarn lint-fix",
      "yarn format-fix"
    ],
    "src/components/**/*.tsx": "yarn test --findRelatedTests --bail"
  },
  "devDependencies": {
    "@babel/core": "^7.26.10",
    "@babel/preset-env": "^7.26.9",
    "@babel/preset-react": "^7.26.3",
    "@babel/preset-typescript": "^7.26.0",
    "@eslint/js": "^9.19.0",
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "11.1.6",
    "@storybook/addon-essentials": "^8.6.12",
    "@storybook/addon-interactions": "^8.6.12",
    "@storybook/addon-onboarding": "^8.6.12",
    "@storybook/blocks": "^8.6.12",
    "@storybook/builder-vite": "^8.6.12",
    "@storybook/react": "^8.6.12",
    "@storybook/react-vite": "^8.6.12",
    "@storybook/test": "^8.6.12",
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.2.0",
    "@types/jest": "^29.5.14",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "chromatic": "^12.0.0",
    "eslint": "^9.19.0",
    "eslint-plugin-react": "^7.37.4",
    "eslint-plugin-storybook": "^0.12.0",
    "husky": "^9.1.7",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "jest-styled-components": "^7.2.0",
    "lint-staged": "^15.5.0",
    "prettier": "^3.4.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "storybook": "^8.6.12",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3",
    "typescript-eslint": "^8.23.0",
    "vite": "^6.3.5"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  },
  "eslintConfig": {
    "extends": [
      "plugin:storybook/recommended"
    ]
  }
}

CHANGELOG 文件

由于即将发布新版本,因此CHANGELOG.md将更新已更改的内容:

## 0.5.0

_May. 29, 2025_

- change Tag and Text default behavior
- add storybook
- add Tag and Text storybook docs

## 0.4.0

_Abr. 29, 2025_

- setup husky and lint-staged
- define pre-commit actions

## 0.3.0

_Mar. 24, 2025_

- setup jest and testing-library
- add components tests

## 0.2.0

_Fev. 24, 2025_

- setup typescript-eslint and prettier
- add custom rules

## 0.1.0

_Jan. 29, 2025_

- initial config

文件夹结构

文件夹结构如下:



发布新版本

第一步是检查汇总构建是否成功运行。为此,请yarn build在终端中运行,如 中所述package.json。如果构建成功,请继续使用以下命令发布新版本的库:npm publish --access public

结论

本文的目的是设置 Storybook,为应用程序中的组件创建文档并进行部署,因为文档对于展示库提供的组件非常重要。以下是GitHub上的代码库以及npmjs上包含新修改的库。


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读1.2k
粉丝0
内容444