如何快速構建React組件庫?

俗話說:“麻雀雖小,五臟俱全”,搭建一個組件庫,知之非難,行之不易,涉及到的技術方方面面,猶如海面風平浪靜,實則暗礁險灘,處處驚險~

前言

目前團隊內已經有較爲成熟的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 出馬了。

1、什麼是 npm link?

在本地開發 npm 模塊的時候,我們可以使用 npm link 命令,將 npm 模塊鏈接到對應的運行項目中去,方便地對模塊進行調試和測試。

2、使用方法

假設組件庫是 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

具體步驟如下:

1、在代碼庫 reactui 中執行npm link

2、在代碼庫 reactui 中執行 npm link ../../demo/node_modules/react

3、在項目 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。

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: 業務組件
---

(2)在 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]基於Storybook5打造組件庫開發與文檔站建設小結: http://jelly.jd.com/article/5f06fe8505541b015b6a708a

作者介紹

京東零售前端工程師甄玉磊。

本文轉載自公衆號京東數科技術說(ID:JDDTechTalk)。

原文鏈接

如何快速構建React組件庫?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章