create react app 如何通过 storybook
目的
希望能生成如下要求的文档,能便于组件的使用
- 能实时渲染组件
- 能查看代码示例
- 能查看组件的参数说明
- 列举组件的常用示例
环境
- react@16.8
- creat-react-app@4+
- storybook@6.3
- webpack@4+
安装
// 初始化
npx sb init
// 启动
npm run storybook
// 打包
npm run build-storybook
安装后会生成 .storybook 和 src/stories 的文件
- .storybook/main.js 编译的配置
- .storybook/preview.js 工具、插件等的配置
- src/stories 默认的示例
配置
main.js
在基于 create-react-app 生成的项目中需要处理 webpack 配置
// main.js 所有配置项
{
"stories": [] // 匹配 story 文档的规则
"addons": [], // 所有的插件列表
"webpackFinal": (config) => {}, // 可以扩展默认的 webpack 配置
"babel": (options) => {} // 可以扩展默认的 bebel 配置
}
// ./.storybook/main.js
const webpackConfig = require("../config/webpack.config.dev.js")
module.exports = {
stories: [
// "../src/stories/*.stories.mdx",
// "../src/stories/*.stories.@(js|jsx|ts|tsx)",
"../src/components/**/*.stories.mdx",
"../src/components/**/*.stories.js",
],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
webpackFinal: config => {
// 移除默认的 css loader 规则
config.module.rules.splice(7, 1)
removeWebpackDefaultFileLoader(webpackConfig)
let newConfig = {
...config,
module: {
...config.module,
rules: [...config.module.rules, ...webpackConfig.module.rules],
},
}
// console.log("old config", newConfig.module)
return newConfig
},
babel: async options => {
// console.log(options.plugins)
// 不返回值,默认使用 package.json 里的 babel 配置
},
}
// 配置文件参见 webpack.dll.config.js
// 移除默认的 file loader
function removeWebpackDefaultFileLoader(webpackConfig) {
webpackConfig.module.rules.forEach(rule => {
const oneKey = "oneOf"
if (Reflect.has(rule, oneKey)) {
rule[oneKey].splice(rule[oneKey].length - 1, 1)
}
})
}
// package.json bebel 配置
"babel": {
"presets": [
"react-app"
],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties",
[
"import",
{
"libraryName": "antd-mobile",
"style": "css"
}
]
]
}
preview.js
可以设置全局默认的 parameter、decorator、loader、全局变量 等等
// 设置默认的主题
export const parameters = {
docs: {
theme: themes.light
}
}
preview-head.html
用于在 preview 页面头部追加一些通用的内容
// 设置 rem 默认字体大小,重置 sb 的一些样式
<style>
html {
font-size: 26.6667vw;
}
/* reset story book style */
.sb-show-main.sb-main-padded {
padding: 16px;
}
.sbdocs-wrapper {
padding-top: 64px !important;
padding-bottom: 64px !important;
}
</style>
名词
- story
- component
- CSF
- Args
- Parameters
- Decorators
- Loaders
- DocsPage
- MDX
- Controls
- Actions
story
story 是描述如何渲染一个组件的函数,可以说是文档的最小结构
component
同一个组件的不同状态渲染(story),都包含在一个 componet 下
Args
定义 story 的参数,Story Args and Component Args
Parameters
关于 story 静态的命名的元数据,用来控制功能和插件,eg:parameters.backgrounds 用来控制工具栏插件的背景颜色
Decorators
可以在 story 外面包裹自定义的 dom 结构,方便定义 story 渲染
Loaders
Loaders 是一个用来给 story 加载数据的异步函数,比如从远程接口获取 story 渲染需要的数据
DocsPage
自动生成包含所有 stories 文档,基于 addon-docs 插件
CSF
Component Story Format 缩写,是指用 js 形式来定义 story 的语法格式
MDX
新的文件格式,包含 MarkDown 和 JSX,
Controls
图形化界面用来控制 story 渲染的状态,通过 Args 和 ArgsType 定义
Actions
用来显示 story 事件回调接收的数据
如何写
// FloatBoard.stories.js
import React from 'react'
import FloatBoard from './float_board'
// component parameter 组件的描述和属性
export default {
title: 'FloatBoard'
component: FloatBoard
}
// story
export const FB1 = (args) => <FloatBoard {...args} />
// actions
FB1.args = {
primary: 1
}
FB1.argTypes = {
type: {
otions: ['primary', 'secondary'],
control: 'select'
}
}
// FloatBoard.stories.mdx
import React from 'react'
import { Meta, Canvas, Story} from '@storybook/docs'
import FloatBoard from './float_board'
// component parameter 组件的描述和属性
<Meta
title='FloatBoard'
component={FloatBoard}
/>
// story
<Canvas>
<Story name="Base FloatBoard">
<FloatBoard ... />
</Story>
</Canvas>
MDX 的示例
import { Meta, Story, ArgsTable } from "@storybook/addon-docs"
import FloatBoard from "./index"
import Preview from "mt-docs/preview"
import { utils } from "common"
const floatData = {
bg_url: "http://f.mengtuiapp.com/common/share_150x150.png",
click_type: 0,
deadline: Date.now()/1000 + 12*60*60,
h5_popup: true,
link: "xxx",
on: true,
position: 0,
scale: 0,
subtitle: "subtitle",
ticktock: 2,
ticktock_bg_url: "http://f.mengtuiapp.com/common/share_150x150.png",
title: "title",
toast: "toast",
// swagger里未找到time的定义,无此参数倒计时不会显示
time: 0,
}
const scope = { floatData, FloatBoard }
const code = `
<FloatBoard
floatBoard={floatData}
/>
`
<Meta title="FloatBoard" component={FloatBoard} />
# FloatBoard
优惠券悬浮窗
## 基本功能
<Story name="基础用法" args={{floatBoard: floatData}}>
{(args) => <FloatBoard {...args} />}
</Story>
## Props
<ArgsTable story="基础用法" />
CSF 的示例
import React from 'react'
import FloatBoard from './index'
const floatData = {
bg_url: "http://f.mengtuiapp.com/common/share_150x150.png",
click_type: 0,
deadline: Date.now()/1000 + 12*60*60,
h5_popup: true,
link: "xxx",
on: true,
position: 0,
scale: 0,
subtitle: "subtitle",
ticktock: 2,
ticktock_bg_url: "http://f.mengtuiapp.com/common/share_150x150.png",
title: "title",
toast: "toast",
}
export default {
title: 'FloatBoard',
component: FloatBoard
}
export const MyFloatBoard = (args) => <FloatBoard {...args} />
MyFloatBoard.args = {
floatData
}
ArgsTable
归属于 addon-docs 插件,用来展示 ArgTypes 定义的属性列表,表格形式展示。使用 addon-docs 插件会默认给每个 sotry 生成一组 ArgTypes,它是从组件定义的 propTypes 和 defaultProps 获取,不论类组件、函数组件、高阶组件都可以生成。
用法
// MDX 形式
// 直接用于组件上,显示指定组件的属性
import ComponentA from 'xxx'
<ArgsTable of={ComponentA} />
// 用于 story 上,显示对应组件的属性
<Story name="mystory">
<ComponentA />
</Story>
<ArgsTable story="mysotry" />
'loose' mode configuration must be the same...
Error: .storybook/preview.js-generated-config-entry.js: 'loose' mode configuration must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled).
遇到的问题是在一个已有的 bebel 配置的项目加入 storybook,项目原有的 babel 配置和 storybook 默认的不一致,比如 plugin-proposal-class-properties 插件,它默认 option.loose=false,和默认 sb 的 babel 配置不一致。
需要确保 storybook 默认的 babel 配置和项目的是一致,修改 sb 的 babel 配置(plugins & presets) loose=false
即可。
// 原项目默认 babel plugin 配置
[
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties",
[
"import",
{
"libraryName": "antd-mobile",
"style": "css"
}
]
]
// sb 默认的 babel preset 配置
[
[
'@storybook/core-common/node_modules/@babel/preset-env',
{ shippedProposals: true, loose: true }
],
'@storybook/core-common/node_modules/@babel/preset-typescript',
[
'@storybook/react/node_modules/@babel/preset-react',
{}
],
'@babel/preset-flow'
]
// sb 默认的 babel plugin 配置
[
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-block-scoping',
[
'@babel/plugin-proposal-decorators',
{ legacy: true }
],
[
'@babel/plugin-proposal-class-properties',
{ loose: true }
],
[
'@babel/plugin-proposal-private-methods',
{ loose: true }
],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-syntax-dynamic-import',
[
'@babel/plugin-proposal-object-rest-spread',
{ loose: true, useBuiltIns: true }
],
'@babel/plugin-transform-classes',
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-parameters',
'@babel/plugin-transform-destructuring',
'@babel/plugin-transform-spread',
'@babel/plugin-transform-for-of',
'babel-plugin-macros/dist/index.js',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
[
'babel-plugin-polyfill-corejs3',
{
method: 'usage-global',
absoluteImports: 'core-js',
version: '3.18.3'
}
],
'babel-plugin-add-react-displayname'
]