前面的文章使用dumi创建的组件库记录了
- dumi创建组件库
- lerna管理多个包
- prettier和eslint配置代码风格
- git hooks和commitlint配置提交规范
这篇文章补充一下
- jest单元测试
- 使用webpack和father打包
- gitlab-ci
- lerna发包到npmjs.com
jest单元测试
- 在根目录中,新建一个tests目录和tests/button和tests/tag文件夹,然后把pro-components项目中的jest.config.js和setupTest.js文件复制过来
jest.config.js
// jest.config.js
const { readdirSync } = require('fs');
const { join } = require('path');
const pkgList = readdirSync(join(__dirname, './packages')).filter((pkg) => pkg.charAt(0) !== '.');
const moduleNameMapper = {
'\\.(css|less|sass|scss)$': require.resolve('identity-obj-proxy'),
};
pkgList.forEach((shortName) => {
const name = `@keith/${shortName}`; // 修改为自己的包名
moduleNameMapper[name] = join(__dirname, `./packages/${shortName}/src`);
});
module.exports = {
// 配置从哪些目录的覆盖率
collectCoverageFrom: [
'packages/**/src/**/*.{ts,tsx}',
'!packages/**/src/demos/**',
'!packages/**/src/**/demos/**',
'!packages/utils/src/isDeepEqualReact/*.{ts,tsx}',
],
moduleNameMapper,
unmockedModulePathPatterns: ['node_modules/react/', 'node_modules/enzyme/'],
testURL:
'http://localhost?navTheme=realDark&layout=mix&primaryColor=daybreak&splitMenus=false&fixedHeader=true',
verbose: true, // 是否在运行期间报告每个单独组件的测试情况
snapshotSerializers: [require.resolve('enzyme-to-json/serializer')],
setupFiles: ['./tests/setupTests.js'], // 设置测试环境的模块路径
};
- collectCoverageFrom: 生成测试覆盖率的文件来源
- moduleNameMapper: 要被mock的文件目录
- unmockedModulePathPatterns: 不需要被mock的目录
- verbose: 是否在运行期间报告每个单独组件的测试情况
- snapshotSerializers:快照序列化模块的路径
- setupFiles: 配置初始化指定的测试环境
setupTests.js
// setupTests.js
//... 省略前面的配置
// 注意!!!
// 如果react的版本是16的话,用enzyme-adapter-react-16; 如果是17的话用
// @wojtekmaj/enzyme-adapter-react-17 否则会报错
const Adapter =
process.env.REACT === '16'
? require('enzyme-adapter-react-16')
: require('@wojtekmaj/enzyme-adapter-react-17');
Enzyme.configure({ adapter: new Adapter() });
//... 省略后面的配置
需要注意的是react
和react dom
的版本与enzyme
的兼容性。如果react
和react dom
的版本是16的话,enzyme 适配器可以用enzyme-adapter-react-16
; 如果react
和react dom
的版本是17的话,enzyme 适配器可以用@wojtekmaj/enzyme-adapter-react-17
; 否则有可能会出现如下报错
- 安装依赖 查看jest.config.js和setupTest.js所需要的依赖,然后安装
npm install --save-dev jest enzyme @types/enzyme enzyme-to-json enzyme-adapter-react-16 jest-canvas-mock @wojtekmaj/enzyme-adapter-react-17
- enzyme是一个react的单元测试工具,能够模拟组件的一些操作和渲染
- enzyme-to-json可以把enzyme转换为与 Jest 快照测试兼容的格式
- enzyme-adapter-react-16是react 16版本的一个enzyme适配器
- @wojtekmaj/enzyme-adapter-react-17是react 17版本的一个enzyme适配器
- jest-canvas-mock是一个用于在jest中模拟画布的模块
如果在后面执行npm run test
的时候出现一些组件版本的问题,检查一下package.json
的某些插件是否安装及版本是否正确
- 编写测试用例 随便编写一个简单的测试用例
// tests/button/index.test.js
import { mount, render } from 'enzyme';
import Button from '@keith/button';
import React from 'react';
describe('Button', () => {
it('renders correctly', () => {
try {
const html = render(<Button />);
expect(html).toMatchSnapshot();
} catch (error) {
console.log(error);
}
});
it('button size', () => {
const wrapper1 = mount(<Button size="large">click me</Button>);
expect(wrapper1.find('.btn-size-large').length).toBe(1);
const wrapper2 = mount(<Button size="small">click me</Button>);
expect(wrapper2.find('.btn-size-small').length).toBe(1);
const wrapper3 = mount(<Button>click me</Button>);
expect(wrapper3.find('.btn-size-middle').length).toBe(1);
});
});
- 运行
直接在终端运行
npm run test
, 因为测试用例里使用了toMatchSnapshot方法
,因此会多出来一个__snapshots
目录
如果想看测试的覆盖率,可以执行npm run test:coverage
, 之后会在终端打印一些测试覆盖率的信息并且生成一个coverage
目录
- 总结一些常见api jest
- jest.fn(): 返回一个mock函数,如果该函数没有实现,则会返回undefined
- jest.spyOn(object, methodName): 与jest.fn()类似,也是返回一个mock函数,不过它可以追踪object里的函数调用信息
- mockImplementation(mock函数): 用来实现mock函数
- toHaveBeenCalledTimes(number): 确保mock函数的调用次数
- toBe(v): 使用Object.is来比较
- toEqual(v): 用于对象的深度比较
- toMatch(regexOrstring): 用来检查字符串是否匹配,可以是正则或表达式
- toContain(item): 用来判断item是否在一个数组中,也可以用于字符串的判断
- not: 用来取反
- toBeNull(value):只匹配null
- toBeUndefined(value):只匹配undefined
- toBeGreaterThan(number): 大于
- toBeGreaterThanOrEqual(number):大于等于
- toBeLessThan(number):小于
- toBeLessThanOrEqual(number):小于等于
- toBeTruthy(value):匹配任何使if语句为真的值
- toBeFalsy(value):匹配任何使if语句为假的值
- toMatchSnapshot(propMatchs?, hint?): 生成快照与并与最近生成的快照比较
- toMatchObject(): 检查对象的属性是否匹配 Enzyme
Enzyme
有三种渲染方法
- shallow: 浅渲染,将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高
- render: 静态渲染,将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构
- mount: 完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期
在这三种方法中,shallow和mount返回的是DOM对象,可以用simulate进行事件模拟操作,而render方法不可以。 常用的方法有:
- simulate(event, mock): 模拟事件
- instance(): 返回组件实例
- find(selector): 根据选择器查找节点
- at(index): 返回一个渲染过的对象
- contains(node): 判断当前对象是否返回某个节点
- text(): 返回当前节点的文本内容
- html(): 返回当前节点的html内容
- props(): 返回根组件的所有属性
- prop(key): 返回根组件的指定属性
下面是摘抄pro-components下的Card
组件的测试用例
import { mount } from 'enzyme';
import React from 'react';
import ProCard from '@ant-design/pro-card';
import { waitForComponentToPaint } from '../util';
import { act } from 'react-dom/test-utils';
jest.mock('antd/lib/grid/hooks/useBreakpoint');
describe('Card', () => {
it('🥩 collapsible onCollapse', async () => {
const fn = jest.fn();
const wrapper = mount(
<ProCard title="可折叠" headerBordered collapsible defaultCollapsed onCollapse={fn}>
内容
</ProCard>,
);
await waitForComponentToPaint(wrapper);
act(() => {
wrapper.find('AntdIcon.ant-pro-card-collapsible-icon').simulate('click');
});
expect(fn).toBeCalled();
});
it('🥩 collapsible defaultCollapsed', async () => {
const wrapper = mount(
<ProCard title="可折叠" headerBordered collapsible defaultCollapsed>
内容
</ProCard>,
);
await waitForComponentToPaint(wrapper);
expect(wrapper.find('.ant-pro-card-collapse').exists()).toBeTruthy();
});
// ...
it('🥩 tabs onChange', async () => {
const fn = jest.fn();
const wrapper = mount(
<ProCard
tabs={{
onChange: fn,
}}
>
<ProCard.TabPane key="tab1" tab="产品一">
内容一
</ProCard.TabPane>
<ProCard.TabPane key="tab2" tab="产品二">
内容二
</ProCard.TabPane>
</ProCard>,
);
act(() => {
wrapper.find('.ant-pro-card-tabs .ant-tabs-tab').at(1).simulate('click');
});
expect(fn).toHaveBeenCalledWith('tab2');
});
});
使用webpack和father打包
- 和上面类似的步骤,首先把
pro-components
项目中的webpack.config.js
和.fatherrc.ts
文件,复制到自己项目中
这里使用webpack主要是限制包的大小和构建组件,然后输出到子包的package/button/dist目录中,webpack的详细配置可以查看webpack印记中文
webpack.config.js
// webpack.config.js
// ...
const externals = isCI
? tailPkgs.reduce((pre, value) => {
return {
...pre,
[`@keith/${value}`]: `K${value // 这里改为自己的组件名称
.toLowerCase()
.replace(/( |^)[a-z]/g, (L) => L.toUpperCase())}`,
};
}, {})
: {};
console.log(externals);
const webPackConfigList = [];
tailPkgs.forEach((pkg) => {
const entry = {};
entry[`${pkg}`] = `./packages/${pkg}/src/index.tsx`; // 这里改为自己的包名
if (!isCI) {
entry[`${pkg}.min`] = `./packages/${pkg}/src/index.tsx`;
}
//...
});
module.exports = webPackConfigList;
使用father可以把组件打包成不同形式的模块,它支持cjs、esm和umd三种格式的打包,详细的也可以看father的仓库地址
.fatherrc.ts
// .fatherrc.ts
import { readdirSync } from 'fs';
import { join } from 'path';
// utils must build before core
// runtime must build before renderer-react
// components dependencies order: form -> table -> list
const headPkgs: string[] = ['button']; // 这里改为自己需要打包的包名
const tailPkgs = readdirSync(join(__dirname, 'packages')).filter(
(pkg) => pkg.charAt(0) !== '.' && !headPkgs.includes(pkg),
);
const type = process.env.BUILD_TYPE;
let config = {};
if (type === 'lib') {
config = {
cjs: { type: 'babel', lazy: true },
esm: false,
// 这里改为false, 不然npm run build会报@babel/runtime dependency is required to use runtimeHelpers 的错误
runtimeHelpers: false,
pkgs: [...headPkgs, ...tailPkgs],
// extraBabelPlugins: [
// ['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],
// ],
};
}
if (type === 'es') {
config = {
cjs: false,
esm: {
type: 'babel',
},
// 这里改为false, 不然npm run build会报@babel/runtime dependency is required to use runtimeHelpers 的错误
runtimeHelpers: false,
pkgs: [...headPkgs, ...tailPkgs],
extraBabelPlugins: [
[require('./scripts/replaceLib')],
['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],
],
};
}
export default config;
button组件里面的package.json
// packages/button/package.json
{
"name": "@keith/button",
"version": "0.0.0",
"keywords": [],
"license": "MIT",
"main": "lib/index.js",
"module": "es/index.js",
"types": "lib/index.d.ts",
"files": [ // 打包成不同形式的模块
"lib",
"es",
"dist"
],
"peerDependencies": {
"antd": "4.x",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"classnames": "^2.3.1"
}
}
需要注意的是:
- 这里father打包需要依赖于
babel
, 可以通过lerna add @babel/runntime packages/button
下载 publishConfig
中的access需要改为public
, 不然npm会把@keith/button
这样的包定义为私有包
-
下载依赖。为了方便和避免报错,可以直接把
pro-components
根目录下的package.json
里面的devDependencies
全部复制到自己项目中的devDependencies
, 然后使用npm install
下载即可 -
执行
npm run build
。如果编译成功,会出现dist
、es
和lib
目录
gitlab-ci
以前搭建的gitlab派上用场了ubuntu搭建gitlab和jenkins自动化部署环境。
要使用gitlab CI/CD,必须要有pipeline中的gitlab runner。gitlab-runner是一个执行Gitlab CI/CD的应用,它有三种分类
- Shared runners: 可以给该gitlab的所有项目共用
- Group runners: 可以给组的项目共用,在组的cicd中注册
- Specific runners: 只可以在当前项目中使用,在当前项目配置
安装步骤
- 根据不同的服务器架构,选择下载对应的二进制版本
在ubuntu系统中可以直接通过
arch
命令查看系统架构,我的系统架构为x86_64
,因此选择Linux x86-64的版本
# Linux x86-64
sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64"
# Linux x86 sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386"
# Linux arm sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm"
# Linux arm64 sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64"
# Linux s390x sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-s390x"
# Linux ppc64le sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-ppc64le"
# Linux x86-64 FIPS Compliant sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64-fips"
2.修改gitlab runner的权限
sudo chmod +x /usr/local/bin/gitlab-runner
3. 创建一个gitlab ci用户
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
4. 安装和运行
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start
详细的可以看gitlab的安装文档:docs.gitlab.com/runner/inst…
注册步骤
-
运行gitlab runner的注册命令
sudo gitlab-runner register
-
输入gitlab实例的URL地址
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://xxx
gitlab实例地址和下面的token可以在Settings >> Runners中查看
- 输入token
Enter the registration token:
xxx
- 输入runner的描述信息
Enter a description for the runner:
my-runner
- 输入runner的tag, 稍后可以在gitlab页面中修改; tag可以多个,中间用
,
隔开, 当使用该runner的时候,在.gitlab-ci.yml
的tag字段也须明确指明这些tag
Enter tags for the runner (comma-separated):
frontend-tag
- 输入runner维护的备注
Enter optional maintenance note for the runner:
frontend
- 输入runner的executor, 一般使用docker
Enter an executor: custom, docker, ssh, virtualbox, docker+machine, docker-ssh+machine, kubernetes, docker-ssh, parallels, shell:
docker
- 如果输入的是docker,则会要求输入docker镜像
Enter the default Docker image (for example, ruby:2.7):
docker:latest
如果创建成功,可以在Settings >> Runners中看到如下
详细步骤可以看gitlab的官方文档: docs.gitlab.com/runner/regi…
总结一些常用的gitlab runner 命令
gitlab-runner start #启动
gitlab-runner stop #停止
gitlab-runner restart #重启。
gitlab-runner status #查看状态,当服务正在运行时,退出代码为零;当服务未运行时,退出代码为非零。
gitlab-runner register #注册
gitlab-runner unregister #取消注册
gitlab-runner list #列出保存在配置文件的所有运行程序
如果想修改gitlab-runner
的服务信息,可以通过编辑/etc/systemd/system/gitlab-runner.service
文件,然后执行systemctl daemon-reload
和systemctl restart gitlab-runner
编写.gitlab-ci.yml
image: node:14
stages:
- install #安装依赖
- test #进行单元测试
- deploy #部署
#使用cache可以在每次job结束前,将一些共用的依赖或文件发到指定的目录中,避免重新下载
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
# stage名可以随便取
install_package:
stage: install
tags:
- frontend-tag #这里的tags要跟注册gitlab-runner的tags保持一致
only:
- master #只要在master分支触发ci
script:
- npm install #执行脚本
test:
stage: test
tags:
- frontend-tag
only:
- master
script:
- npm run test
deploy:
stage: deploy
tags:
- frontend-tag
only:
- master
script:
- apt-get update -y
- apt-get -y install rsync
- apt-get -y install sshpass
- npm run build
- npm run site
- cd docs-dist
- if [ "$SSH_PASS" ]; then sshpass -p "$SSH_PASS" rsync -rtvhze "ssh -o StrictHostKeyChecking=no" . root@192.168.80.129:/home/nginx --stats; fi
.gitlab-ci.yml
详细配置可以看.gitlab-ci
lerna发包到npmjs.com
- 使用
npm login
登录,如果出现one-time-password
,则是因为npm开启了2FA
验证,可以在npm >> Account中关闭
2. 直接复制pro-components项目下的
scripts
文件夹
-
检查是否需要安装依赖
-
然后执行
npm run release
-
如果发布成功会生成
CHANGELOG.md
文件 -
总结npm发包常见的错误有
- 用了淘宝镜像源
- 包名重复
- npm 账号没有验证邮箱
- vpn冲突