前言
俗話說:“麻雀雖小,五臟俱全”,搭建一個元件庫,知之非難,行之不易,涉及到的技術方方面面,猶如海面風平浪靜,實則暗礁險灘,處處驚險~
目前團隊內已經有較為成熟的 Vue 技術棧的 NutUI 元件庫[1] 和 React 技術棧的 yep-react 元件庫[2]。然而這些元件庫大都從零開始搭建,包括 Webpack 的繁雜配置,Markdown 檔案轉 Vue 檔案功能的開發,單元測試功能的開發、按需載入的 Babel 外掛開發等等,完成整個元件庫專案實屬不易,也是一個浩大的工程。如果我們想快速搭建一個元件庫,大可不必如此耗費精力,可以藉助業內專業的相關庫,經過拼裝除錯,快速實現一個元件庫。
本篇文章就來給大家介紹一下使用 create-react-app 腳手架、docz 文件生成器、node-sass、結合 Netlify 部署專案的整個開發元件庫的流程,本著包教包會,不會沒有退費的原則,來一場手摸手式教學,話不多說,讓我們進入正題:
首先看一下元件庫的最終效果:
本文將從以下步驟介紹如何搭建一個 React 元件庫:
一、構建本地開發環境
開發一個元件庫的首要步驟就是除錯本地 React 環境,我們直接使用 React 官方腳手架 create-react-app
,可以省去從底層配置 Webpack+TypeScript+React 的摧殘:
1、使用 create-react-app 初始化腳手架,並且安裝 TypeScript
npx create-react-app myapp --typescript
注意使用 node 為較高版本 >10.15.0
2、配置 eslint 進行格式化
由於安裝最新的 create-react-app 結合 VScode 編輯器即可支援 eslit,但是需要在專案根目錄中要新增 .env 這個配置檔案,設定 EXTEND_ESLINT=true
這樣才會啟用 eslint 檢測,注意要 重啟 vscode
3、元件庫系統檔案結構
新建 styles 資料夾,包含了基本樣式檔案,結構如下:
|-styles
| |-variables.scss // 各種變數以及可配置設定
| |-mixins.scss // 全域性 mixins
| |-index.scss // 引入全部的 scss 檔案,向外丟擲樣式入口
|-components
| |-Button
| |-button.scss // 元件的單獨樣式
| |-button.mdx // 元件的文件
| |-button.tsx // 元件的核心程式碼
| |-button.test.tsx // 元件的單元測試檔案
| |-index.tsx // 元件對外入口
4、安裝 node-sass 處理器
安裝 node-sass 用來編譯 SCSS 樣式檔案:npm i node-sass -D
這樣最基本的 react 開發環境就完成了,可以開心的開發元件了。
二、元件庫打包編譯
本地除錯完元件庫之後,需要打包壓縮編譯程式碼,供其他使用者使用,這裡我們用的 TypeScript 編寫的程式碼,所以使用 Typescript 來編譯專案:
首先在每個元件中新建 index.tsx 檔案:
import Button from './button'
export default Button
修改 index.tsx 檔案,匯入匯出各個模組
export { default as Button } from './components/Button'
在根目錄新建 tsconfig.build.json,對 .tsx 檔案進行編譯:
{
"compilerOptions": {
"outDir": "dist",// 生成目錄
"module": "esnext",// 格式
"target": "es5",// 版本
"declaration": true,// 為每一個 ts 檔案生成 .d.ts 檔案
"jsx": "react",
"moduleResolution":"Node",// 規定尋找引入檔案的路徑為 node 標準
"allowSyntheticDefaultImports": true,
},
"include": [// 要編譯哪些檔案
"src"
],
"exclude": [// 排除不需要編譯的檔案
"src/**/*.test.tsx",
"src/**/*.stories.tsx",
"src/setupTests.ts",
]
}
對於樣式檔案,使用 node-sass 編譯 SCSS,抽取所有 SCSS 檔案生成 CSS 檔案:
"script":{
"build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
}
並且修改 build 命令:
"script":{
"clean": "rimraf ./dist",// 跨平臺的相容
"build": "npm run clean && npm run build-ts && npm run build-css",
}
這樣,執行 npm run build
之後,就可以生成對應的元件 JS 和 CSS 檔案,為後面使用者按需載入和部署到 npm 上提供準備。
三、本地除錯元件庫
本地完成元件庫的開發之後,在釋出到 npm 前,需要先在本地除錯,避免帶著問題上傳到 npm 上。這時就需要使用 npm link 出馬了。
什麼是 npm link
在本地開發 npm 模組的時候,我們可以使用 npm link 命令,將 npm 模組連結到對應的執行專案中去,方便地對模組進行除錯和測試。
使用方法
假設元件庫是 reactui 資料夾,要在本地的 demo 專案中使用元件。則在元件庫中(要被 link 的地方)執行 npm link
,則生成從本機的 node_modules/reactui
到 元件庫的路徑 / reactui
中的對映關係。
然後在要使用元件庫的資料夾 demo 中執行 npm link reactui
則生成以下對應鏈條:
在要使用元件的資料夾 demo 中 -[對映到]—> 本機的 node_modules/reactui
—[對映到]-> 開發元件庫 reactui 的資料夾 /reactui
需要修改元件庫的 package.json 檔案來設定入口:
{
"name": "reactui",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
}
然後在要使用元件的 demo 專案的依賴中新增:
"dependencies":{
"reactui":"0.0.1"
}
注意,此時並不用安裝依賴,之所以寫上該依賴,是為了方便在專案中使用的時候可以有程式碼提示功能。
然後在 demo 專案中使用:
import { Button } from 'reactui'
在 index.tsx 中引入 CSS 檔案
import 'reactui/build/index.css'
正當以為大功告成的時候,下面這個報錯猶如一盆冷水從天而降:
經過各種問題排查,在 react 官方網站[3] 上查到以下說法:
? Do not call Hooks in class components.
? Do not call in event handlers.
? Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
說的很明白:
原因 1: React 和 React DOM 的版本不一樣的問題
原因 2: 可能打破了 Hooks 的規則
原因 3: 在同一個專案中使用了多個版本的 React
官網很貼心,給出瞭解決方法:
This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder. Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib. This should make the library use the application’s React copy.
核心思想在元件庫中使用 npm link
方式,引到 demo 專案中的 react; 所以在元件庫中執行: npm link ../demo/node_modules/react
具體步驟如下:
-
在程式碼庫 reactui 中執行
npm link
-
在程式碼庫 reactui 中執行
npm link ../../demo/node_modules/react
-
在專案 demo 中執行
npm link reactui
如此可以解決上面 react 衝突問題;於是可以在本地一邊快樂的除錯元件庫,一邊快樂的在使用元件的專案中看到最終效果了。
四、元件庫釋出到 npm
該過程一定要注意使用的是 npm 源!![非常重要]
首先確定自己是否已經登入了 npm:
npm adduser
// 填入使用者名稱;密碼;email
npm whoami // 檢視當前登入名
修改元件庫的 package.json ,注意 files 配置;以及 dependencies 檔案的化簡:
react 依賴原本是要放在 dependencies 中的,但是可能會和使用者安裝的 react 版本衝突,所以放在了 devDependencies 中,但是這樣話使用者如果沒有安裝 react 則無法使用元件庫,所以要在 peerDependencies 中定義前置依賴 peerDependencies,告訴使用者 react 和 react-dom 是必要的前置依賴:
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [ // 把哪些檔案上傳到 npm
"dist"
],
"dependencies": { // 執行 npm i 的時候會安裝這些依賴到 node_modules 中
"axios": "^0.19.1",// 傳送請求
"classnames": "^2.2.6",//
"react-transition-group": "^4.3.0"
},
"peerDependencies": { // 重要!!,提醒使用者,元件庫的核心依賴,必須先安裝這些依賴才能使用
"react": ">=16.8.0", // 在 16.8 之後 才引入了 hooks
"react-dom": ">=16.8.0"
}
好了,整個元件庫經過上述過程,基本上各個功能已經有了,提及一句:由於元件庫使用的是 create-react-app 腳手架,最新的版本已經整合了單元測試功能。還有配置 husky 等規範程式碼提交,在這裡不在做贅述,讀者可以自行配置。
五、生成說明文件
目前生成說明文件較好的工具有 storybook[4]、docz[5] 等工具,兩者都是很優秀的文件生成工具,但是尺有所短,寸有所長,經過認真調研比較,最終選擇了 docz。
工具名稱 | 區別一 | 區別二 |
---|---|---|
storybook | 使用特有的API開發文件說明,可以引入markdown檔案 | 生成文件的介面帶有storybook的痕跡較多一些 |
docz | 完美的結合了react和markdown語法開發文件 | 生成的文件介面是常規的文件介面 |
1、確定選型
1)storybook 的常用編譯文件規範相對 docz 而言,略有繁瑣
storybook 的編譯文件規範如下所示:
//省略 import 引入的程式碼
storiesOf('Buttons', module)
.addDecorator(storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>)
.add('with text', () => (
<Button onClick={action('clicked')}>Hello Button111</Button>
),{
notes:{markdown} // 將會渲染 markdown 內容
})
對比 docz 的開發文件:
# Button 元件
使用方式如下所示:
import { Playground, Props } from 'docz';
import Button from './index.tsx';
## 按鈕元件
<Playground>
<Button btnWidth="100">我是按鈕</Button>
</Playground>
** 基本屬性 **
| 屬性名稱 | 說明 | 預設值 |
|--|--|--|
|btnType | 按鈕型別 |--|
眾所周知,Markdown 是一種輕量級標記語言,它允許人們使用易讀易寫的純文字格式編寫文件。團隊成員在開發文件時,熟練使用 markdown 語法,開發 docz 文件的 mdx 檔案,結合了 Markdown 和 React 語法,相比 storybook 要使用很多的 API 來編寫文件的方式,無疑減少了很多的學習 storybook 語法的成本。
2)docz 生成的文件樣式更加符合個人審美
storybook 生成的文件樣式,帶有 storybook 的痕跡更為嚴重一些, 其生成文件介面如下所示:
docz 生成的文件圖如下所示:
由上圖對比可以看出,docz 生成的介面更加簡介,較為常規。
綜上,結合預設文件開發習慣和介面風格,我選擇了docz,當然仁者見仁、智者見智,讀者也可以使用同為優秀的 storybook 嘗試,這都不是事兒~
2、使用 docz 開發
確定了 docz 進行開發後,根據官網介紹,在 create-react-app 生成的元件庫中進行了安裝配置:
npm install docz
安裝成功後,就會向 package.json 檔案中新增如下配置
{
"scripts": {
"docz:dev": "docz dev",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve"
}
}
這時還需要在專案的根目錄下新建 doczrc.js 檔案,對 docz 進行配置:
export default {
files: ['./src/components/**/*.mdx','./src/*.mdx'],
dest: 'docsite', // 打包 docz 文件到哪個資料夾下
title: '元件庫左上角標題', // 設定文件的標題
typescript: true, // 支援 typescript 語法
themesDir: 'theme', // 主題樣式放在哪個資料夾下,後面會講
menu: ['快速上手', '業務元件'] // 生成文件的左側選單分類
}
其中 files 規定了 docz 去對哪些檔案進行編譯生成文件,如果不做限制,會搜尋專案中所有的 md、mdx 為字尾的檔案生成文件,因此我在該檔案中做了範圍限制,避免一些 README.md
檔案也被生成到文件中。
此外還需要注意到兩點:
1、menu: ['快速上手', '業務元件']
對應著元件庫左側的選單欄分類,比如在 mdx 文件中在最上面設定元件所屬的選單 menu: 業務元件
, 則 Button 元件屬於 "業務元件" 的分類:
---
name: Button
route: /button
menu: 業務元件
---
在 src 中新建歡迎頁,路由為跟路徑,所屬選單為“快速上手”;
---
name: 快速上手
route: /
---
執行 npm run docz:dev
,就可以開啟
介紹到這裡,估計有小夥伴會有疑問了,這樣生成的網站千篇一律,能否隨心所欲的自定義網站的樣式和功能呢?當初我也有這種疑問,經過多次嘗試,皇天不負苦心人,終於摸索出如下方法:
1、修改 docz 文件本身的樣式
根據 docz 官方文件中增加 logo 的方法[6],可以通過自定義元件覆蓋原有元件的形式:
Example: If you're using our gatsby-theme-docz which has a Header component located at src/components/Header/index.js you can override the component by creating src/gatsby-theme-docz/components/Header/index.js. Cool right?
所以根據 docz 原始碼主題部分程式碼: https://github.com/doczjs/docz/tree/master/core/gatsby-theme-docz/src
,找到對應的文件元件的程式碼結構,在元件庫專案根目錄新建同名稱的資料夾:
|-theme
| |-gatsby-theme-docz
| |-components
| |-Header
| |-index.js // 在這裡修改自定義的文件元件
| |-styles.js // 在這裡修改生成的樣式檔案
這樣在執行 npm run docz:dev
的時候,就會把自定義的程式碼覆蓋原有樣式,實現文件的多樣化。
2、修改 markdown 文件樣式
事情到這裡就結束了嗎?不!我們的目標不僅如此,因為我發現自動生成的 markdown 格式,並不符合我的審美,比如生成的表格文字居左對齊,並且整個表格樣式單一,但是這裡屬於 markdown 樣式的範疇,修改上述文件元件中並不包括這裡的程式碼,那麼如何修改 markdown 生成文件的樣式呢?
經過我靈機一動又一動,發現既然在上面修改文件元件樣式的時候,重寫了 component/Header/styles.js 檔案,是否可以在該檔案中引入自定義的樣式呢?檔案結構如下:
|-theme
| |-gatsby-theme-docz
| |-components
| |-Header
| |-index.js // 在這裡修改自定義的文件元件
| |-styles.js // 在這裡修改生成的樣式檔案
| |-base.css // 這裡修改 markdown 生成文件的樣式
這樣修改後的表格樣式如下:
接下來各位小主可以根據自己的審美或者視覺設計的要求自定義文件的樣式了。
六、部署文件到伺服器
生成的元件庫文件只在本地顯示是沒有意義的,所以需要部署到伺服器上,於是第一時間想到的是放在 github 進行託管,開啟 github 中的 setting 設定選項,GitHub Pages 設定配置的分支:
這時預設開啟的首頁路徑為:
https://plusui.github.io/plusReact/
但實際上頁面有效的訪問地址是帶有資料夾 docsite 路徑的:
https://plusui.github.io/plusReact/docsite/button/index.html
此外,頁面引入的其他資源路徑,都是絕對路徑,如下圖資源路徑所示:
所以直接把打包後的資源放在 github 上是無法訪問各種資源的。
這時我們只好把網站部署到雲伺服器上了,考慮到伺服器配置的繁瑣,這裡給大家提供一個簡便的部署網站:Netlify[7]
Netlify 是一個提供靜態網站託管的服務,提供 CI 服務,能夠將託管 GitHub,GitLab 等網站上的 Jekyll,Hexo,Hugo 等靜態網站。
部署專案的過程也很簡單,傻瓜式的點選選擇 github 網站中程式碼路徑,以及配置資料夾跟路徑,如下圖所示:
然後就可以點選生成的網站 url,訪問到部署的網站了:
而且很方便的是,一旦完成部署之後,之後再次向程式碼庫中提交程式碼,Netlify 會自動更新網站。
此外,如果想自定義 url,那麼就只能去申請域名了,在自己的雲伺服器上,解析域名即可。下面簡單說一下配置步驟:
1)首先在 Netlify 網站上,選擇元件庫對應的 Domain settings 下 Custom domains,增加自己的域名:
2)然後開啟雲伺服器中的域名解析中的解析設定,將該域名指向 Netlify:
3)最後開啟設定的網址,就可以訪問到元件庫了:
七、元件按需載入
好了,經過上面的流程,可以在 demo 專案中使用元件庫了,但是在 demo 專案中,執行 npm run build
,就會發現生成的靜態資源中即使只使用了一個元件,也會把 reactui 元件庫中所有的元件打包進來。
所以如何進行按需載入呢?
按需載入首先映入腦海的是使用 babel-plugin-import
外掛, 該外掛可以在 Babel 配置中針對元件庫進行按需載入.
使用者需要安裝 babel-plugin-impor
外掛,然後在 plugins 中加入配置:
"plugins": [
[
"import",
{
"libraryName": "reactui", // 轉換元件庫的名字
"libraryDirectory": "dist/components", // 轉換的路徑
"camel2DashComponentName":false, // 設定為 false 來阻止元件名稱的轉換
"style":true
}
]
]
這樣在 demo 專案中使用如下方式:
import { Button } from 'reactui';
就會在 babel 中編譯成:
import { Button } from 'reactui/dist/components/Button';
require('reactui/dist/components/Button/style');
但是這樣還有些弊端:
1、 使用者在使用元件庫的時候還需要安裝 babel-plugin-import
, 並做相關 plugins 配置;
2、 開發元件庫的時候元件對應的樣式檔案還需要放在 style 資料夾下;
那有沒有更為簡單的方法呢?在 ant-design 中尋找答案,發現這樣一句話 “antd 的 JS 程式碼預設支援基於 ES modules 的 tree shaking”。 對呀!還可以使用 webpack 的新技術“tree shaking”。
什麼是 tree shaking? AST 對 JS 程式碼進行語法分析後得出的語法樹 (Abstract Syntax Tree)。AST 語法樹可以把一段 JS 程式碼的每一個語句都轉化為樹中的一個節點。DCE Dead Code Elimination,在保持程式碼執行結果不變的前提下,去除無用的程式碼。
webpack 4x 中已經使用了 tree shaking 技術,我們只需要在 package.json 檔案中配置引數 "sideEffects": false
,來告訴 webpack 打包的時候可以大膽的去掉沒有用到的模組即可。這時使用者在 demo 專案中使用元件庫的時候不需要做任何處理,就可以按需引用 JS 資源了。
不知道大家在看到這裡時,是否發現這樣配置還是有問題的:即 sideEffects 配置成 false 是有問題的。
因為按照上述配置,就會發現元件的樣式不見了!!
經過排查,原因是引入 CSS 樣式的程式碼:import './button.scss'
,可以看到相當於只是引入了樣式,並不像其他 JS 模組後面做了呼叫,在 tree shaking 的時候,會把 css 樣式去掉。所以在配置 sideEffects 就要把 CSS 檔案排除掉:
"sideEffects": [
"*.scss"
]
通過上述 tree shaking 的方法,可以實現元件庫的按需載入功能,打包的檔案去除了沒有用到的元件程式碼,同時省去了使用者的配置。
八、樣式按需載入
通常來說,元件庫的 JS 是按需載入的,但是樣式檔案一般只輸出一個檔案,即把元件庫中的所有檔案打包編譯成一個 index.css 檔案,使用者在專案中引入即可;但是如果就是想做按需載入元件的樣式檔案,該如何去做呢?
這裡我提供一種思路,由於 .tsx 檔案是由 TS 編譯器打包編譯的,並沒有處理 SCSS,所以我使用了 node-sass 來編譯 SCSS 檔案,如果需要按需載入 SCSS 檔案,則每個元件的 index.tsx 檔案中就需要引入對應的 SCSS 檔案:
import Button from './button';
import './button.scss';
export default Button;
生成的 SCSS 檔案也需要打包到每個元件中,而不是生成到一個檔案中:
所以使用了 node-sass 中的 sass.render 函式,抽取每個檔案中的樣式檔案,並打包編譯到對應的檔案中,程式碼如下所示:
//省略 import 引入,核心程式碼如下
function createCss(name){
const lowerName = name.toLowerCase();
sass.render({ // 呼叫 node-sass 函式方法,編譯指定的 scss 檔案到指定的路徑下
file: currPath(`../src/components/${name}/${lowerName}.scss`),
outputStyle: 'compressed', // 進行壓縮
sourceMap: true,
},(err,result)=>{
if(err){
console.log(err);
}
const stylePath = `../dist/components/${name}/`;
fs.writeFile(currPath(stylePath+`/${lowerName}.scss`), result.css, function(err){
if(err){
console.log(err);
}
});
});
}
這樣就在生成的 dist 檔案中的每個元件中增加了 SCSS 檔案,使用者通過“按需載入小節”中的方法在引入元件的時候,會呼叫對應的 index 檔案,在 index.js 檔案中就會呼叫對應的 SCSS 檔案,從而也實現了樣式檔案的按需載入。
但是這樣還有一個問題,就是在開發元件庫的時候每個元件中的 index.tsx 檔案中引入的是 SCSS 檔案 import './button.scss';
,所以 node-sass 編譯後的檔案需要是 SCSS 字尾的檔案(雖然已經是 CSS 格式),如果生成的是 CSS 檔案,則使用者在使用元件的時候就會因找不到 SCSS 檔案而報錯,也就是使用者在使用元件的時候,也需要安裝 node-sass 外掛。
不知大家有沒有更好的辦法,在元件庫開發的時候使用的是 SCSS 檔案,編譯後生成的是 CSS 字尾的檔案,在使用者使用元件的中呼叫的也是 CSS 檔案呢?歡迎在文末留言討論~
結語
以上就是整個搭建元件庫的過程,從一開始決定使用現有的 create-react-app 腳手架和 docz 來構成核心功能,到文件的網站部署和 npm 資源的釋出,最初感覺應該能夠快速完成整個元件庫的搭建,實際上如果要想改動這些現有的庫來實現自己想要的效果,還是經歷了一些探索,不過整個摸索過程也是一種收穫和樂趣所在,願走過路過的小夥伴能有所收穫~
參考文章
[1] NutUI 元件庫: http://nutui.jd.com/#/index
[2] yep-react 元件庫: http://yep-react.jd.com
[3] react 官方網站: https://reactjs.org/warnings/invalid-hook-call-warning.html
[4] storybook: https://storybook.js.org/
[5] docz: https://www.docz.site/
[6] docz 官方文件: https://www.docz.site/docs/gatsby-theme
[7] Netlify: https://app.netlify.com/teams/zhenyulei/sites
[8]基於 Storybook 5 打造元件庫開發與文件站建設小結: http://jelly.jd.com/article/5f06fe8505541b015b6a708a