原文鏈接
對于一名前端開發(fā)者,必須面對的就是組件化開發(fā)。我做Angular開發(fā)已經(jīng)有些日子了,也曾為自己的項(xiàng)目開發(fā)過通用組件,但僅是在項(xiàng)目內(nèi)部使用,而且是直接用業(yè)務(wù)界面對組件進(jìn)行測試。如果其他項(xiàng)目要使用這些組件,也是使用老土的拷貝方式來進(jìn)行復(fù)用。偶然發(fā)現(xiàn)StoryBook,研究了下,頓生好感,原來組件開發(fā)可以這么簡單的管理和測試,還可以編寫清晰明了的說明文檔,對提升組件開發(fā)的效率那是大大滴提升。
本文將以一個(gè)基于Material的三級(jí)選擇組件為例,進(jìn)行StoryBook實(shí)戰(zhàn),實(shí)實(shí)在在滴體驗(yàn)下StoryBook的強(qiáng)大。
術(shù)語解釋
StoryBook給開發(fā)這提供了一個(gè)強(qiáng)大的組件開發(fā)的生態(tài)環(huán)境,涉及組件的測試、實(shí)景展示、文檔,以及術(shù)語。常見術(shù)語解釋如下:
- Addon:類似Plugin,StoryBook中的功能組件以及擴(kuò)展以Addon形式存在,開發(fā)者亦可以自行編寫Addon來擴(kuò)展StoryBook的功能;
- Story:類似用例,是組件的各種使用場景;
- Decorator:就是給Story做個(gè)包裝,可以是樣式包裝、模塊元數(shù)據(jù)包裝、類型包裝等。
- Notes:備注,可以為每個(gè)Story設(shè)置Notes,支持MarkDown語法。
如下是常用的Addon:
- addon-actions:組件操作事件,如click、change
- addon-links:鏈接,如某個(gè)Story中單擊按鈕,鏈接到另一個(gè)Story中
- addon-notes:Story的備注
- addon-options:調(diào)整StoryBook的外觀
- addon-knobs:在頁面上改變變量
對于StoryBook入門內(nèi)容,在網(wǎng)上可以找到很多,同時(shí)StoryBook for Angular中也有入門級(jí)的詳細(xì)解釋,這里不贅述。 下面來個(gè)實(shí)戰(zhàn)演練。
創(chuàng)建自己的組件
三級(jí)級(jí)聯(lián)選擇組件,在項(xiàng)目中比較常用,比如省市區(qū)、多級(jí)分類等,通用的UI大多僅提供一級(jí)選擇組件。三級(jí)選擇則需要根據(jù)業(yè)務(wù)需求,開發(fā)者自己編寫。于是我自己寫了一個(gè)省市區(qū)的三級(jí)選擇的組件。
首先為這個(gè)組件創(chuàng)建了獨(dú)立的Angular工程:
ng new cityselect
cd cityselect
組件目錄如下:

組件名稱為MatCascaderComponent,期望的運(yùn)行效果如下:

StoryBook實(shí)戰(zhàn)
在cityselect中安裝StoryBook,命令如下:
npx -p @storybook/cli sb init --type angular
//同時(shí)安裝如下Addon
npm install --save @storybook/addon-options
執(zhí)行 npm run storybook 成功后,訪問 http://localhost:6006 可以看到StoryBook的界面以及缺省的Story。

寫個(gè)簡單的Story
StoryBook提供了兩種Story的寫法,第一種是直接使用組件,第二種是使用Html標(biāo)簽。
直接使用組件
由于組件使用到了Materail的相關(guān)Module,Story中需要先將這些外部Module引入,這里用到了:moduleMetadata。
可以單獨(dú)對每個(gè)Story設(shè)置moduleMetadata:
.add( "直接使用組件", () => ({
component: MatCascaderComponent, //直接使用組件
props: {},
//僅對當(dāng)前Story生效
moduleMetadata:{
imports: [BrowserAnimationsModule, MatFormFieldModule, MatSelectModule],
schemas: [],
declarations: [],
providers: [CityCascsdeService, CommonService]
}
}),
{ notes: `缺省是三級(jí)地區(qū)選擇` }
)
亦可使用addDecorator對storiesOf下的所有Story設(shè)置moduleMetadata:
.addDecorator(
//對此storiesOf下的所有Story生效
moduleMetadata({
imports: [BrowserAnimationsModule, MatFormFieldModule, MatSelectModule],
schemas: [],
declarations: [],
providers: [CityCascsdeService, CommonService]
})
)
.add( "直接使用組件", () => ({
component: MatCascaderComponent, //直接使用組件
props: {}
}),
{ notes: `缺省是三級(jí)地區(qū)選擇` }
);
使用HTML標(biāo)簽
下面我們使用Html標(biāo)簽,這個(gè)我們項(xiàng)目中的用法是一樣的,所以需要對組件MatCascaderComponent進(jìn)行聲明,同樣是在moduleMetadata中:
.addDecorator(
moduleMetadata({
imports: [BrowserAnimationsModule, MatFormFieldModule, MatSelectModule],
schemas: [],
declarations: [MatCascaderComponent], //這里聲明下
providers: [CityCascsdeService, CommonService]
})
)
.add("使用HTML標(biāo)簽",() => ({
template: `<ngx-mat-cascader ></ngx-mat-cascader>`,
props: {}
}),
{ notes: `缺省是三級(jí)地區(qū)選擇` }
);
此時(shí)看到的StoryBook的效果如下:

引入外部CSS
上面我們看到組件的樣式不對。StoryBook不會(huì)自動(dòng)引入組件需要的CSS文件,需要告訴StoryBook訪問哪個(gè)靜態(tài)文件,詳情可參見參考文檔。
這里給出主要改動(dòng):
-
.storybook 目錄下創(chuàng)建名為 preview-head.html 文件,該文件是為HTML添加自定義的Head內(nèi)容。內(nèi)容如下:
<link rel="stylesheet" href="./styles.css" /> -
在src/styles.css文件中引入Materail的CSS:
@import "../node_modules/@angular/material/prebuilt-themes/indigo-pink.css" @import "../node_modules/bootstrap-material-design/dist/css/bootstrap-material-design.css" -
package.json文件中修改啟動(dòng)StoryBook的命令,通過 -s 參數(shù)指定靜態(tài)目錄
//指定./ 和 ./src均為靜態(tài)目錄 "scripts": { "storybook": "start-storybook -p 6006 -s ./,./src", }
.storybook目下的修改,必須重啟StoryBook。這時(shí)我們看到樣式正確了。

設(shè)置參數(shù)的Story
上面的Story非常簡單,沒有參數(shù)和Action。下面看看如果設(shè)置參數(shù)和Action。MatCascaderComponent缺省是個(gè)省市區(qū)的三級(jí)選擇組件,但是如果提供不同的參數(shù),它就會(huì)華麗的變身了:
.add( "設(shè)置選擇數(shù)據(jù)",() => ({
component: MatCascaderComponent,
props: {
data: [
{
code: "11",
name: "易耗品",
children: [
{
code: "1101",
name: "打印機(jī)",
children: [
{ code: "110101", name: "彩色墨盒" },
{ code: "110102", name: "黑色墨盒" }
]
}
]
},
{
code: "12",
name: "食品",
children: [
{
code: "1201",
name: "快餐",
children: [
{ code: "120101", name: "薯?xiàng)l" },
{ code: "120102", name: "熱狗" }
]
}
]
}
],
level1placeholder: "選擇分類",
level2placeholder: "選擇貨區(qū)",
level3placeholder: "選擇貨架",
allTitle: "全部",
showAll: true,
onZoneChange:action('onZoneChange')
}
}),
{ notes: '這是一個(gè)三級(jí)商品選擇組件' }
);
效果如下:

Html標(biāo)簽測試設(shè)置參數(shù)參見如下示例代碼:
.add( "設(shè)置初始值", () => ({
template: `<ngx-mat-cascader [data]="basedatas" [separate]="separate" [(value)]="selectvalue" (onZoneChange)="zoneChange()" ></ngx-mat-cascader>`,
props: {
basedatas:[...],
separate:'-',
selectvalue:'11-1101',
zoneChange:action('change')
}
}),
{ notes: '這是一個(gè)三級(jí)商品選擇組件' }
);
是不是方便的不能再方便?對拷貝、粘貼深惡痛絕的我,看到了組件開發(fā)的春天。
為Story寫備注
在上面的示例中,你會(huì)發(fā)現(xiàn)notes屬性,就是備注的意思。一個(gè)完美的備注即提升了Story的可讀性,也方便后期對組件的維護(hù),更可以通過備注向使用者展示展示自己的組件。
StoryBook提供了兩種途徑,一是在Story的notes屬性中直接使用MarkDown,二是使用一個(gè)MarkDown文件。
如下是在notes屬性中直接使用MarkDown,需要注意的是折行后前面不能有空格:
.add("設(shè)置選擇數(shù)據(jù)", () => ({
component: MatCascaderComponent,
props: {...}
}),
{ notes: ` # 我是一級(jí)標(biāo)題
## 我是二級(jí)標(biāo)題,行首不能有空格,下同
### 我是三級(jí)標(biāo)題
1. 我是列表1
2. 我是列表2
` }
)
效果如下:

如果備注一兩句能說描述清楚Story,上述方法可行。但對于復(fù)雜的Story,還是一個(gè)MarkDown文件更方便。要使用md文件做備注,需要做些改動(dòng),要讓代碼識(shí)別出MarkDown文件:
//引入對md文件的支持,在.storybook目錄下創(chuàng)建typings.d.ts文件,內(nèi)容如下:
declare module "*.md" {
const content: string;
export default content;
}
// 在.storybook/tsconfig.json文件添加:
"files": [
"./typings.d.ts"
]
// story中引入文件:
import * as readme from '../app/components/select/README.md';
.add("選擇測試",() => ({
component: MatCascaderComponent,
props: {...}
}),
{ notes: readme }
);
效果如下:

是不是有點(diǎn)喜歡上StoryBook了?至少我是這樣,甚至憧憬著自己開發(fā)的組件減輕了更多同行的工作量。
更換主題
StoryBook還可以打包成靜態(tài)頁面放在公網(wǎng)上,供大家品評。但是界面左上角顯示的還是StoryBook,需要換下,這就涉及的修改StoryBook的主題了,這里僅提供名字和鏈接更改的方法:
import { addParameters } from '@storybook/angular';
import logo from '../src/assets/img/dteam.svg';
addParameters({
options: {
theme:{
brandTitle:'DTeam組件庫',
brandUrl: 'https://github.com/dteam-top'
},
}
});
效果如下:

關(guān)于主題的更詳細(xì)的說明,請參見文檔2。
總結(jié)
通過對StoryBook的學(xué)習(xí)和實(shí)踐,我覺得它的確不錯(cuò),對于組件的開發(fā)、測試、文檔化非常方便:
- 支持的主流前端框架
- 為組件提供獨(dú)立的開發(fā)環(huán)境
- 多種測試場景,全面測試組件
- 使用MarkDown編寫備注
- 眾多的Addon,方便擴(kuò)展
這些對于前端開發(fā)的工作,提供了不少改進(jìn):
- 避免混亂的重復(fù)代碼,提高代碼復(fù)用性
- 測試人員可以直接測試組件
- 豐富的Story,所見即所得
- 組件開發(fā),督促開發(fā)者提高自身技能
- Markdown的文檔,方便展示組件
但是“人無完人”,我在使用StoryBook過程中,也發(fā)現(xiàn)了一些問題:
- Addon太多,管理有些混亂
- 支持UI框架多,但是有的Andon卻不是所有UI框架都支持,比如info,具體可參見文檔3
- 大版本之間變化大,從網(wǎng)上找到的示例代碼比較老,不能用
- 不知道是否支持國際化
任何工具都是入門易、深耕難,StoryBook亦是如此,這需要開發(fā)人員提升組件化思維,并結(jié)合更多的實(shí)踐,才能讓它更好的助力前端開發(fā)。