在使用 React开发组件时经常会有一些苦恼,比如当一个组件的复杂度逐步上升时,它所拥有的状态不容易追溯;当需要查看某种状态的组件时,可能需要手动更改组件的属性或是更改接口返回的数据(数据驱动的组件)等等。于是我就去了解并学习 Storybook,然后组织了一次分享会,这也是我们团队的第一次技术分享。
关于 Storybook,我在一两年前有接触并尝试使用,当时对组件化开发的理解可能有限,只是为了用而用,并未感受到它的实用之处;加上经过多次的迭代,Storybook已经到了 6.0 版本,可以说是更易用、更优雅了。

动机
- 新项目的 UI 系统需要重新设计
- 项目迭代,组件复杂度逐步变高,组件状态不容易追溯
- 追求更优雅、更具维护性的编码方式
目标
这篇文章主要给大家分享一下几点:
- 介绍 Storybook
- 通过一个小例子展示如何在 Next.js 中使用 Storybook
- 我的代码编写习惯
要求
因为包含了实践,可能有以下几点要求,不过不用担心,只要你能看懂就行:
- 示例是基于 Next.js 的,这个我在上一篇文章中有讲到如何搭建 Next.js 项目,可以点击这里把我搭建的脚手架克隆到本地,以便可以跟着动手。
- 因为是基于上一篇文章所搭建的脚手架,所以它所拥有的特性也需要了解,比如 Typescript、styled-component。
storybook是一个开源工具,为React、Vue、Angular等框架提供一个沙箱环境,可独立地开发UI组件;它更有组织和高效地构建出令人惊叹的 UIs。

提供强大的 UIs
-
独立构建组件
创建组件时不需要竖起屏幕,不需要处理数据,也不需要构建业务逻辑。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
模拟难以达到的用例
在一个应用中渲染关键状态是不容易的
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
用例作为一个故事
将用例保存为 Javascript 中的故事,以便在开发、测试和QA期间重新访问。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
使用插件减少工作流程
使用插件可以更快地构建UI,组件文档化,并简化工作流程。

组件更具可靠性
-
确保一致的用户体验
每当写一个故事,就得到一种状态的视觉效果。快速地浏览故事,检查最贱 UI 的正确性。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
自动回归测试代码
使用官方插件 Storyshots 启动代码快照。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
单元测试组件
对组件进行单元测试确保组件能正常工作。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
基于每次提交像素级地捕获UI变化
用视觉测试工具查明UI的变化。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的?
分享和重用所有东西
-
在项目中查找任何组件
Storybook 可搜索编写的任何组件,为你的UI组件提供真实信息的单一来源。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
开发过程中获得及时反馈
通过 Storybook 部署到云端,与团队协作实现UI。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
跨端跨应用共享组件
每个故事都是一个用例,团队成员可以找到它并决定是否重用。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的? -
生成文档
编写 markdown/MDX,为组件库和设计系统生成可定制化的文档。
我是如何在 Nextjs 项目中使用Storybook驱动组件开发的?
使用 Storybook
下面我会通过一个示例想大家展示 Storybook 是如何工作的,期间也能看到我是如何使用结合 Typescript、styled-components以及我的编码习惯。
安装
假设你已经克隆了这个仓库,首先在项目中安装 storybook:
# 安装 storybookyarn add storybook# 初始化 storybook 项目,会根据项目类型自动地进行配置npx sb init# 启动 storybook 服务yarn storybook复制代码
以上几部没问题之后,现在就可以在 http://localhost:6006/ 访问 Storybook 提供的 UIs 了:

它默认提供了几个例子,如 Button、Header等,例子代码在 src/pages/stories 中:

后缀名为 stories.tsx 的文件就是一个故事,它定义了我们想要定义的组件的表现状态;大家可能不是很理解一个故事是什么,后面大家看了示例之后就会理解了,我先打个比方,一个人就好比一个故事,当他有不同的心情时,就会表现出不同的表情,同一时间只能看到它的一种表情,但我现在用照片记录他所表现的一个个不同的表情,这有利于我去分析这个人的性格;Storybook 就像是照相机,可以记录组件的不同状态,便于我们去追溯。
设计 ProductOptimCard 组件
接下来设计并实现 ProductOptimCard 组件,这个组件是数据驱动的,也就是内容是根据数据的变化而变化的,为了方便,我只定义了标题、是否必做、是否完成这三个属性,它们的变化会展示不同状态下的视图,默认的效果如下:

以下是组件实现代码:
// src/components/towone/ProductOptim/ProductOptimCard/index.tsx
import React from 'react';
import styled from 'styled-components';
interface IProductOptimCardProps {
data: {
isMustDo: boolean;
isFinish: boolean;
title: string;
};
}
const Container = styled.div`
width: 452px;
height: 276px;
background: #fefeff;
border: 1px solid #edf0fa;
box-shadow: 0px 4px 14px 0px rgba(0, 10, 71, 0.07);
`;
const Content = styled.div`
height: 225px;
background: #fff;
padding-top: 21px;
padding-left: 20px;
position: relative;
`;
const Footer = styled.div`
height: 50px;
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 10px;
padding-left: 20px;
`;
const Title = styled.div`
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 14px;
`;
const Badge = styled.div<{ isMustDo: boolean }>`
width: 37px;
height: 21px;
background: ${({ isMustDo }) => (isMustDo ? '#0af' : '#999999')};
font-weight: bold;
color: #fefeff;
font-size: 12px;
border-radius: 11px 2px 11px 11px;
position: absolute;
top: 10px;
right: 10px;
display: flex;
align-items: center;
justify-content: center;
`;
const Text = styled.div`
font-size: 14px;
color: #666666;
margin-bottom: 14px;
`;
const MoreText = styled.a`
font-size: 14px;
color: #333333;
`;
const FinishButton = styled.div<{ isFinish: boolean }>`
width: 60px;
height: 28px;
background: ${({ isFinish }) => (isFinish ? '#999' : '#046eff')};
color: #fefeff;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
`;
const ProductOptimCard: React.FC<IProductOptimCardProps> = ({ data }) => {
const { isMustDo, isFinish, title } = data;
return (
<Container>
<Content>
<Title>{title}</Title>
<Text>1、尺寸:800 x 800px</Text>
<Text>2、卖点提炼文字展示(针对同款多、标品类目)</Text>
<Text>3、产品占图片三分之二</Text>
<Text>4、参考五家淘宝以及阿里优秀类似款主图(按成交金额排序)</Text>
<Badge isMustDo={isMustDo}>必做</Badge>
</Content>
<Footer>
<MoreText>更多教程</MoreText>
<FinishButton isFinish={isFinish}>完成了</FinishButton>
</Footer>
</Container>
);
};
export default ProductOptimCard;复制代码
然后在首页引入它:
// src/pages/index.tsx
//...
export default function Home() {
return (
<Conotainer>
<ProductOptimCard
data={{ isMustDo: false, isFinish: false, title: '单品标题优化' }}
/>
</Conotainer>
);
}复制代码
执行 yarn dev 启动项目,然后打开 http://localhost:3000/ 查看:

图中红框中的组件就是 ProductOptimCard 的默认样式,组件本身已经实现了不同状态:如必做、不必做、已完成、未完成;但我想查看某个状态,将不得不更改 src/pages/index.tsx 中传给 ProductOptimCard 的 data 属性,而这个通常是根据接口返回的数据,要去该代码就显得麻烦不优雅了,不过不用担心,我们现在有 Storybook了,请往下看。
在同级目录新建一个 ProductOptimCard.stories.tsx 文件,为 ProductOptimCard 编写故事,代码如下:
import React, { ComponentProps } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import ProductOptimCard from './';
export default {
title: 'TWOONE/ProductOptim/ProductOptimCard',
component: ProductOptimCard,
} as Meta;
const Template: Story<ComponentProps<typeof ProductOptimCard>> = (args) => (
<ProductOptimCard {...args} />
);
export const DefaultCard = Template.bind({});
DefaultCard.args = {
data: {
isMustDo: false,
isFinish: false,
title: '单品标题优化',
},
};
export const MustDoCard = Template.bind({});
MustDoCard.args = {
data: {
isMustDo: true,
isFinish: false,
title: '单品标题优化',
},
};
export const FinishCard = Template.bind({});
FinishCard.args = {
data: {
isMustDo: false,
isFinish: true,
title: '单品标题优化',
},
};
export const UnFinishCard = Template.bind({});
UnFinishCard.args = {
data: {
isMustDo: false,
isFinish: false,
title: '单品标题优化',
},
};复制代码
我们引入了 ProductOptimCard,并为其编写了四种状态,分别是 DefaultCard、MustDoCard、FinishCard、UnFinishCard,传入不同的data,自然会表现出不同的状态。然后打开 http://localhost:6006/:

红框是我们为 ProductOptimCard 编写的故事,点击不同状态以查看 UI 效果:

可以看到,我们很容易就知道并查看这个组件的不同状态,是不是有点跃跃欲试了呢,点击 Docs 可查看文档,其它操作就大家课后自己尝试:

项目中如有使用 alias 为文件夹设置别名,导入形式是这样 import { Box } from '@/styles/common';,这通常是在我们的 tsconfig.json 中已经配置了,但是 storybook 不认识,也需要配置一下,它支持我们自定义 webpack 配置,打开 .storybook/main.js,添加如下代码:
// .storybook/main.jsconst path = require('path');module.exports = { // ...
webpackFinal: async (config, { configType }) => {
config.resolve.alias['@'] = path.resolve(__dirname, '../src'); return config;
},
};复制代码
到这里我们已经通过一个示例来了解如何使用 Storybook 了,接下来会简单聊聊我的一些编码心得。
我的编码习惯与心得
分类
从数据获取的层面看,我将组件分为容器组件和内容组件:
**容器组件:**从接口获取数据。
**内容组件:**接收 props 数据、可编写 story 组件驱动开发。
story组件编写的大致顺序
- Typescript 定义组件接收的参数
- 为可选的类型设置默认值
- 编写 story 描述不同状态的组件
组件编写顺序
通常一个组件引入的三方库在最顶部,其次是自定义组件,所以我这里的顺序值得是组件中变量定义的位置,以下是我所习惯的定义顺序(从上往下),每个区域隔一行:
- 三方库
- 自定义组件
- 图片常量
- Typescript 接口
- 样式组件
import React from 'react';
import styled from 'styled-components';
import { MySelfComp } from '@/components';
import ICON_LOGO from '@/assets/images/icon.logo.png';
interface IProps {}
const Container = styled.div``;
const DemoComp: React.FC<IProps> = () => {
return <Container></Container>
}
export default DemoComp;复制代码