在使用 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;複制代碼