从0到1搭建AntDesign组件库

1,168 阅读8分钟

0.AntDesign

ant.design 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品

0.1 技术栈

  • 框架 react

  • 测试 jest+enzyme

  • 检查 eslint

  • 打包 webpack+gulp

  • 文档 bisheng

  • 钩子 husky

0.2 源码目录

  • .husky git钩子

  • _site 网站

  • components 组件

  • docs 文档

  • dist 打包生成的文件

  • es ES6

  • lib ES5

  • scripts 脚本

  • site 组件预览项目

  • tests 测试

  • typings 类型定义

0.3 本文主要内容

  • webpack配置

  • storybook文档和组件编写

  • 单元测试+E2E快照测试+代码覆盖率

  • eslint+prettier+editorconfig

  • git hook

  • 编译发布

  • 持续集成

1.创建项目

1.创建文件夹


mkdir antd

cd antd

npm init -d

2.配置webpack

2.1 安装依赖


yarn add webpack webpack-cli webpack-dev-server mini-css-extract-plugin babel-loader css-loader autoprefixer postcss-loader less-loader less @babel/core @babel/preset-react @babel/preset-env @babel/runtime @babel/plugin-transform-typescript typescript @babel/plugin-transform-runtime @types/node --dev

yarn add react react-dom

yarn add @types/react @types/react-dom --dev

2.2 webpack.config.js


const path = require('path');

//提取CSS文件的

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

//当前命令所在的目录

const cwd = process.cwd();

module.exports = {

mode: 'development',//开发模式

devtool: false,//关闭生成sourcemap

entry: {

antd: './index.js',

},

output: {

path: path.resolve('dist'),//输出到dist目录

filename: '[name].js',//打包后的文件 antd.css

library: 'antd',//打包后库的名字

libraryTarget: 'umd',//打包后模块的格式 umd amd cmd commonjs commonjs window

},

externals: {//组件库代码其实是不需要打包react 和react-dom进去的

react: {//外部依赖

root: 'React',

commonjs2: 'react',

commonjs: 'react',

amd: 'react',

},

'react-dom': {

root: 'ReactDOM',

commonjs2: 'react-dom',

commonjs: 'react-dom',

amd: 'react-dom',

},

},

resolve: {

extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']//指定扩展名

},

module: {

rules: [

{

test: /\.(j|t)sx?$/,//配置如何加载js ts jsx tsx

exclude: /node_modules/,

loader: 'babel-loader',

},

{

test: /\.css$/,

use: [

MiniCssExtractPlugin.loader,//把这些CSS收集起来后面通过插件写入单独的antd.css

{

loader: 'css-loader',//处理@import和url

options: {

sourceMap: true,

},

},

{

loader: 'postcss-loader', //加厂商前缀

options: {

postcssOptions: {

plugins: ['autoprefixer'],

},

sourceMap: true,

},

},

],

},

{

test: /\.less$/,

use: [

MiniCssExtractPlugin.loader,

{

loader: 'css-loader',

options: {

sourceMap: true,

},

},

{

loader: 'postcss-loader',

options: {

postcssOptions: {

plugins: ['autoprefixer'],

},

sourceMap: true,

},

},

{

loader: 'less-loader',//把less编译 成css

options: {

lessOptions: {

javascriptEnabled: true,

},

sourceMap: true,

},

},

],

},

{//webpack5里file-loaer url-loader已经废弃 了

test: /\.(png|jpg|jpeg|gif|svg)(\?v=\d+\.\d+\.\d+)?$/i,

type: 'asset'//静态文件不再需要配置loader

},

],

},

plugins: [

new MiniCssExtractPlugin({

filename: '[name].css',

}),

],

};

2.3 babel.config.js


/*

* @Description:

* @Author: changqing

* @Date: 2021-09-21 11:14:00

* @LastEditTime: 2021-09-21 11:15:16

* @LastEditors: changqing

* @Usage:

*/

module.exports = {

presets: [

'@babel/preset-react',//把React编译 成ES5

[

'@babel/preset-env',//把ES6编译 成ES5

{

modules: 'auto',

targets: {//编译 兼容的目标

browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11'],

},

},

],

],

plugins: [

[//支持typescript

'@babel/plugin-transform-typescript',

{

isTSX: true,

},

],

//提取一些编译运行时帮助方法

['@babel/plugin-transform-runtime'],

],

}

2.4 .gitignore

.gitignore


*.iml

.idea/

.ipr

.iws

*~

~*

*.diff

*.patch

*.bak

.DS_Store

Thumbs.db

.project

.*proj

.svn/

*.swp

*.swo

*.log

*.log.*

*.json.gzip

node_modules/

.buildpath

.settings

npm-debug.log

nohup.out

_site

_data

dist

report.html

/lib

/es

elasticsearch-*

config/base.yaml

/.vscode/

/coverage

yarn.lock

package-lock.json

components/**/*.js

components/**/*.jsx

!components/**/__tests__/**/*.js

!components/**/__tests__/**/*.js.snap

/.history

*.tmp

# Docs templates

site/theme/template/Color/ColorPicker.jsx

site/theme/template/IconDisplay/*.js

site/theme/template/IconDisplay/*.jsx

site/theme/template/IconDisplay/fields.js

site/theme/template/Home/**/*.jsx

site/theme/template/utils.jsx

site/theme/template/Layout/Footer.jsx

site/theme/template/Layout/Header/**/*.jsx

site/theme/template/Layout/SiteContext.jsx

site/theme/template/Content/Article.jsx

site/theme/template/Content/EditButton.jsx

site/theme/template/Resources/*.jsx

site/theme/template/Resources/**/*.jsx

site/theme/template/NotFound.jsx

scripts/previewEditor/index.html

components/version/version.tsx

# Image snapshot diff

__diff_output__/

__image_snapshots__/

/jest-stare

/imageSnapshots

/imageDiffSnapshots

storybook-static

sh.exe.stackdump

/snapshots

/diffSnapshots

2.5 tsconfig.json


{

"compilerOptions": {

"strictNullChecks": true,

"module": "esnext",

"moduleResolution": "node",

"esModuleInterop": true,

"experimentalDecorators": true,

"jsx": "react",

"noUnusedParameters": true,

"noUnusedLocals": true,

"noImplicitAny": true,

"target": "es6",

"lib": ["dom", "es2017"],

"skipLibCheck": true,

"types": ["node"]

},

"exclude": ["node_modules", "lib", "es"]

}

2.6 index.js


module.exports = require('./components');

2.7 components\index.tsx


import Button from './button';

export type { ButtonProps } from './button';

export { Button };

2.8 button\index.tsx

components\button\index.tsx


import Button from './button';

export type { ButtonProps } from './button';

export { Button };

2.9 button.tsx

components\button\button.tsx


import React, { ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}

const Button: React.FC<ButtonProps> = (props) => {

const { children } = props;

return <button type="button">{children}</button>;

};

export default Button;

export type { ButtonProps };

3.storybook

storybook是一个用于开发UI组件的开源工具,是UI组件的开发环境

@storybook/react是React的运行环境

@storybook/addon-essentials是storybook最好插件的合集

3.1 安装


yarn add @storybook/react @storybook/addon-essentials --dev

3.2 .storybook\main.js

.storybook\main.js


module.exports = {

stories: [

"../components/Introduction.stories.mdx",

"../components/Install.stories.mdx",

"../components/Components.stories.mdx",

"../components/**/*.stories.mdx",

"../components/**/*.stories.@(js|jsx|ts|tsx)"

],

addons: ['@storybook/addon-essentials'],

};

3.3 Introduction.stories.mdx


<Meta title="开始/介绍" />

## Ant Design of React

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

3.3 Introduction.stories.mdx

components\Introduction.stories.mdx


<Meta title="开始/介绍" />

## Ant Design of React

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

3.4 Install.stories.mdx

components\Install.stories.mdx


<Meta title="开始/安装使用" />

## 安装

使用 npm 或 yarn 安装

npm install antd --save

yarn add antd

## 浏览器引入

在浏览器中使用 script 和 link 标签直接引入文件,并使用全局变量 ant

我们在 npm 发布包内的 antdesign/dist 目录下提供了 antd.js

## 示例

import { Button } from 'antd';

ReactDOM.render(<Button>按钮</Button>, mountNode);

3.5 Components.stories.mdx

components\Components.stories.mdx


<Meta title="开始/组件总览" />

## 组件总览

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

## 通用

- Button 按钮

- Icon 图标

- Typography 排版

## 布局

- Divider 分割线

- Grid 栅格

- Layout 布局

- Space 间距

## 导航

- Affix 固钉

- Breadcrumb 面包屑

- Dropdown 下拉菜单

- Menu 导航菜单

- Pagination 分页

- PageHeader 页头

- Steps 步骤条

## 数据录入

- AutoComplete 自动完成

- Checkbox 多选框

- Cascader 级联选择

- DatePicker 日期选择框

- Form 表单

- InputNumber 数字输入框

- Input 输入框

- Mentions 提及

- Rate 评分

- Radio 单选框

- Switch 开关

- Slider 滑动输入条

- Select 选择器

- TreeSelect 树选择

- Transfer 穿梭框

- TimePicker 时间选择框

- Upload 上传

## 数据展示

- Avatar 头像

- Badge 徽标数

- Comment 评论

- Collapse 折叠面板

- Carousel 走马灯

- Card 卡片

- Calendar 日历

- Descriptions 描述列表

- Empty 空状态

- Image 图片

- List 列表

- Popover 气泡卡片

- Statistic 统计数值

- Tree 树形控件

- Tooltip 文字提示

- Timeline 时间轴

- Tag 标签

- Tabs 标签页

- Table 表格

## 反馈

- Alert 警告提示

- Drawer 抽屉

- Modal 对话框

- Message 全局提示

- Notification 通知提醒框

- Progress 进度条

- Popconfirm 气泡确认框

- Result 结果

- Spin 加载中

- Skeleton 骨架屏

## 其他

- Anchor 锚点

- BackTop 回到顶部

- ConfigProvider 全局化配置

3.6 button.stories.ts

components\button\button.stories.tsx


import React from "react";

import { ComponentStory, ComponentMeta } from "@storybook/react";

import Button from ".";

export default {

title: "通用/Button(按钮)",

component: Button,

} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => (

<Button {...args} />

);

export const Basic = Template.bind({});

Basic.args = {

children: "按钮",

};

3.7 package.json


"scripts": {

+ "storybook": "start-storybook -p 6006",

+ "build-storybook": "build-storybook"

},

4.测试

configuration

code-transformation

4.1 安装

jest是一个令人愉快的 JavaScript 测试框架

Enzyme 用于 React 的 JS 测试工具

puppeteer是一个控制 headless Chrome 的 Node.js API

jest-image-snapshot执行图像比较的Jest匹配器,对于视觉回归测试非常有用


yarn add jest @types/jest @wojtekmaj/enzyme-adapter-react-17 puppeteer @types/puppeteer jest-environment-puppeteer @types/jest-environment-puppeteer jest-puppeteer jest-image-snapshot @types/jest-image-snapshot --dev

yarn add enzyme @types/enzyme --dev

4.2 tests\setup.js

tests\setup.js


const React = require('react');

const Enzyme = require('enzyme');

const Adapter = require('@wojtekmaj/enzyme-adapter-react-17')

Enzyme.configure({ adapter: new Adapter() });

4.3 tests\index.html

tests\index.html


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>Amazing Antd</title>

<style>

body {

border: 5px solid #1890ff;

}

</style>

</head>

<body>

<div id="root"></div>

</body>

</html>

4.4 unit.jest.js

unit.jest.js


module.exports = {

verbose: true,

testEnvironment: 'jsdom',//运行测试的环境

setupFiles: ['./tests/setup.js'],

testMatch: ['**/unit/**/*.(spec|test).(js|ts|jsx|tsx)'],

collectCoverage: true,

collectCoverageFrom: [

'components/**/*.(js|ts|jsx|tsx)',

'!components/**/*.stories.(js|ts|jsx|tsx)',

'!components/**/*.(spec|test).(js|ts|jsx|tsx)',

],

};

4.5 e2e.jest.js

e2e.jest.js


module.exports = {

verbose: true,

testEnvironment: 'jest-environment-puppeteer',

setupFiles: ['./tests/setup.js'],

preset: 'jest-puppeteer',

testMatch: ['**/e2e/**/*.(spec|test).(j|t)sx'],

};

4.6 unit\index.test.tsx

components\button\unit\index.test.tsx


import React from 'react';

import { mount } from 'enzyme';

import Button from '..';

describe('Button', () => {

it('mount correctly', () => {

expect(() => mount(<Button>Follow</Button>)).not.toThrow();

});

});

4.7 snapshot.spec.tsx

components\button\e2e\snapshot.spec.tsx


import React from 'react';

import ReactDOMServer from 'react-dom/server';

import { configureToMatchImageSnapshot } from 'jest-image-snapshot';

import Button from '..';

import 'jest-environment-puppeteer';

const toMatchSnapshot = configureToMatchImageSnapshot({

customSnapshotsDir: `${process.cwd()}/snapshots`,

customDiffDir: `${process.cwd()}/diffSnapshots`,

});

expect.extend({ toMatchSnapshot });

describe('Button snapshot', () => {

it('screenshot should correct', async () => {

await jestPuppeteer.resetPage();

await page.goto(`file://${process.cwd()}/tests/index.html`);

const html = ReactDOMServer.renderToString(<Button>按钮</Button>);

await page.evaluate((innerHTML:string) => {

document.querySelector('#root')!.innerHTML = innerHTML;

}, html);

const screenshot = await page.screenshot();

expect(screenshot).toMatchSnapshot();

});

});

4.8 jest-puppeteer.config.js

jest-puppeteer.config.js


module.exports = {

launch: {

headless: true,

},

browserContext: 'default',

};

4.9 package.json

package.json


+ "test:unit": "jest --config unit.jest.js",

+ "test:e2e": "jest --config e2e.jest.js",

+ "test": "npm run test:unit && npm run test:e2e"

5.eslint

eslint是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具

eslint-config-airbnb提供的eslint配置

5.1 安装


yarn add @typescript-eslint/parser eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks and eslint-plugin-jsx-a11y eslint-config-airbnb --dev

5.2 .eslintrc.js

.eslintrc.js


module.exports = {

parser: '@typescript-eslint/parser',

extends: ['airbnb'],

env: {

browser: true,

node: true,

jasmine: true,

jest: true,

es6: true,

},

rules: {

'import/extensions': 0,

'import/no-unresolved': 0,

'react/jsx-filename-extension': 0,

// https://github.com/typescript-eslint/typescript-eslint/issues/2540#issuecomment-692866111

'no-use-before-define': 0,

'import/prefer-default-export': 0,

'import/no-named-default': 0,

'no-console': 0,

'no-param-reassign': 0,

'func-names': 0,

}

};

5.3 .eslintignore


components/**/e2e/*

components/**/unit/*

components/**/*.stories.*

lib

es

umd

dist

.storybook

gulpfile.js

5.4 package.json


"scripts": {

+ "lint": "eslint --ext .js,.jsx,.ts,.tsx components",

+ "lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx components"

6.prettier

prettier 是一个有主见的代码格式化工具

eslint-config-prettier关闭和prettier冲突的规则

eslint-plugin-prettier把Prettier当Eslint规则来运行并且进行报告

6.1 安装依赖


yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev

6.2 .eslintrc.js

.eslintrc.js


module.exports = {

+ extends: ['airbnb','prettier'],

+ plugins: ['prettier'],

rules: {

+ 'prettier/prettier': ['error', { endOfLine: 'auto' }],

};

6.3 .prettierrc

.prettierrc


{

"singleQuote": true

}

6.5 settings.json

.vscode\settings.json


{

"editor.codeActionsOnSave": {

"source.fixAll.eslint": true

},

"files.autoSave": "afterDelay"

}

7.editorconfig

editorconfig由用于定义编码样式的文件格式和一组文本编辑器插件组成,这些插件使编辑器能够读取文件格式并遵循定义的样式

7.1 .editorconfig


# top-most EditorConfig file

root = true

# Unix-style newlines with a newline ending every file

[*.{js,css}]

end_of_line = lf

insert_final_newline = true

indent_style = space

indent_size = 2

8. git hook

Git钩子能在特定的重要动作发生时触发自定义脚本

husky可以让我们向项目中方便添加 git hooks

lint-staged用于实现每次提交只检查本次提交所修改的文件

8.1 安装


yarn add husky --dev

npm set-script prepare "husky install"

npm run prepare

8.2 pre-commit

pre-commit在git add提交之后,然后执行git commit时执行,脚本执行没报错就继续提交,反之就驳回提交的操作

可以在 git commit 之前检查代码,保证所有提交到版本库中的代码都是符合规范的

8.2.1 安装脚本


npx husky add .husky/pre-commit "npx lint-staged"

8.2.2 .lintstagedrc


{

"*.{js,ts,jsx,tsx}": "eslint"

}

8.3 commit-msg

validate-commit-msg用于检查 Node 项目的 Commit message 是否符合格式

commitizen插件可帮助实现一致的提交消息

cz-customizable可以实现自定义的提交

@commitlint/cli可以检查提交信息

@commitlint/config-conventional检查您的常规提交

8.3.1 安装依赖


yarn add commitizen cz-customizable @commitlint/cli @commitlint/config-conventional --dev

8.3.2 安装脚本


npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

8.3.3 .cz-config.js


module.exports = {

types: [

{ value: "feat", name: "feat:一个新特性" },

{ value: "fix", name: "fix:修复BUG" },

],

scopes: [{ name: "admin" }, { name: "user" }],

};

8.3.4 commitlint.config.js


module.exports = {

extends: ['@commitlint/config-conventional'],

};

8.4 pre-push

可以在 git push 之前执行单元测试,保证所有的提交的代码经过的单元测试

8.4.1 安装脚本


npx husky add .husky/pre-push "npm run test"

9. 编译发布

rimraf是 node版本的 rm -rf

gulp将开发流程中让人痛苦或耗时的任务自动化,从而减少你所浪费的时间、创造更大价值。

merge2合并多个流为同一个

9.1 安装依赖


yarn add rimraf gulp gulp-typescript gulp-babel merge2 --dev


npm version patch

npm publish

cat ~/.npmrc

9.2 gulpfile.js

gulpfile.js


const gulp = require('gulp');//定义执行任务

const path = require('path');//处理路径

const rimraf = require('rimraf');//删除跑路的 rm -rf

const ts = require('gulp-typescript');

const babel = require('gulp-babel');

const merge2 = require('merge2');//Promise.all

const {compilerOptions} = require('./tsconfig.json');

const tsConfig = {

noUnusedParameters: true,//不能有未使用的参数

noUnusedLocals: true,//不能有未使用的本地变量

strictNullChecks: true,//严格的Null检查

target: 'es6',//编译 的目标

jsx: 'react',//jsx如何处理preserve 保留不处理 react变成React.createElement()

moduleResolution: 'node',//模块的查找规则 node

declaration: true,//生成声明文件 d.ts

allowSyntheticDefaultImports: true,//允许 默认导入

...compilerOptions,

}

const babelConfig = require('./babel.config');

//准备好要编译 的文件

//glob 文件匹配模板,类似于正则

const source = [

'components/**/*.{js,ts,jsx,tsx}',

'!components/**/*.stories.{js,ts,jsx,tsx}',

'!components/**/e2e/*',

'!components/**/unit/*',

];

//C:\aproject\antd\components

const base = path.join(process.cwd(), 'components');

function getProjectPath(filePath) {

return path.join(process.cwd(), filePath);

}

//C:\aproject\antd\lib

const libDir = getProjectPath('lib');

//C:\aproject\antd\es

const esDir = getProjectPath('es');

/**

* 执行编译

* @param {*} modules 是否要转换模块

*/

function compile(modules) {

const targeDir = modules===false?esDir:libDir;

rimraf.sync(targeDir);//删除老的内容 rm -rf

//把文件匹配模式传给gulp,gulp会按这个模式把文件匹配了出来

//ts转译后会生成二个流,一个流是JS一个流是类型声明d.ts

const {js,dts} = gulp.src(source,{base}).pipe(ts(tsConfig));

const dtsStream = dts.pipe(gulp.dest(targeDir));

let jsStream = js;

if(modules){//如果要转成ES5,就用babel进行转义

jsStream=js.pipe(babel(babelConfig));

}

jsStream=jsStream.pipe(gulp.dest(targeDir));

return merge2([jsStream,dtsStream]);

}

gulp.task('compile-with-es',(done)=>{

console.log('compile to es');

compile(false).on('finish',done);

});

gulp.task('compile-with-lib',(done)=>{

console.log('compile to js');

compile().on('finish',done);

});

gulp.task('compile',gulp.parallel('compile-with-es','compile-with-lib'));

9.3 package.json


+ "main": "lib/index.js",

+ "module": "es/index.js",

+ "unpkg": "dist/antd.js",

+ "typings": "lib/index.d.ts",

+ "files": [

+ "dist",

+ "es",

+ "lib"

+ ],

10. 持续集成

Travis CI提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器

10.1 .travis.yml


language: node_js

node_js:

- "stable"

cache:

directories:

- node_modules

env:

- CI=true

install:

- yarn config set registry https://registry.npm.taobao.org

- yarn install

script:

- npm run build-storybook

deploy:

- provider: pages

skip_cleanup: true

github_token: $GITHUB_TOKEN

local_dir: storybook-static

on:

branch: master

- provider: npm

email: 981603572@qq.com

api_key: "$NPM_TOKEN"

skip_cleanup: true

on:

branch: master

10.1 设置环境变量

app.travis-ci.com/ 对应项目的 settings 的Environment Variables 添加GITHUB_TOKEN 和NPM_TOKEN

代码仓库地址

antd

组件库文档地址

文档地址