每一個前端都值得擁有本身的組件庫,就像每一個夏天都擁有西瓜🍉

⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載php

你們好,我是洛竹🎋,一隻住在杭城的木系前端🧚🏻‍♀️,若是你喜歡個人文章📚,能夠經過點贊幫我彙集靈力⭐️。前端

洛竹有一個朋友小黑最近在面試時被問到如何設計一個前端組件庫。沒啥經驗的小黑回答了業務提取封裝成庫以及基於 antd 結合業務二次封裝。最後小黑被 HR 以靈力不夠掛掉了。其實這個問題考察的並非假大空的概念,而是有關開發者倉庫管理、組件設計、單元測試、持續集成、協做管理等等能力。那麼爲了賦能小黑完美回答這個問題呢,我決定帶領小黑一步一步建設一個 React Native 組件庫。vue

這是一篇乾貨比較多的組件庫搭建實戰教程,不只有通用的代碼規範、提交規範、文檔維護、單元測試、GitHub Action 配置的講解,還涉及基於 lerna 的多包管理架構、React Native 圖標庫建設、React Native 組件庫開發調試、按需加載原理及實現。工程化的思想是通用的,因此不管是你用的框架是什麼,本文都值得一讀。node

若是電腦前的掘友也對組件庫開發感興趣,不妨先給個點贊,再持續關注洛竹和小黑的組件庫開發之旅。PS:配合倉庫組件庫文檔閱讀本文效果更佳喲!react

站在 Vant Design 的肩膀上

維護開發一個組件庫無疑是須要投入不少時間和精力的,Flag 立了倒,倒了又立。可謂萬事開頭難,首先咱們要有自知之明,在沒有設計師和業餘開發的狀況下,我選擇了給現有 UI Design 實現 React Native 版本的方式開啓組件庫開發之旅。在調研了 vantfishd-mobileantd-mobile 後我選擇了 vant。這是幾個倉庫的現狀對比:android

組件庫 團隊 Github Star Npm 周下載量 維護度
vant 有贊 17.7K 27,789 維高度高,流行度也高
antd-mobile Ant Design Team 8.9K 31,470 幾乎不維護,聽說螞蟻內部也不用了
fishd-mobile 網易雲商前端 29 22 看起來是個 KPI 項目無疑了

肯定了旅程的方向,就是給咱們的組件庫起一個合適的名字和口號,用前端工程師的方式表述就是 package.jsonnamedescription 字段:webpack

// package.json
{
    "name": "vant-react-native",
    "description": "Lightweight React Native UI Components inspired on Vant"
}
複製代碼

因爲咱們的組件庫定位是 vant 的 RN 版,參照 lottie-react-native、styled-react-native、jpush-react-native 的命名方式咱們將組件庫命名爲 vant-react-native,同時也是但願組件庫完成時能得到 vant 官方的支持。ios

基於 Lerna 的多包管理架構

Lerna 是一個管理工具,用於管理包含多個軟件包(package)的 JavaScript 項目。由 Lerna 管理的倉庫咱們通常稱之爲單體倉庫(monorepo)。基於 Lerna 的多包管理架構的優勢在於:git

  • 組件級別解耦,獨立版本控制,每一個組件都有版本記錄可追溯
  • 組件單獨發佈,支持灰度、版本回滾以及平滑升降級
  • 按需引用,用戶安裝具體某個組件包,無需配置便可實現按需加載的效果。
  • 關注點分離,下降大型複雜度、組件之間依賴清晰且可控制
  • 單一職責原則,下降開源基友的參與和貢獻難度
.
└── packages
    ├── button # @vant-react-native/button
    └── icons # @vant-react-native/icon
複製代碼

初始化 lerna 項目

$ mkdir vant-react-native && lerna init --independent
複製代碼

yarn workspaces

使用 yarn workspaces 結合 Lerna useWorkspaces 能夠實現 Lerna Hoisting。這並非畫蛇添足,這可讓你在統一的地方(根目錄)管理依賴,這即節省時間又節省空間。github

配置 lerna.json:

{
  ...
  "npmClient": "yarn",
  "useWorkspaces": true
}
複製代碼

託管給 yarn wrokspace 以後,lerna 的 packages 將會被頂級 package.jsonworkspaces 覆蓋:

{
  "private": true,
  ...
  "workspaces": [
    "packages/*"
  ],
}
複製代碼

lerna publish config

若是你不想在全部 package.json 文件中單獨明確設置你的註冊表配置,例如使用私有註冊表時,設置 command.publish.registry 頗有用。配置 ignoreChanges 則是爲了不沒必要要的版本升級。

"ignoreChanges": [
  "ignored-file",
  "**/__tests__/**",
  "**/*.md"
],
"command": {
  "publish": {
    "registry": "https://registry.npmjs.org"
  }
}
複製代碼

除此以外,若是你的包名是帶 scope 的,須要在那個包的 package.json 中設置 publishConfig.access"public"

lerna version config

當配置 conventionalCommitstrue 後,lerna 版本將使用 Conventional Commits Specification 來肯定版本升級並 生成 CHANGELOG.md 文件

"command": {
  "version": {
    "conventionalCommits": true,
    "message": "chore(release): publish"
  }
}
複製代碼

規範化提交

規範化 git commit 對於提升 git log 可讀性、可控的版本控制和 changelog 生成都有着重要的做用。洛竹以前在 一文搞定規範化Git Commit 中詳細講述了 Conventional Commits 的概念以及 commitizen、cz-customizable、@commitlint/cli、yorkie 和 commitlint-config-cz 等工具的配置。

因爲配置繁瑣,我在 @youngjuning/cli 中添加了 init-commit 命令一鍵配置 conventional commit。能夠打開這個 commit 查看配置信息。

注意:husky 高版本用法不向後兼容,我在這個 commit 中用尤大的 yorkie 代替了 husky。

代碼規範化

代碼規範化的重要性不言而喻,代碼規範化涉及的工具備 editorconfig、eslint、prettier 等,在 裝它|不再用操心ESLint配置 一文中我介紹瞭如何一步一步建設屬於本身的 eslint config 插件併產出了 @youngjuning/eslint-config@youngjuning/prettier-config

vant-react-native 暫時使用 @youngjuning/eslint-config、@youngjuning/prettier-config 約束項目代碼規範。相關配置以下文。

eslint

首先安裝 react-native 所需的插件。

yarn add -D eslint-plugin-react \
  eslint-plugin-react-hooks \
  eslint-plugin-jsx-a11y \
  eslint-plugin-import \
  eslint-plugin-react-native
複製代碼

而後配置 .eslintrc.js

// .eslintrc.js
module.exports = {
  extends: ['@youngjuning/eslint-config/react-native']
}
複製代碼

prettier

// .prettierrc.js
module.exports = require('@youngjuning/prettier-config');
複製代碼

@youngjuning/eslint-config 計劃也用 lerna 管理,產出 @youngjuning/eslint-config-react、@youngjuning/eslint-config-react-native、@youngjuning/eslint-config-vue 讓開發者無需過多配置開箱即用。

editorconfig

# .editorconfig
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[*.gradle]
indent_size = 4

[BUCK]
indent_size = 4
複製代碼

yorkie & lint-staged

$ yarn add -D yorkie lint-staged
複製代碼
{
  "gitHooks": {
    "commit-msg": "commitlint -e -V",
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "**/*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "git add ."
    ]
  },
}
複製代碼

第一個組件從 Icon 開始

一個成熟的組件庫都會擁有本身的一套 Icon,Icon 通常由設計師經過 Sketch 設計,而後導出 svg 文件。

ant-design-icons 的 svg 文件是 保存在本地,而後經過腳本生成 react 組件vue 組件icons-react-native 等組件,因爲支持的框架比較完備咱們無需本身實現,RN 咱們直接使用 icons-react-native

vant 以及 fishd-mobile 則是經過 Iconfont 維護 svg 文件,而後經過設置 @font-face 的方式實現 Icon 組件,如圖所示:

image.png

有了 ttf 文件,咱們能夠像 @ant-design/icons-react-native 同樣基於 ttf 文件使用腳本生成 Icon 組件,可是使用 ttf 字體有一個弊端,就是每次更新圖標,都要相應的更新 ttf 文件,而後再次打包發佈 APP。並且 ttf 不支持多種色彩的圖標,致使全部圖標都是單色。若是你是藉助 react-native-vector-icons,該庫內置了 10 多套 ttf 文件,合起來有 2M 左右;你可能用不到它們,可是它們仍然會被打包進你的 APP 裏,這也是我認爲 react-native-elements 這個庫外強中乾的一大緣由。

那麼只有 Iconfont 連接咱們如何實現 vant-icons 的 React Native 版本呢?這裏洛竹沒有本身寫腳本,而是使用了一款叫 react-native-iconfont-cli 的工具,fwh1990 大佬針對以上痛點用純 Javascript 實現 iconfont 到 React 組件的轉換操做,不須要依賴 ttf 字體文件,不須要手動下載圖標到本地。

建立 lerna 子包

# 建立主包,主包用來統一導出全部的組件
$ lerna create vant-react-native -y
# 建立 icons 包,咱們的第一個組件!
$ lerna create @vant-react-native/icons -y
複製代碼

咱們的目錄結構看起來是這樣的:

.
└── packages
    ├── icons
    │   ├── README.md
    │   └── package.json
    └── vant-react-native
        ├── README.md
        └── package.json
複製代碼

生成 icons

安裝插件

yarn workspace @vant-react-native/icons add -D react-native-svg react-native-iconfont-cli
複製代碼

生成配置文件

咱們在 packages/icons 目錄下使用 npx iconfont-init 命令會生成 iconfont.json 文件,自定義後內容以下:

{
  "symbol_url": "https://at.alicdn.com/t/font_2553510_7cds497uxwn.js",
  "use_typescript": false,
  "save_dir": "./lib",
  "trim_icon_prefix": "van-icon",
  "default_icon_size": 18
}
複製代碼

生成 React Native 標準組件

執行 npx iconfont-rn 命令便可生成標準 React Native 組件。因爲圖標文件比較多,咱們不將圖標產物加入 git 管理。因此咱們須要在 npm 發佈前執行構建命令:

{
  "build": "npx iconfont-rn",
  "prepublishOnly": "yarn build"
}
複製代碼

配置 react-native-vant

咱們前面提到 packages/vant-react-native 是主包的目錄,咱們須要將 @vant-react-native/icons 包添加到主包的依賴中並導出。

添加依賴

$ lerna add @vant-react-native/icons --scope vant-react-native
複製代碼

導出 Icon 組件

// packages/vant-react-native/src/index.ts
export { default as Icon } from '@vant-react-native/icons';
export * from '@vant-react-native/icons';
複製代碼

tsconfig 配置

對與每一個子包咱們指望使用同樣的配置,因此咱們會先在整個項目的根目錄新建 tsconfig. base.json,在子包繼承便可。

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "lib",
  },
  "include": ["src/**/*"]
}
複製代碼

配置發佈腳本

@vant-react-native/icons 子包同樣,咱們須要添加 buildprepublishOnly 腳本:

{
  "build": "tsc",
  "prepublishOnly": "yarn build"
}
複製代碼

發佈包

第一次發佈的話,注意使用的是 lerna publish 0.0.1,由於 lerna 的發佈命令沒有第一次發佈這個參數,因此須要顯示指定初始版本。或者能夠將初始版本設置爲 0.0.0 而後執行 lerna publish

小技巧:若是發佈後想查看包內容,能夠經過 jsdelivr 查看。好比剛發佈的 vant-react-native@vant-react-native/icons

開發調試

一個完善且體驗良好的調試流程不只可以知足在開發階段驗證組件是否符合預期,還能夠下降開源社區基友的參與難度。React Native 組件庫的調試和其餘技術棧流程大致沒有區別,只不過由於 Metro 不支持軟鏈接 以及 vant-react-native 是基於 lerna 的單體倉庫項目,咱們的配置會有不一樣。

image.png

初始化 React Native App

因爲是 React Native 項目,咱們須要初始化一個 React Native 項目。首先找一個地方使用 react-native init vantapp --template react-native-template-typescript 建立一個新的 React Native App。而後將生成的 App 與咱們的主項目合併。合併後的項目結構以下:

.
├── App.tsx
├── __tests__
│   └── App-test.tsx
├── android
│   ├── app
│   ├── build.gradle
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   └── settings.gradle
├── app.json
├── babel.config.js
├── commitlint.config.js
├── index.js
├── ios
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   ├── vantapp
│   ├── vantapp.xcodeproj
│   ├── vantapp.xcworkspace
│   └── vantappTests
├── lerna.json
├── metro.config.js
├── package.json
├── packages
│   ├── icons
│   └── vant-react-native
├── tsconfig.base.json
├── tsconfig.json
└── yarn.lock
複製代碼

主要衝突的是 Prettier、eslint 等工具的配置,合併沒那麼難。在運行項目以前,咱們通常須要編譯項目。咱們能夠藉助 lerna run build 命令批量運行子包裏的 build npm script。

注意📢:因爲子包之間有依賴關係,不要使用 --parallel 參數並行執行打包腳本。

如今咱們編寫一個九宮格 Demo 驗證一下:

// App.tsx
import React, { Component } from 'react';
import { View, Text, SafeAreaView, ScrollView } from 'react-native';
import { Icon } from 'vant-react-native';
// 咱們也能夠只安裝 @vant-react-native/icons 包
// import { VanIconAdd } from '@vant-react-native/icons'

type IconNameType = React.ComponentProps<typeof Icon>['name'];

export default class App extends Component {
  render() {
    return (
      <SafeAreaView> <ScrollView> <Text style={{ textAlign: 'center', paddingVertical: 20, fontSize: 25, color: '#007fff' }} > vant-react-native </Text> <View style={{ flexWrap: 'wrap', flexDirection: 'row' }}> {data.map((item, index) => { const lastLineLength = data.length % 4 || 4; return ( <View key={item} style={{ width: '25%', marginBottom: index < data.length - lastLineLength ? 40 : 0, alignItems: 'center', }} > <Icon name={item} size={40} /> <Text style={{ color: '#646566', marginTop: 10 }}>{item}</Text> </View> ); })} </View> </ScrollView> </SafeAreaView>
    );
  }
}

const data: IconNameType[] = ['location-o', 'like-o', 'star-o', 'phone-o', 'setting-o', 'fire-o', 'coupon-o', 'cart-o', 'shopping-cart-o', 'cart-circle-o', 'friends-o', 'comment-o', 'gem-o', 'gift-o', 'point-gift-o', 'send-gift-o', 'service-o', 'bag-o', 'todo-list-o', 'balance-list-o', 'close', 'clock-o', 'question-o', 'passed'];
複製代碼

而後執行 yarn ios 查看實際效果(以後咱們就能夠執行 yarn start --reset-cache 快速開始調試):

image.png

上面的示例代碼中咱們能夠看到咱們直接使用了 import { Icon } from 'vant-react-native'; 而不是相對路徑引用 packages 下的模塊。但是咱們的項目並沒與安裝這個依賴,編譯器是怎麼找到的呢?這裏也沒有什麼銀彈,這是由於 lerna 會把子包軟連接到 node_modules 中,咱們可使用 ls -al 發現看到包的實際指向:

image.png

咱們也能夠在類型提示中看到實際指向的是 packages 下的文件:

image.png

注意📢:Metro 不支持符號連接 指的是軟鏈接的目錄不在項目根目錄下,這裏咱們軟鏈接指向的位置還在根目錄下,因此能夠正確工做✅。這個特性保證了調試與生產開發的一致性和便利性。

實時編譯

如今咱們的調試流程是:

  1. 修改代碼
  2. 執行 lerna run build 編譯每一個子包
  3. 執行 yarn ios 調試項目
  4. 修改代碼
  5. 執行 lerna run build 從新編譯
  6. 執行 yarn start --reset-cache 運行項目
  7. 循環 四、五、6。

儘管 React Native 有 Fast Refresh 功能,可是因爲咱們的代碼是須要編譯的,因此咱們須要重複編譯運行的動做。

任何重複的工做均可以用腳本代替。首先咱們須要給每一個子包添加實時編譯的 script,像 rollup、babel、webpack、typescript 都有參數能夠實現實時編譯:

{
  "scripts": {
    "dev": "tsc -w",
    "build": "tsc",
    "prepublishOnly": "yarn build"
  },
}
複製代碼

而咱們的 @vant-react-native/icons 包使用的 npx iconfont 沒有實時編譯選項,通過調研,我引入了 onchange 這個庫能夠基於 glob 模式監聽文件改動後執行一個命令:

{
  "scripts": {
    "dev": "onchange -i 'iconfont.json' -- yarn build",
  }
}
複製代碼

而後咱們須要使用 lerna run dev --parallel 批量執行實時編譯腳本,這裏加 --parallel 是由於子包若是是實時編譯,進程會卡住。爲了補救,咱們不得不預先編譯 @vant-react-native/icons 包,而後由於一樣的緣由我引入了 npm-run-all 來並行執行 lerna run devreact-native start,完整腳本以下:

{
  "predev": "lerna run build --scope @vant-react-native/icons",
  "dev": "lerna run dev --parallel",
  "start": "react-native start",
  "debug": "run-p dev start",
}
複製代碼

按需加載

小黑:「洛竹哥哥,我以前爲了使用 react-native-elements 的其中幾個組件而引入了整個組件庫。由於這個組件庫依賴了 react-native-vector-icons 致使 bundle 包變大。若是我就是想用整套 vant-react-native,如何解決這個問題呢?」

衆所周知,React Native 的打包工具 Metro 不支持 tree-shaking。解決這個問題的方式其實很簡單,機智的你可能知道配合 babel-plugin-import 是能夠實現按需加載的需求的。但因爲咱們是多包管理架構,須要針對多包的架構設計一個方案。

react-naitve bundle 包

爲了比對優化先後包大小,咱們須要使用 react-native bundle 命令看一下純 JS 包的大小,咱們來簡單看下這個命令:

react-native bundle --platform ios --entry-file index.js --bundle-output ./bundle/ios/index.ios.jsbundle --assets-dest ./bundle/ios --dev false --reset-cache
複製代碼
  • --entry:入口 js 文件
  • --bundle-output:生成的 bundle 文件路徑
  • --platform:平臺
  • --assets-dest:圖片資源的輸出目錄
  • --dev:是否爲開發版本,打正式版的安裝包時咱們將其賦值爲 false
  • --reset-cache:重置緩存,避免打包使用舊的緩存

按需加載原理

前面咱們提到 packages/vant-react-native 只有一個文件 src/index.ts 用來導出全部子包,如今咱們添加一個新的包 Button,看上去就是這樣:

export { default as Icon } from '@vant-react-native/icons';
export * from '@vant-react-native/icons';
export { default as Button } from '@vant-react-native/icons';
複製代碼

這種導出方式,用戶只能經過 import Button from '@vant-react-native/button';import Button from 'vant-react-native/lib/button'; 的方式手動實現按需加載,這不只不方便開發者使用,從打包產物來講也增長了不少字節。那麼問題來了,怎麼樣的組織形式才能知足按需加載呢?答案就在 babel-plugin-import 插件的文檔中:

image.png

從圖中咱們看出 babel-plugin-import 插件是在編譯階段將引用指向了模塊所在文件夾。用戶使用時安裝插件並作以下配置就完成了按需加載。

"plugins": [
  ["import", { libraryName: "antd", style: true }]
]
複製代碼

依然沒有銀彈,插件作的工做只是代替了你的右手。知道了原理咱們就能夠按照文檔要求的格式從新組織咱們的 vant-react-native 包:

.
├── CHANGELOG.md
├── lib                    # 上傳到 NPM 的編譯產物 
│   ├── button             # 符合 babel-plugin-import 的默認配置要求
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── icon
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── index.d.ts
│   └── index.js          # export * from './button';
├── package.json
├── src                   # 源碼目錄
│   ├── button
│   │   └── index.ts
│   ├── icon
│   │   └── index.ts
│   └── index.ts
└── tsconfig.json         # 編譯配置,將 ts 文件編譯到 lib 文件夾下
複製代碼

vant-react-native/src/button/index.ts:

import Button from '@vant-react-native/button';
export default Button;
export { Button };
複製代碼

vant-react-native/src/icon/index.ts:

import Icon from '@vant-react-native/icons';

export default Icon;
export { Icon };
export * from '@vant-react-native/icons';
複製代碼

vant-react-native/src/index.ts:

export * from './icon';
export * from './button';
複製代碼

而後項目中修改 babel.config.js:

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    ["import", {libraryName: 'vant-react-native'}]
  ],
};
複製代碼

編寫 Babel 插件?

雖然經過修改主包的導出方式能夠完成需求,可是卻極大地增長了項目自己的複雜度。前面咱們已經知道 babel-plugin-import 的原理是轉換引用路徑。那麼咱們是否是能夠經過插件動態把 import {Button} from 'vant-react-native' 轉成 import Button from '@vant-react-native/button' 呢?答案是確定的,下面是我基於 babel-plugin-import 的 customName 配置編寫了一套配置並封裝在 babel-plugin-import-vant 包中:

import camelCase from 'camelcase';

export default (): any[] => [
  [
    'import',
    {
      libraryName: 'vant-react-native',
      customName: (name: string) => {
        if (name === 'icon') {
          return '@vant-react-native/icons';
        }
        if (name.match(/^van-icon-/)) {
          return `@vant-react-native/icons/lib/${camelCase(name, { pascalCase: true })}`;
        }
        return `@vant-react-native/${name}`;
      },
    },
    'vant-react-native',
  ],
  [
    'import',
    {
      libraryName: '@vant-react-native/icons',
      customName: (name: string) => {
        return `@vant-react-native/icons/lib/${camelCase(name, { pascalCase: true })}`;
      },
    },
    '@vant-react-native/icons',
  ],
];
複製代碼

在項目的 babel.config.js 配置中添加 plugins: [...require('babel-plugin-import-vant').default()] 便可實現按需加載。

還有能夠優化的地方嗎?機智的你可能又發現我只是經過函數導出了一個配置而已,並非真正的插件,因此將來我會定製一個 vant-react-native 本身的按需加載 babel 插件。

name.match(/^van-icon-/) 這個判斷條件是由於 @vant-react-native/icons 包除了包含一個默認導出的 Icon 組件,還導出了不少單個圖標組件,爲了進一步減少打包體積,咱們對這個子包也進行了按需加載處理。

咱們已經知道按需加載的原理是沒有中間商賺差價直接和賣家談,因此後面咱們碰見相似的需求經過轉換返回賣家地址便可。不須要破壞性地改項目結構。

成果展現

初始包大小 未配置按需加載(引入 Button) 按需加載(引入 Button) 按需加載(引入 Icon) 按需加載(引入 VanIconAdd)
723KB 1.8M 725KB 1.8M 1.22M

之因此 Icon 包會大,是由於 react-native-svg 這個庫大,因此不建議直接使用 Icon 組件,而是使用 VanIconAdd、VanIconEye 這種單獨的圖標組件,少了 593KB 仍是挺香的。

組件庫文檔

組件庫文檔比較重要的是有能夠交互的 Demo 演示,我是 Dumi 的資深用戶,藉助 dumi-theme-mobile 和 umi-plugin-react-native 咱們能夠很好地知足 React Native 組件庫文檔的搭建。

集成 Dumi 到項目中

安裝依賴:

$ yarn add dumi dumi-theme-mobile umi-plugin-react-native -D
複製代碼

配置文件:

在項目根目錄添加 .umirc.ts

import { defineConfig, IConfig } from 'dumi';

export default defineConfig({
  title: 'vant-react-native',
  mode: 'site',
  logo: 'https://img01.yzcdn.cn/vant/logo.png',
  favicon: 'https://img01.yzcdn.cn/vant/logo.png',
  resolve: {
    includes: ['docs', 'packages/button', 'packages/icons'],
  },
  // more config: https://d.umijs.org/config
} as IConfig);
複製代碼

值得一提的是,Dumi 是支持 Lerna 倉庫的,它默認會以 packages/[包名]/src 爲基礎路徑搜尋全部子包的 Markdown 文檔並生成路由。經過 resolve.includes 能夠配置 dumi 嗅探的文檔目錄,dumi 會嘗試在配置的目錄中遞歸尋找 markdown 文件。

添加 NPM 腳本:

注意📢:因爲實際依賴的是 packages 下的包,咱們必須先編譯全部的包,不然部署的時候會報 This dependency was not found: 的錯誤。

{
  "scripts": {
    "start:dumi": "dumi dev",
    "build:dumi": "lerna run build && dumi build"
  }
}
複製代碼

忽略文件(.gitignore):

# umi
.umi
.umi-production
.env.local
dist/
複製代碼

部署到 GitHub Pages

在根目錄新建 .github/workflows/gh-pages

name: github pages
on:
  push:
    branches:
      - main # default branch
jobs:
  deploy:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v2
      - run: yarn install
      - run: yarn build:dumi
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist
複製代碼

預覽

如今咱們能夠訪問 youngjuning.js.org/vant-react-… 查看效果了:

image.png

image.png

image.png

配置優化

如今基於 dumi 的文檔站點只是初始化,不少配置(.umirc.ts)能夠優化,好比:

  1. 基於 jsdeliver 配置 CDN 加速
const isProd = process.env.NODE_ENV === 'production';
...
publicPath: isProd ? 'https://cdn.jsdelivr.net/gh/youngjuning/vant-react-native@gh-pages/': '/',
複製代碼
  1. 增量發佈和避免瀏覽器加載緩存
{
  hash: true
}
複製代碼
  1. 友盟網站統計
{
  scripts: ['https://s9.cnzz.com/z_stat.php?id=1280093214&web_id=1280093214'],
  styles: ['a[title=站長統計] { display: none; }'],
}
複製代碼
  1. 配置 exportStatic: {} 將全部路由輸出爲 HTML 目錄結構,以避免刷新頁面時 404。

Pull Request 預發預覽

考慮到後期社區會貢獻代碼和文檔。在 pr 合進主分支以前,咱們須要預覽文檔或組件。知足這一需求的是一個叫 surge.sh 的靜態託管服務,surge 支持在命令行經過簡單的命令免費發佈 HTML、CSS 和 JS 文件到 web。

申請 Surge Token

安裝 surge cli:

npm install --global surge
複製代碼

註冊 surge 帳號:

suerge login
複製代碼

獲取 token:

suerge token
複製代碼

配置 CI

因爲 GitHub 的安全問題,surge-preview Action 插件沒法使用,咱們參考 dumi 官方的配置自定義了 CI,首先咱們拷貝下圖中的三個文件到項目中。

image.png

而後修改 preview-build.yml 中的 build step

- NODE_OPTIONS='--max-old-space-size=4096' yarn build
+ NODE_OPTIONS='--max-old-space-size=4096' PREVIEW_PR=true yarn build:dumi
複製代碼

添加環境變量 PREVIEW_PR=true 是爲了讓 dumi 打包時識別出不是生產環境打包,.umirc.ts 須要相應修改成:

const isProd =
  process.env.NODE_ENV === 'production' && process.env.PREVIEW_PR !== "true";
...
publicPath: isProd ? 'https://cdn.jsdelivr.net/gh/youngjuning/vant-react-native@gh-pages/': '/',
...
複製代碼

再而後,修改 preview-deploy.yml 文件中的部署域名 dumi-previewvant-react-native-preview

最後咱們把前面獲取的 Surge Token 添加到倉庫的 Secrets 便可。

成果展現

正在部署 PR 預覽狀態:

image.png

部署成功狀態:

image.png

訪問 vant-react-native-preview-pr-1.surge.sh/ 便可驗證文檔的正確性✅。

單元測試

我在 使用 Jest 和 Enzyme 進行 React Native 單元測試|技術點評 一文中曾提交單元測試和文檔同樣,是保障程序最小單元質量的重要一環。誠然一個成熟的組件庫是必然有單元測試的身影。本章就不展開講單元測試了,主要講 vant-react-native 是如何配置單元測試的。

安裝依賴

jest、babel-jest、@types/jest 這些依賴都已經安裝了,咱們須要安裝的是 enzyme 這個基於 jest 的單元測試框架。

$ yarn add enzyme jest-enzyme enzyme-adapter-react-16 enzyme-to-json @types/enzyme react-native-mock-render -DW
複製代碼

Enzyme 是用於 React 的 JavaScript 測試實用程序,能夠更輕鬆地測試 React 組件的輸出。您還能夠根據給定的輸出進行操做,遍歷並以某種方式模擬運行時。

配置

jest.config.js:

module.exports = {
  preset: 'react-native',
  verbose: true,
  collectCoverage: true, // 生成測試覆蓋率報告
  moduleNameMapper: {
    // for https://github.com/facebook/jest/issues/919
    '^image![a-zA-Z0-9$_-]+$': 'GlobalImageStub',
    '^[@./a-zA-Z0-9$_-]+\\.(png|gif)$': 'RelativeImageStub',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], // 使用 Jest 運行安裝文件以配置 Enzyme 和適配器(以下文jest.setup.js中所示),以前是setupTestFrameworkScriptFile,也可使用setupFiles
  snapshotSerializers: ['enzyme-to-json/serializer'], // 推薦使用序列化程序使用 enzyme-to-json,它的安裝和使用很是簡單,並容許您編寫簡潔的快照測試。
};
複製代碼

jest.setup.js:

import 'react-native';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
複製代碼

一個簡單的示例:

// packages/button/__test__/index.tsx
import React from 'react';
import { shallow } from 'enzyme';
import Button from '../src/index';

function setup(props = {}) {
  const wrapper = shallow(<Button />);
  const instance = wrapper.instance();
  return { wrapper, instance };
}

describe('Button Component', () => {
  it('renders correctly', () => {
    const { wrapper } = setup();
    expect(wrapper).toMatchSnapshot();
  });
});
複製代碼

執行 jest 命令後能夠查看覆蓋率以下:

image.png

寫給勇士

能寫長文的不算勇士,能堅持看到這裏的纔是勇士。洛竹在此感謝您的閱讀。然而組件庫工程化這只是一個起點,若是本文反響好,組件庫具體組件的設計實現、完整的 React Native 單元測試教程等等洛竹會在後續的文章中展開講。

推薦的 UI 庫

固然了,vant-react-native 並非你惟一的選擇,下面的幾個 UI 庫都是很優秀的項目。在實現 vant-react-native 時我也多少借鑑了前人優秀的設計。

相關文章
相關標籤/搜索