前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零打造组件库

从零打造组件库

作者头像
leocoder
发布2022-03-09 20:40:33
1.6K0
发布2022-03-09 20:40:33
举报
文章被收录于专栏:前端进阶之路前端进阶之路

前言

组件库,一套标准化的组件集合,是前端工程师开发提效不可或缺的工具。

业内优秀的组件库比如 Antd DesignElement UI,大大节省了我们的开发时间。那么,做一套组件库,容易吗?

答案肯定是不容易,当你去做这件事的时候,会发现它其实是一套体系。从开发、编译、测试,到最后发布,每一个流程都需要大量的知识积累。但是当你真正完成了一个组件库的搭建后,会发现收获的也许比想象中更多。

希望能够通过本文帮助大家梳理一套组件库搭建的知识体系,聚点成面,如果能够帮助到你,也请送上一颗 Star 吧。

示例组件库线上站点: Frog-UI

仓库地址:Frog-Kits

概览

本文主要包括以下内容:

  • 环境搭建:​Typescript​ + ​ESLint​ + ​StyleLint​ + ​Prettier​ + ​Husky
  • 组件开发:标准化的组件开发目录及代码结构
  • 文档站点:基于 docz 的文档演示站点
  • 编译打包:输出符合 ​umd​ / ​esm​ / ​cjs​ 三种规范的打包产物
  • 单元测试:基于 jest 的 ​React​ 组件测试方案及完整报告
  • 一键发版:整合多条命令,流水线控制 npm publish 全部过程
  • 线上部署:基于 now 快速部署线上文档站点

如有错误欢迎在评论区进行交流~

初始化

整体目录

代码语言:javascript
复制
├── CHANGELOG.md    // CHANGELOG
├── README.md    // README
├── babel.config.js    // babel 配置
├── build    // 编译发布相关
│   ├── constant.js
│   ├── release.js
│   └── rollup.config.dist.js
├── components    // 组件源码
│   ├── Alert
│   ├── Button
│   ├── index.tsx
│   └── style
├── coverage    // 测试报告
│   ├── clover.xml
│   ├── coverage-final.json
│   ├── lcov-report
│   └── lcov.info
├── dist    // 组件库打包产物:UMD
│   ├── frog.css
│   ├── frog.js
│   ├── frog.js.map
│   ├── frog.min.css
│   ├── frog.min.js
│   └── frog.min.js.map
├── doc    // 组件库文档站点
│   ├── Alert.mdx
│   └── button.mdx
├── doczrc.js    // docz 配置
├── es    // 组件库打包产物:ESM
│   ├── Alert
│   ├── Button
│   ├── index.js
│   └── style
├── gatsby-config.js    // docz 主题配置
├── gulpfile.js    // gulp 配置
├── lib    // 组件库打包产物:CJS
│   ├── Alert
│   ├── Button
│   ├── index.js
│   └── style
├── package-lock.json
├── package.json    // package.json
└── tsconfig.json    // typescript 配置

配置 ESLint + StyleLint + Prettier

每个 Lint 都可以单独拿出来写一篇文章,但配置不是我们的重点,所以这里使用 @umijs/fabric,一个包含 ESLint + StyleLint + Prettier 的配置文件合集,能够大大节省我们的时间。

感兴趣的同学可以去查看它的源码,在时间允许的情况下自己从零配置当做学习也是不错的。

安装

代码语言:javascript
复制
yarn add @umijs/fabric prettier @typescript-eslint/eslint-plugin -D

.eslintrc.js

代码语言:javascript
复制
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    require.resolve('@umijs/fabric/dist/eslint'),
    'prettier/@typescript-eslint',
    'plugin:react/recommended'
  ],
  rules: {
    'react/prop-types': 'off',
    "no-unused-expressions": "off",
    "@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true }]
  },
  ignorePatterns: ['.eslintrc.js'],
  settings: {
    react: {
      version: "detect"
    }
  }
}

由于 @umijs/fabric 中判断 isTsProject 的目录路径如图所示是基于 src 的,且无法修改,我们这里组件源码在 components 路径下,所以这里要手动添加相关 typescript 的配置。

.prettierrc.js

代码语言:javascript
复制
const fabric = require('@umijs/fabric');

module.exports = {
  ...fabric.prettier,
};

.stylelintrc.js

代码语言:javascript
复制
module.exports = {
  extends: [require.resolve('@umijs/fabric/dist/stylelint')],
};

配置 Husky + Lint-Staged

husky 提供了多种钩子来拦截 git 操作,比如 git commitgit push 等。但是一般情况我们都是接手已有的项目,如果对所有代码都做 Lint 检查的话修复成本太高了,所以我们希望能够只对自己提交的代码做检查,这样就可以从现在开始对大家的开发规范进行约束,已有的代码等修改的时候再做检查。

这样就引入了 lint-staged,可以只对当前 commit 的代码做检查并且可以编写正则匹配文件。

安装

代码语言:javascript
复制
yarn add husky lint-staged -D

package.json

代码语言:javascript
复制
"lint-staged": {
  "components/**/*.ts?(x)": [
    "prettier --write",
    "eslint --fix"
  ],
  "components/**/**/*.less": [
    "stylelint --syntax less --fix"
  ]
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
}

配置 Typescript

typescript.json

代码语言:javascript
复制
{
  "compilerOptions": {
    "baseUrl": "./",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es6", "dom"],
    "sourceMap": true,
    "allowJs": true,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDir": "src",
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "paths": {
      "components/*": ["src/components/*"]
    }
  },
  "include": [
    "components"
  ],
  "exclude": [
    "node_modules",
    "build",
    "dist",
    "lib",
    "es"
  ]
}

组件开发

正常写组件大家都很熟悉了,这里我们主要看一下目录结构和部分代码:

代码语言:javascript
复制
├── Alert
│   ├── __tests__
│   ├── index.tsx
│   └── style
├── Button
│   ├── __tests__
│   ├── index.tsx
│   └── style
├── index.tsx
└── style
    ├── color
    ├── core
    ├── index.less
    └── index.tsx

components/index.ts​​ 是整个组件库的入口,负责收集所有组件并导出:

代码语言:javascript
复制
export { default as Button } from './Button';
export { default as Alert } from './Alert';

components/style​​ 包含组件库的基础 ​less​ 文件,包含 ​core、​color​ 等通用样式及变量设置。

每个 ​style​ 目录下都至少包含 ​index.tsx​ 及 ​index.less​ 两个文件:

style/index.tsx

代码语言:javascript
复制
import './index.less';

style/index.less

代码语言:javascript
复制
@import './core/index';
@import './color/default';

可以看到,​style/index.tsx​ 是作为每个组件样式引用的唯一入口而存在。

__tests__​ 是组件的单元测试目录,后续会单独讲到。具体 ​Alert​ 和 ​Button​ 组件的代码都很简单,这里就不赘述,大家可以去源码里找到。

组件测试

为什么要写测试以及是否有必要做测试,社区内已经有很多的探讨,大家可以根据自己的实际业务场景来做决定,我个人的意见是:

  • 基础工具,一定要做好单元测试,比如 ​utils、​hooks、​components
  • 业务代码,由于更新迭代快,不一定有时间去写单测,根据节奏自行决定

但是单测的意义肯定是正向的:

The more your tests resemble the way your software is used, the more confidence they can give you. - Kent C. Dodds

安装

代码语言:javascript
复制
yarn add jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer @testing-library/react -D
yarn add @types/jest @types/react-test-renderer -D

package.json

代码语言:javascript
复制
"scripts": {
  "test": "jest",
  "test:coverage": "jest --coverage"
}

在每个组件下新增 ​__tests__/index.test.tsx​,作为单测入口文件。

代码语言:javascript
复制
import React from 'react';
import renderer from 'react-test-renderer';
import Alert from '../index';

describe('Component <Alert /> Test', () => {
  test('should render default', () => {
    const component = renderer.create(<Alert message="default" />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  test('should render specific type', () => {
    const types: any[] = ['success', 'info', 'warning', 'error'];
    const component = renderer.create(
      <>
        {types.map((type) => (
          <Alert key={type} type={type} message={type} />
        ))}
      </>,
    );
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

这里采用的是 ​snapshot​ 快照的测试方式,所谓快照,就是在当前执行测试用例的时候,生成一份测试结果的快照,保存在 ​__snapshots__/index.test.tsx.snap​ 文件中。下次再执行测试用例的时候,如果我们修改了组件的源码,那么会将本次的结果快照和上次的快照进行比对,如果不匹配,则测试不通过,需要我们修改测试用例更新快照。这样就保证了每次源码的修改必须要和上次测试的结果快照做比对,才能确定是否通过,省去了写复杂的逻辑测试代码,是一种简化的测试手段。

还有一种是基于 ​DOM​ 的测试,基于 ​@testing-library/react

代码语言:javascript
复制
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import renderer from 'react-test-renderer';
import Button from '../index';

describe('Component <Button /> Test', () => {
  let testButtonClicked = false;
  const onClick = () => {
    testButtonClicked = true;
  };

  test('should render default', () => {
    // snapshot test
    const component = renderer.create(<Button onClick={onClick}>default</Button>);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();

    // dom test
    render(<Button onClick={onClick}>default</Button>);
    const btn = screen.getByText('default');
    fireEvent.click(btn);
    expect(testButtonClicked).toEqual(true);
  });
});

可以看到,​@testing-library/react​ 提供了一些方法,​render​ 将组件渲染到 ​DOM​ 中,​screen​ 提供了各种方法可以从页面中获取相应 ​DOM​ 元素,​fireEvent​ 负责触发 ​DOM​ 元素绑定的事件。

更多关于组件测试的细节推荐阅读以下文章:

组件库打包

组件库打包是我们的重头戏,我们主要实现以下目标:

  • 导出 umd / cjs / esm 三种规范文件
  • 导出组件库 css 样式文件
  • 支持按需加载

这里我们围绕 ​package.json​ 中的三个字段展开:​main、​module​ 以及 ​unpkg

代码语言:javascript
复制
{
  "main": "lib/index.js",
  "module": "es/index.js",
  "unpkg": "dist/frog.min.js"
}

我们去看业内各个组件库的源码时,总能看到这三个字段,那么它们的作用究竟是什么呢?

  • main​,是包的入口文件,我们通过 ​require​ 或者 ​import​ 加载 ​npm​ 包的时候,会从 ​main​ 字段获取需要加载的文件
  • module​,是由打包工具提出的一个字段,目前还不在 package.json 官方规范中,负责指定符合 esm 规范的入口文件。当 ​webpack​ 或者 ​rollup​ 在加载 ​npm​ 包的时候,如果看到有 ​module​ 字段,会优先加载 ​esm​ 入口文件,因为可以更好的做 ​tree-shaking​,减小代码体积。
  • unpkg​,也是一个非官方字段,负责让 ​npm​ 包中的文件开启 ​CDN​ 服务,意味着我们可以通过 https://unpkg.com/ 直接获取到文件内容。比如这里我们就可以通过 https://unpkg.com/frog-ui@0.1.3/dist/frog.min.js 直接获取到 ​umd​ 版本的库文件。

我们使用 ​gulp 来串联工作流,并通过三条命令分别导出三种格式文件:

代码语言:javascript
复制
"scripts": {
  "build": "yarn build:dist && yarn build:lib && yarn build:es",
  "build:dist": "rm -rf dist && gulp compileDistTask",
  "build:lib": "rm -rf lib && gulp",
  "build:es": "rm -rf es && cross-env ENV_ES=true gulp"
}
  • build​,聚合命令
  • build:es​,输出 ​esm​ 规范,目录为 ​es
  • build:lib,输出 cjs 规范,目录为 lib
  • build:dist​,输出 ​umd​ 规范,目录为 ​dist

导出 umd

通过执行 ​gulp compileDistTask​ 来导出 ​umd​ 文件,具体看一下 gulpfile:

gulpfile

代码语言:javascript
复制
function _transformLess(lessFile, config = {}) {
  const { cwd = process.cwd() } = config;
  const resolvedLessFile = path.resolve(cwd, lessFile);

  let data = readFileSync(resolvedLessFile, 'utf-8');
  data = data.replace(/^\uFEFF/, '');

  const lessOption = {
    paths: [path.dirname(resolvedLessFile)],
    filename: resolvedLessFile,
    plugins: [new NpmImportPlugin({ prefix: '~' })],
    javascriptEnabled: true,
  };

  return less
    .render(data, lessOption)
    .then(result => postcss([autoprefixer]).process(result.css, { from: undefined }))
    .then(r => r.css);
}

async function _compileDistJS() {
  const inputOptions = rollupConfig;
  const outputOptions = rollupConfig.output;

  // 打包 frog.js
  const bundle = await rollup.rollup(inputOptions);
  await bundle.generate(outputOptions);
  await bundle.write(outputOptions);

  // 打包 frog.min.js
  inputOptions.plugins.push(terser());
  outputOptions.file = `${DIST_DIR}/${DIST_NAME}.min.js`;

  const bundleUglify = await rollup.rollup(inputOptions);
  await bundleUglify.generate(outputOptions);
  await bundleUglify.write(outputOptions);
}

function _compileDistCSS() {
  return src('components/**/*.less')
    .pipe(
      through2.obj(function (file, encoding, next) {
        if (
          // 编译 style/index.less 为 .css
          file.path.match(/(\/|\\)style(\/|\\)index\.less$/)
        ) {
          _transformLess(file.path)
            .then(css => {
              file.contents = Buffer.from(css);
              file.path = file.path.replace(/\.less$/, '.css');
              this.push(file);
              next();
            })
            .catch(e => {
              console.error(e);
            });
        } else {
          next();
        }
      }),
    )
    .pipe(concat(`./${DIST_NAME}.css`))
    .pipe(dest(DIST_DIR))
    .pipe(uglifycss())
    .pipe(rename(`./${DIST_NAME}.min.css`))
    .pipe(dest(DIST_DIR));
}

exports.compileDistTask = series(_compileDistJS, _compileDistCSS);

rollup.config.dist.js

代码语言:javascript
复制
const resolve = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const commonjs = require('@rollup/plugin-commonjs');
const { terser } = require('rollup-plugin-terser');
const image = require('@rollup/plugin-image');
const { DIST_DIR, DIST_NAME } = require('./constant');

module.exports = {
  input: 'components/index.tsx',
  output: {
    name: 'Frog',
    file: `${DIST_DIR}/${DIST_NAME}.js`,
    format: 'umd',
    sourcemap: true,
    globals: {
      'react': 'React',
      'react-dom': 'ReactDOM'
    }
  },
  plugins: [
    peerDepsExternal(),
    commonjs({
      include: ['node_modules/**', '../../node_modules/**'],
      namedExports: {
        'react-is': ['isForwardRef', 'isValidElementType'],
      }
    }),
    resolve({
      extensions: ['.tsx', '.ts', '.js'],
      jsnext: true,
      main: true,
      browser: true
    }),
    babel({
      exclude: 'node_modules/**',
      babelHelpers: 'bundled',
      extensions: ['.js', '.jsx', 'ts', 'tsx']
    }),
    image()
  ]
}

rollup​ 或者 ​webpack​ 这类打包工具,最擅长的就是由一个或多个入口文件,依次寻找依赖,打包成一个或多个 ​Chunk​ 文件,而 ​umd​ 就是要输出为一个 ​js​ 文件。

所以这里选用 ​rollup​ 负责打包 ​umd​ 文件,入口为 ​component/index.tsx​,输出 ​format​ 为 ​umd​ 格式。

为了同时打包 ​frog.js​ 和 ​frog.min.js​,在 ​_compileDistJS​ 中引入了 teser 插件,执行了两次 ​rollup​ 打包。

一个组件库只有 ​JS​ 文件肯定不够用,还需要有样式文件,比如使用 ​Antd​ 时:

代码语言:javascript
复制
import { DatePicker } from 'antd';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'

ReactDOM.render(<DatePicker />, mountNode);

所以,我们也要打包出一份组件库的 ​CSS​ 文件。

这里 ​_compileDistCSS​ 的作用是,遍历 ​components​ 目录下的所有 ​less​ 文件,匹配到所有的 ​index.less​ 入口样式文件,使用 ​less​ 编译为 ​CSS​ 文件,并且进行聚合,最后输出为 ​frog.css​ 和 ​frog.min.css

最终 ​dist​ 目录结构如下:

代码语言:javascript
复制
├── frog.css
├── frog.js
├── frog.js.map
├── frog.min.css
├── frog.min.js
└── frog.min.js.map

导出 cjs 和 esm

导出 ​cjs​ 或者 ​esm​,意味着模块化导出,并不是一个聚合的 ​JS​ 文件,而是每个组件是一个模块,只不过 ​cjs​ 的代码时符合 ​Commonjs​ 标准,​esm​ 的代码时 ​ES Module​ 标准。

所以,我们自然的就想到了 ​babel​,它的作用不就是编译高级别的代码到各种格式嘛。

gulpfile

代码语言:javascript
复制
function _compileJS() {
  return src(['components/**/*.{tsx, ts, js}', '!components/**/__tests__/*.{tsx, ts, js}'])
    .pipe(
      babel({
        presets: [
          [
            '@babel/preset-env',
            {
              modules: ENV_ES === 'true' ? false : 'commonjs',
            },
          ],
        ],
      }),
    )
    .pipe(dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR));
}

function _copyLess() {
  return src('components/**/*.less').pipe(dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR));
}

function _copyImage() {
  return src('components/**/*.@(jpg|jpeg|png|svg)').pipe(
    dest(ENV_ES === 'true' ? ES_DIR : LIB_DIR),
  );
}

exports.default = series(_compileJS, _copyLess, _copyImage);

babel.config.js

代码语言:javascript
复制
module.exports = {
  presets: [
    "@babel/preset-react",
    "@babel/preset-typescript",
    "@babel/preset-env"
  ],
  plugins: [
    "@babel/plugin-proposal-class-properties"
  ]
};

这里代码就相对简单了,扫描 ​components​ 目录下的 ​tsx​ 文件,使用 ​babel​ 编译后,拷贝到 ​es​ 或 ​lib​ 目录。​less​ 文件直接拷贝,这里 ​_copyImage​ 是为了防止有图片,也直接拷贝过去,但是组件库中不建议用图片,可以用字体图标代替。

组件文档

这里使用 docz 来搭建文档站点,更具体的使用方法大家可以阅读官网文档,这里不再赘述。

doc/Alert.mdx

代码语言:javascript
复制
---
name: Alert 警告提示
route: /alert
menu: 反馈
---

import { Playground, Props } from 'docz'
import { Alert } from '../components/';
import '../components/Alert/style';

# Alert
警告提示,展现需要关注的信息。

<Props of={Alert} />

## 基本用法

<Playground>
  <Alert message="Success Text" type="success" />
  <Alert message="Info Text" type="info" />
  <Alert message="Warning Text" type="warning" />
  <Alert message="Error Text" type="error" />
</Playground>

package.json

代码语言:javascript
复制
"scripts": {
  "docz:dev": "docz dev",
  "docz:build": "docz build",
  "docz:serve": "docz build && docz serve"
}

线上文档站点部署

这里使用 now.sh 来部署线上站点,注册后安装命令行,登录成功。

代码语言:javascript
复制
yarn docz:build
cd .docz/dist
now deploy
vercel --production

一键发版

我们在发布新版 npm 包时会有很多步骤,这里提供一套脚本来实现一键发版。

安装

代码语言:javascript
复制
yarn add conventional-changelog-cli -D

release.js

代码语言:javascript
复制
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const util = require('util');
const semver = require('semver');
const exec = util.promisify(child_process.exec);
const semverInc = semver.inc;
const pkg = require('../package.json');

const currentVersion = pkg.version;

const run = async command => {
  console.log(chalk.green(command));
  await exec(command);
};

const logTime = (logInfo, type) => {
  const info = `=> ${type}:${logInfo}`;
  console.log((chalk.blue(`[${new Date().toLocaleString()}] ${info}`)));
};

const getNextVersions = () => ({
  major: semverInc(currentVersion, 'major'),
  minor: semverInc(currentVersion, 'minor'),
  patch: semverInc(currentVersion, 'patch'),
  premajor: semverInc(currentVersion, 'premajor'),
  preminor: semverInc(currentVersion, 'preminor'),
  prepatch: semverInc(currentVersion, 'prepatch'),
  prerelease: semverInc(currentVersion, 'prerelease'),
});

const promptNextVersion = async () => {
  const nextVersions = getNextVersions();
  const { nextVersion } = await inquirer.prompt([
    {
      type: 'list',
      name: 'nextVersion',
      message: `Please select the next version (current version is ${currentVersion})`,
      choices: Object.keys(nextVersions).map(name => ({
        name: `${name} => ${nextVersions[name]}`,
        value: nextVersions[name]
      }))
    }
  ]);
  return nextVersion;
};

const updatePkgVersion = async nextVersion => {
  pkg.version = nextVersion;
  logTime('Update package.json version', 'start');
  await fs.writeFileSync(path.resolve(__dirname, '../package.json'), JSON.stringify(pkg));
  await run('npx prettier package.json --write');
  logTime('Update package.json version', 'end');
};

const test = async () => {
  logTime('Test', 'start');
  await run(`yarn test:coverage`);
  logTime('Test', 'end');
};

const genChangelog = async () => {
  logTime('Generate CHANGELOG.md', 'start');
  await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0');
  logTime('Generate CHANGELOG.md', 'end');
};

const push = async nextVersion => {
  logTime('Push Git', 'start');
  await run('git add .');
  await run(`git commit -m "publish frog-ui@${nextVersion}" -n`);
  await run('git push');
  logTime('Push Git', 'end');
};

const tag = async nextVersion => {
  logTime('Push Git', 'start');
  await run(`git tag v${nextVersion}`);
  await run(`git push origin tag frog-ui@${nextVersion}`);
  logTime('Push Git Tag', 'end');
};

const build = async () => {
  logTime('Components Build', 'start');
  await run(`yarn build`);
  logTime('Components Build', 'end');
};

const publish = async () => {
  logTime('Publish Npm', 'start');
  await run('npm publish');
  logTime('Publish Npm', 'end');
};

const main = async () => {
  try {
    const nextVersion = await promptNextVersion();
    const startTime = Date.now();

    await test();
    await updatePkgVersion(nextVersion);
    await genChangelog();
    await push(nextVersion);
    await build();
    await publish();
    await tag(nextVersion);

    console.log(chalk.green(`Publish Success, Cost ${((Date.now() - startTime) / 1000).toFixed(3)}s`));
  } catch (err) {
    console.log(chalk.red(`Publish Fail: ${err}`));
  }
}

main();

package.json

代码语言:javascript
复制
"scripts": {
  "publish": "node build/release.js"
}

代码也比较简单,都是对一些工具的基本使用,通过执行 ​yarn publish​ 就可以一键发版。

结尾

本文是我在搭建组件库过程中的学习总结,在过程中学习到了很多知识,并且搭建了清晰的知识体系,希望能够对你有所帮助,欢迎在评论区交流~

参考文档

Tree-Shaking性能优化实践 - 原理篇

彻底搞懂 ESLint 和 Prettier

集成配置 @umijs/fabric

TypeScript and React: Components

TypeScript ESLint

由 allowSyntheticDefaultImports 引起的思考

tsconfig.json入门指南

React 单元测试策略及落地

The Complete Beginner's Guide to Testing React Apps

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/02/02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 概览
    • 初始化
      • 整体目录
        • 配置 ESLint + StyleLint + Prettier
          • 配置 Husky + Lint-Staged
            • 配置 Typescript
            • 组件开发
            • 组件测试
            • 组件库打包
              • 导出 umd
                • 导出 cjs 和 esm
                • 组件文档
                • 线上文档站点部署
                • 一键发版
                • 结尾
                • 参考文档
                相关产品与服务
                内容分发网络 CDN
                内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档