使用dumi创建组件库(下)

1,934 阅读1分钟

前面的文章使用dumi创建的组件库记录了

  • dumi创建组件库
  • lerna管理多个包
  • prettier和eslint配置代码风格
  • git hooks和commitlint配置提交规范

这篇文章补充一下

  • jest单元测试
  • 使用webpack和father打包
  • gitlab-ci
  • lerna发包到npmjs.com

jest单元测试

  1. 在根目录中,新建一个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() });

//... 省略后面的配置

需要注意的是reactreact dom的版本与enzyme的兼容性。如果reactreact dom的版本是16的话,enzyme 适配器可以用enzyme-adapter-react-16; 如果reactreact dom的版本是17的话,enzyme 适配器可以用@wojtekmaj/enzyme-adapter-react-17; 否则有可能会出现如下报错

微信截图_16492305455739(2).png

  1. 安装依赖 查看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的某些插件是否安装及版本是否正确

  1. 编写测试用例 随便编写一个简单的测试用例
// 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);
  });
});


  1. 运行 直接在终端运行npm run test, 因为测试用例里使用了toMatchSnapshot方法,因此会多出来一个__snapshots 目录

微信截图_16492388547476.png

如果想看测试的覆盖率,可以执行npm run test:coverage, 之后会在终端打印一些测试覆盖率的信息并且生成一个coverage目录

微信截图_16492425717591.png

  1. 总结一些常见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有三种渲染方法

  1. shallow: 浅渲染,将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高
  2. render: 静态渲染,将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构
  3. 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打包

  1. 和上面类似的步骤,首先把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这样的包定义为私有包
  1. 下载依赖。为了方便和避免报错,可以直接把pro-components根目录下的package.json里面的devDependencies全部复制到自己项目中的devDependencies, 然后使用npm install下载即可

  2. 执行npm run build。如果编译成功,会出现disteslib目录

微信截图_16494134021255.png

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: 只可以在当前项目中使用,在当前项目配置

安装步骤

  1. 根据不同的服务器架构,选择下载对应的二进制版本 在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…

注册步骤

  1. 运行gitlab runner的注册命令 sudo gitlab-runner register

  2. 输入gitlab实例的URL地址

Enter the GitLab instance URL (for example, https://gitlab.com/):
http://xxx

gitlab实例地址和下面的token可以在Settings >> Runners中查看

微信截图_16492958442210(2).png

  1. 输入token
Enter the registration token:
xxx
  1. 输入runner的描述信息
Enter a description for the runner:
my-runner
  1. 输入runner的tag, 稍后可以在gitlab页面中修改; tag可以多个,中间用,隔开, 当使用该runner的时候,在.gitlab-ci.yml的tag字段也须明确指明这些tag
Enter tags for the runner (comma-separated):
frontend-tag
  1. 输入runner维护的备注
Enter optional maintenance note for the runner:
frontend
  1. 输入runner的executor, 一般使用docker
Enter an executor: custom, docker, ssh, virtualbox, docker+machine, docker-ssh+machine, kubernetes, docker-ssh, parallels, shell:
docker
  1. 如果输入的是docker,则会要求输入docker镜像
Enter the default Docker image (for example, ruby:2.7):
docker:latest

如果创建成功,可以在Settings >> Runners中看到如下

微信截图_16493001306953.png

详细步骤可以看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-reloadsystemctl 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

微信截图_16498114881316.png

.gitlab-ci.yml详细配置可以看.gitlab-ci

lerna发包到npmjs.com

  1. 使用npm login登录,如果出现one-time-password,则是因为npm开启了2FA验证,可以在npm >> Account中关闭

微信截图_16497575486186.png

微信截图_16497574326396.png 2. 直接复制pro-components项目下的scripts文件夹

微信截图_16497577244397.png

  1. 检查是否需要安装依赖

  2. 然后执行npm run release

  3. 如果发布成功会生成CHANGELOG.md文件

  4. 总结npm发包常见的错误有

  • 用了淘宝镜像源
  • 包名重复
  • npm 账号没有验证邮箱
  • vpn冲突

github

参考链接

jest中文文档
使用Jest进行React单元测试
gitlab文档