每一个前端都值得拥有本身的组件库,就像每一个夏天都拥有西瓜🍉

⚠️本文为掘金社区首发签约文章,未获受权禁止转载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 时我也多少借鉴了前人优秀的设计。

相关文章
相关标签/搜索