每个前端都值得拥有自己的组件库,就像每个夏天都拥有西瓜

发表于 3年以前  | 总阅读数:914 次

朋友小黑最近在面试时被问到如何设计一个前端组件库。没啥经验的小黑回答了业务提取封装成库以及基于 antd 结合业务二次封装。最后小黑被 HR 以灵力不够挂掉了。其实这个问题考察的并不是假大空的概念,而是有关开发者仓库管理、组件设计、单元测试、持续集成、协作管理等等能力。那么为了赋能小黑完美回答这个问题呢,我决定带领小黑一步一步建设一个 React Native 组件库。

这是一篇干货比较多的组件库搭建实战教程,不仅有通用的代码规范、提交规范、文档维护、单元测试、GitHub Action 配置的讲解,还涉及基于 lerna 的多包管理架构、React Native 图标库建设、React Native 组件库开发调试、按需加载原理及实现。工程化的思想是通用的,所以无论是你用的框架是什么,本文都值得一读。

如果电脑前的掘友也对组件库开发感兴趣,不妨先给个点赞,再持续关注洛竹和小黑的组件库开发之旅。PS:配合仓库[1]和组件库文档[2]阅读本文效果更佳喲!

站在 Vant Design 的肩膀上

维护开发一个组件库无疑是需要投入很多时间和精力的,Flag 立了倒,倒了又立。可谓万事开头难,首先我们要有自知之明,在没有设计师和业余开发的情况下,我选择了给现有 UI Design 实现 React Native 版本的方式开启组件库开发之旅。在调研了 vant[3]、fishd-mobile[4] 和 antd-mobile[5] 后我选择了 vant。这是几个仓库的现状对比:

组件库 团队 Github Star Npm 周下载量 维护度
vant 有赞 17.7K 27,789 维高度高,流行度也搞
antd-mobile Ant Design Team 8.9K 31,470 几乎不维护,据说蚂蚁内部也不用了
fishd-mobile 网易云商前端 29 22 看起来是个 KPI 项目无疑了

确定了旅程的方向,就是给我们的组件库起一个合适的名字和口号,用前端工程师的方式表述就是 package.jsonnamedescription 字段:

// 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 官方的支持。

基于 Lerna 的多包管理架构

Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。由 Lerna 管理的仓库我们一般称之为单体仓库(monorepo)。基于 Lerna 的多包管理架构的优点在于:

  • 组件级别解耦,独立版本控制,每个组件都有版本记录可追溯
  • 组件单独发布,支持灰度、版本回滚以及平滑升降级
  • 按需引用,用户安装具体某个组件包,无需配置即可实现按需加载的效果。
  • 关注点分离,降低大型复杂度、组件之间依赖清晰且可控制
  • 单一职责原则,降低开源基友的参与和贡献难度
.
└── packages
    ├── button # @vant-react-native/button
    └── icons # @vant-react-native/icon

初始化 lerna 项目

$ mkdir vant-react-native && lerna init --independent

yarn workspaces

使用 yarn workspaces[6] 结合 Lerna useWorkspaces 可以实现 Lerna Hoisting[7]。这并不是多此一举,这可以让你在统一的地方(根目录)管理依赖,这即节省时间又节省空间。配置 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[8] 来确定版本升级并 生成 CHANGELOG.md 文件[9]。

"command": {
  "version": {
    "conventionalCommits": true,
    "message": "chore(release): publish"
  }
}

规范化提交

规范化 git commit 对于提高 git log 可读性、可控的版本控制和 changelog 生成都有着重要的作用。洛竹之前在 一文搞定规范化 Git Commit[10] 中详细讲述了 Conventional Commits 的概念以及 commitizen、cz-customizable、@commitlint/cli、yorkie 和 commitlint-config-cz 等工具的配置。

由于配置繁琐,我在 \@youngjuning/cli[11] 中添加了 init-commit 命令一键配置 conventional commit。可以打开这个 commit[12] 查看配置信息。

注意:husky 高版本用法不向后兼容,我在这个 commit[13] 中用尤大的 yorkie 代替了 husky。

代码规范化

代码规范化的重要性不言而喻,代码规范化涉及的工具有 editorconfig、eslint、prettier 等,在 装它|再也不用操心 ESLint 配置[14] 一文中我介绍了如何一步一步建设属于自己的 eslint config 插件并产出了 \@youngjuning/eslint-config[15] 和 \@youngjuning/prettier-config[16]。

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 文件是 保存在本地[17],然后通过脚本生成 react 组件[18]、vue 组件[19] 和 icons-react-native[20] 等组件,由于支持的框架比较完备我们无需自己实现,RN 我们直接使用 icons-react-native[21]。

vant 以及 fishd-mobile 则是通过 Iconfont 维护 svg 文件,然后通过设置 @font-face 的方式实现 Icon 组件,如图所示:

有了 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[22] 大佬针对以上痛点用纯 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[23],在子包继承即可。

{
  "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[24] 查看。比如刚发布的 vant-react-native[25] 和 \@vant-react-native/icons[26]

开发调试

一个完善且体验良好的调试流程不仅能够满足在开发阶段验证组件是否符合预期,还可以降低开源社区基友的参与难度。React Native 组件库的调试和其他技术栈流程大体没有区别,只不过因为 Metro 不支持软连接[27] 以及 vant-react-native 是基于 lerna 的单体仓库项目,我们的配置会有不同。

初始化 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 快速开始调试): 上面的示例代码中我们可以看到我们直接使用了 import { Icon } from 'vant-react-native'; 而不是相对路径引用 packages 下的模块。可是我们的项目并没与安装这个依赖,编译器是怎么找到的呢?这里也没有什么银弹,这是因为 lerna 会把子包软链接到 node_modules 中,我们可以使用 ls -al 发现看到包的实际指向: 我们也可以在类型提示中看到实际指向的是 packages 下的文件:

注意 :Metro 不支持符号链接[28] 指的是软连接的目录不在项目根目录下,这里我们软连接指向的位置还在根目录下,所以可以正确工作 ✅。这个特性保证了调试与生产开发的一致性和便利性。

实时编译

现在我们的调试流程是:

  1. 修改代码
  2. 执行 lerna run build 编译每个子包
  3. 执行 yarn ios 调试项目
  4. 修改代码
  5. 执行 lerna run build 重新编译
  6. 执行 yarn start --reset-cache 运行项目
  7. 循环 4、5、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[29] 这个库可以基于 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[30]。解决这个问题的方式其实很简单,机智的你可能知道配合 babel-plugin-import[31] 是可以实现按需加载的需求的。但由于我们是多包管理架构,需要针对多包的架构设计一个方案。

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 插件的文档中: 从图中我们看出 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[32] 我们可以很好地满足 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-…[33] 查看效果了:

配置优化

现在基于 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/': '/',

2 . 增量发布和避免浏览器加载缓存

{
  hash: true
}

3 . [友盟网站统计]

{
  scripts: ['https://s9.cnzz.com/z_stat.php?id=1280093214&web_id=1280093214'],
  styles: ['a[title=站长统计] { display: none; }'],
}

4 . 配置 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,首先我们拷贝下图中的三个文件到项目中。 然后修改 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 预览状态: 部署成功状态: 访问 vant-react-native-preview-pr-1.surge.sh/[34] 即可验证文档的正确性 ✅。

单元测试

我在 使用 Jest 和 Enzyme 进行 React Native 单元测试|技术点评[35] 一文中曾提交单元测试和文档一样,是保障程序最小单元质量的重要一环。诚然一个成熟的组件库是必然有单元测试的身影。本章就不展开讲单元测试了,主要讲 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 命令后可以查看覆盖率如下:

写给勇士

能写长文的不算勇士,能坚持看到这里的才是勇士。 在此感谢您的阅读。然而组件库工程化这只是一个起点,如果本文反响好,组件库具体组件的设计实现、完整的 React Native 单元测试教程等等洛竹会在后续的文章中展开讲。

推荐的 UI 库

当然了,vant-react-native 并不是你唯一的选择,下面的几个 UI 库都是很优秀的项目。在实现 vant-react-native 时我也多少借鉴了前人优秀的设计。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/3E0irVhQ9uA5qt0LIt2rfQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录