• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

可能是的React组件库搭建

武飞扬头像
Michael18811380328
帮助1

可能是最详细的React组件库搭建总结

概览

本文包含以下内容:

  • prepare: 组件库前期开发准备工作。eslint/commit lint/typescript等等;
  • dev: 使用docz进行开发调试以及文档编写;
  • build:umd/cjs/esm、types、polyfill 以及按需加载;
  • test: 组件测试;
  • release: 组件库发布流程;
  • deploy: 使用now部署文档站点,待补充;
  • other: 使用plop.js快速创建组件模板。

准备工作

初始化项目

新建一个happy-ui文件夹,并初始化。

  1.  
    mkdir happy-uicd happy-ui
  2.  
     
  3.  
    npm init --y
  4.  
     
  5.  
    mkdir components && cd components && touch index.ts # 新建源码文件夹以及入口文件

代码规范

此处直接使用@umijs/fabric的配置。

  1.  
    yarn add @umijs/fabric --dev
  2.  
     
  3.  
    yarn add prettier --dev# 因为@umijs/fabric没有将prettier作为依赖 所以我们需要手动安装

.eslintrc.js

  1.  
    module.exports = {
  2.  
    extends: [require.resolve('@umijs/fabric/dist/eslint')],
  3.  
    };

.prettierrc.js

  1.  
    const fabric = require('@umijs/fabric');
  2.  
     
  3.  
    module.exports = {
  4.  
    ...fabric.prettier,
  5.  
    };

.stylelintrc.js

  1.  
    module.exports = {
  2.  
    extends: [require.resolve('@umijs/fabric/dist/stylelint')],
  3.  
    };

想自行配置的同学可以参考以下文章:

  • Linting Your React Typescript Project with ESLint and Prettier!
  • 使用 ESLint Prettier 规范 React Typescript 项目

Commit Lint

进行pre-commit代码规范检测。

yarn add husky lint-staged --dev

package.json

  1.  
    "lint-staged": {
  2.  
    "components/**/*.ts?(x)": [
  3.  
    "prettier --write",
  4.  
    "eslint --fix",
  5.  
    "git add"
  6.  
    ],
  7.  
    "components/**/*.less": [
  8.  
    "stylelint --syntax less --fix",
  9.  
    "git add"
  10.  
    ]
  11.  
    },
  12.  
    "husky": {
  13.  
    "hooks": {
  14.  
    "pre-commit": "lint-staged"
  15.  
    }
  16.  
    }
学新通

进行 Commit Message 检测。

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

新增.commitlintrc.js写入以下内容

module.exports = { extends: ['@commitlint/config-conventional'] };

package.json 写入以下内容:

  1.  
    // ...
  2.  
    "scripts": {
  3.  
    "commit": "git-cz",
  4.  
    }
  5.  
    // ...
  6.  
    "husky": {
  7.  
    "hooks": {
  8.  
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
  9.  
    "pre-commit": "lint-staged"
  10.  
    }
  11.  
    },
  12.  
    "config": {
  13.  
    "commitizen": {
  14.  
    "path": "cz-conventional-changelog"
  15.  
    }
  16.  
    }
学新通

后续使用 yarn commit 替代 git commit生成规范的 Commit Message,当然为了效率你可以选择手写,但是要符合规范。

TypeScript

yarn add typescript --dev

新建tsconfig.json并写入以下内容

  1.  
    {
  2.  
    "compilerOptions": {
  3.  
    "baseUrl": "./",
  4.  
    "target": "esnext",
  5.  
    "module": "commonjs",
  6.  
    "jsx": "react",
  7.  
    "declaration": true,
  8.  
    "declarationDir": "lib",
  9.  
    "strict": true,
  10.  
    "moduleResolution": "node",
  11.  
    "allowSyntheticDefaultImports": true,
  12.  
    "esModuleInterop": true,
  13.  
    "resolveJsonModule": true
  14.  
    },
  15.  
    "include": ["components", "global.d.ts"],
  16.  
    "exclude": ["node_modules"]
  17.  
    }
学新通

测试

components文件夹下新建alert文件夹,目录结构如下:

  1.  
    alert
  2.  
    ├── alert.tsx # 源文件
  3.  
    ├── index.ts # 入口文件
  4.  
    ├── interface.ts # 类型声明文件
  5.  
    └── style
  6.  
    ├── index.less # 样式文件
  7.  
    └── index.ts # 样式文件里为什么存在一个index.ts - 按需加载样式 管理样式依赖 后面章节会提到

安装React相关依赖:

  1.  
    yarn add react react-dom @types/react @types/react-dom --dev# 开发时依赖,宿主环境一定存在
  2.  
     
  3.  
    yarn add prop-types
  4.  
    # 运行时依赖,宿主环境可能不存在 安装本组件库时一起安装

此处依旧安装了prop-types这个库,因为无法保证宿主环境也使用typescript,从而能够进行静态检查,故使用prop-types保证javascript用户也能得到友好的运行时报错信息。

components/alert/interface.ts

  1.  
    export type Kind = 'info' | 'positive' | 'negative' | 'warning';
  2.  
    export type KindMap = Record<Kind, string>;
  3.  
     
  4.  
    export interface AlertProps {
  5.  
    /**
  6.  
    * Set this to change alert kind
  7.  
    * @default info
  8.  
    */
  9.  
    kind?: 'info' | 'positive' | 'negative' | 'warning';
  10.  
    }

components/alert/alter.tsx

  1.  
    import React from 'react';
  2.  
    import t from 'prop-types';
  3.  
     
  4.  
    import { AlertProps, KindMap } from './interface';
  5.  
     
  6.  
    const prefixCls = 'happy-alert';
  7.  
     
  8.  
    const kinds: KindMap = {
  9.  
    info: '#5352ED',
  10.  
    positive: '#2ED573',
  11.  
    negative: '#FF4757',
  12.  
    warning: '#FFA502',
  13.  
    };
  14.  
     
  15.  
    const Alert: React.FC<AlertProps> = ({ children, kind = 'info', ...rest }) => (
  16.  
    <div
  17.  
    className={prefixCls}
  18.  
    style={{
  19.  
    background: kinds[kind],
  20.  
    }}
  21.  
    {...rest}
  22.  
    >
  23.  
    {children}
  24.  
    </div>
  25.  
    );
  26.  
     
  27.  
    Alert.propTypes = {
  28.  
    kind: t.oneOf(['info', 'positive', 'negative', 'warning']),
  29.  
    };
  30.  
     
  31.  
    export default Alert;
学新通

components/alert/index.ts

  1.  
    import Alert from './alert';
  2.  
     
  3.  
    export default Alert;
  4.  
     
  5.  
    export * from './interface';

components/alert/style/index.less

  1.  
    @popupPrefix: happy-alert;
  2.  
     
  3.  
    .@{popupPrefix} {
  4.  
    padding: 20px;
  5.  
    background: white;
  6.  
    border-radius: 3px;
  7.  
    color: white;
  8.  
    }

components/alert/style/index.ts

import './index.less';

components/index.ts

export { default as Alert } from './alert';

此处组件参考的docz项目typescript以及less示例。

git 一把梭,可以看到控制台已经进行钩子检测了。

  1.  
    git add .
  2.  
     
  3.  
    yarn commit# 或 git commit -m'feat: chapter-1 准备工作'
  4.  
     
  5.  
    git push

准备工作完成。代码可以在仓库的chapter-1分支获取,若存在与本文内容不符的地方,以master分支以及文章为准。

开发与调试

本节解决开发组件时的预览以及调试问题,顺路解决文档编写。

此处选择docz来辅助预览调试。

docz基于MDX(Markdown JSX),可以在 Markdown 中引入 React 组件,使得一边编写文档,一边预览调试成为了可能。而且得益于 React 组件生态,我们可以像编写应用一般编写文档,不仅仅是枯燥的文字。docz 也内置了一些组件,比如<Playground>

安装 docz 以及自定义配置

  1.  
    yarn add docz --dev
  2.  
     
  3.  
    yarn add rimraf --dev # 清空目录的一个辅助库

增加 npm scriptspackage.json

  1.  
    "scripts": {
  2.  
    "dev": "docz dev", // 启动本地开发环境
  3.  
    "start": "npm run dev", // dev命令别名
  4.  
    "build:doc": "rimraf doc-site && docz build", // 后续会配置打包出来的文件目录名为doc-site,故每次build前删除
  5.  
    "preview:doc": "docz serve" // 预览文档站点
  6.  
    },

注意:本节所有操作都是针对站点应用。打包指代文档站点打包,而非组件库。

新建doczrc.js配置文件,并写入以下内容:

doczrc.js

  1.  
    export default {
  2.  
    files: './components/**/*.{md,markdown,mdx}', // 识别的文件后缀
  3.  
    dest: 'doc-site', // 打包出来的文件目录名
  4.  
    title: 'happy-ui', // 站点标题
  5.  
    typescript: true, // 组件源文件是通过typescript开发,需要打开此选项
  6.  
    };

由于使用了less作为样式预处理器,故需要安装 less 插件。

yarn add less gatsby-plugin-less --dev

新建gatsby-config.js,并写入以下内容:

gatsby-config.js

  1.  
    module.exports = {
  2.  
    plugins: ['gatsby-theme-docz', 'gatsby-plugin-less'],
  3.  
    };

编写文档

新建components/alert/index.mdx,并写入以下内容:

  1.  
    ---
  2.  
    name: Alert 警告提示
  3.  
    route: /Alert
  4.  
    menu: 组件
  5.  
    ---
  6.  
     
  7.  
    import { Playground } from 'docz'; import Alert from './alert'; // 引入组件 import './style'; // 引入组件样式
  8.  
     
  9.  
    # Alert 警告提示
  10.  
     
  11.  
    警告提示,展现需要关注的信息。
  12.  
     
  13.  
    ## 代码演示
  14.  
     
  15.  
    ### 基本用法
  16.  
     
  17.  
    <Playground>
  18.  
    <Alert kind="warning">这是一条警告提示</Alert>
  19.  
    </Playground>
  20.  
     
  21.  
    ## API
  22.  
     
  23.  
    | 属性 | 说明 | 类型 | 默认值 |
  24.  
    | ---- | -------- | -------------------------------------------- | ------ |
  25.  
    | kind | 警告类型 | 'info'/'positive'/'negative'/'warning'非必填 | 'info' |
学新通

执行脚本命令:

yarn start # or yarn dev

可以在localhost:3000看到如下页面 :

学新通文档站点

现在可以在index.mdx中愉快地进行文档编写和调试了!

倘若本文到了这里就结束(其实也可以结束了(_^▽^_)),那我只是官方文档的翻译复读机罢了,有兴趣的同学可以继续向下看。

优化文档编写

如果代码演示部分的demo较多(比如基本用法、高级用法以及各种用法等等),在组件复杂的情况下(毕竟<Alert/>着实太简单了),会导致文档很长难以维护,你到底是在写文档呢还是在写代码呢?

那就抽离吧。

components/alert/文件夹下新建demo文件夹,存放我们在编写文档时需要引用的 demo

components/alert/demo/1-demo-basic.tsx

  1.  
    import React from 'react';
  2.  
    import Alert from '../alert';
  3.  
    import '../style';
  4.  
     
  5.  
    export default () => <Alert kind="warning"></Alert>;

components/alert/index.mdx

  1.  
    - import Alert from './alert'; // 引入组件
  2.  
    - import './style'; // 引入组件样式
  3.  
    import BasicDemo from './demo/1-demo-basic';
  4.  
     
  5.  
    ...
  6.  
     
  7.  
    <Playground>
  8.  
    - <Alert kind="warning">这是一条警告提示</Alert>
  9.  
    <BasicDemo />
  10.  
    </Playground>

这样我们就将 demo 与文档进行了分隔。预览如下:

学新通文档重构

等等,下面显示的是<BasicDemo />,而非demo源码。

<Playground />组件暂时无法支持上述形式的展示:自定义下方展示的代码,而非<Playground />内部的代码。相关讨论如下:

  • Allow to hide the LiveError overlay #907
  • Allow to override the playground's editor's code #906

其实第一条 PR 已经解决了问题,但是被关闭了,无奈。

不过既然都能引入 React 组件了,在MDX的环境下自定义一个Playground组件又有何难呢,无非就是渲染组件(MDX 自带)和展示源码,简单开放的东西大家都是喜闻乐见的,就叫HappyBox吧。

优化代码展示

编写 ``组件

安装依赖:

yarn add react-use react-tooltip react-feather react-simple-code-editor prismjs react-copy-to-clipboard raw-loader styled-components --dev
  • react-use - 2020 年了,当然要用hooks
  • react-simple-code-editor - 代码展示区域
  • prismjs - 代码高亮
  • raw-loader - 将源码转成字符串
  • react-copy-to-clipboard - 让用户爸爸们能够 copy demo 代码
  • react-tooltip/react-feather 辅助组件
  • styled-components 方便在文档示例中让用户看到样式,也用作文档组件的样式处理

这些依赖都是服务于文档站点应用,和组件库自身毫无关联。

最终效果如下:

学新通最终效果

根目录下新建doc-comps文件夹,存放文档中使用的一些工具组件,比如<HappyBox />

doc-comps

  1.  
    ├── happy-box
  2.  
    │ ├── style.ts
  3.  
    │ └── index.tsx
  4.  
    └── index.ts

components/doc-comps/happy-box/index.tsx

  1.  
    import React from 'react';
  2.  
    import Editor from 'react-simple-code-editor';
  3.  
    import CopyToClipboard from 'react-copy-to-clipboard';
  4.  
    import { useToggle } from 'react-use';
  5.  
    import ReactTooltip from 'react-tooltip';
  6.  
    import IconCopy from 'react-feather/dist/icons/clipboard';
  7.  
    import IconCode from 'react-feather/dist/icons/code';
  8.  
    import { highlight, languages } from 'prismjs/components/prism-core';
  9.  
    import { StyledContainer, StyledIconWrapper } from './style';
  10.  
     
  11.  
    import 'prismjs/components/prism-clike';
  12.  
    import 'prismjs/components/prism-javascript';
  13.  
    import 'prismjs/components/prism-markup';
  14.  
     
  15.  
    require('prismjs/components/prism-jsx');
  16.  
     
  17.  
    interface Props {
  18.  
    code: string;
  19.  
    title?: React.ReactNode;
  20.  
    desc?: React.ReactNode;
  21.  
    }
  22.  
     
  23.  
    export const HappyBox: React.FC<Props> = ({ code, title, desc, children }) => {
  24.  
    const [isEditVisible, toggleEditVisible] = useToggle(false);
  25.  
     
  26.  
    return (
  27.  
    <StyledContainer>
  28.  
    <section className="code-box-demo"> {children}</section>
  29.  
    <section className="code-box-meta">
  30.  
    <div className="text-divider">
  31.  
    <span>{title || '示例'}</span>
  32.  
    </div>
  33.  
    <div className="code-box-description">
  34.  
    <p>{desc || '暂无描述'}</p>
  35.  
    </div>
  36.  
    <div className="divider" />
  37.  
    <div className="code-box-action">
  38.  
    <CopyToClipboard text={code} onCopy={() => alert('复制成功')}>
  39.  
    <IconCopy data-place="top" data-tip="复制代码" />
  40.  
    </CopyToClipboard>
  41.  
     
  42.  
    <StyledIconWrapper onClick={toggleEditVisible}>
  43.  
    <IconCode data-place="top" data-tip={isEditVisible ? '收起代码' : '显示代码'} />
  44.  
    </StyledIconWrapper>
  45.  
    </div>
  46.  
    </section>
  47.  
    {renderEditor()}
  48.  
    <ReactTooltip />
  49.  
    </StyledContainer>
  50.  
    );
  51.  
     
  52.  
    function renderEditor() {
  53.  
    if (!isEditVisible) return null;
  54.  
    return (
  55.  
    <div className="container_editor_area">
  56.  
    <Editor
  57.  
    readOnly
  58.  
    value={code}
  59.  
    onValueChange={() => {}}
  60.  
    highlight={code => highlight(code, languages.jsx)}
  61.  
    padding={10}
  62.  
    className="container__editor"
  63.  
    style={{
  64.  
    fontFamily: '"Fira code", "Fira Mono", monospace',
  65.  
    fontSize: 14,
  66.  
    }}
  67.  
    />
  68.  
    </div>
  69.  
    );
  70.  
    }
  71.  
    };
  72.  
     
  73.  
    export default HappyBox;
学新通

相关配置变更

  • 增加 alias别名,样例源码展示相对路径不够友好,让用户直接拷贝才够省心

新建gatsby-node.js,写入以下内容以开启alias

  1.  
    const path = require('path');
  2.  
     
  3.  
    exports.onCreateWebpackConfig = args => {
  4.  
    args.actions.setWebpackConfig({
  5.  
    resolve: {
  6.  
    modules: [path.resolve(__dirname, '../src'), 'node_modules'],
  7.  
    alias: {
  8.  
    'happy-ui/lib': path.resolve(__dirname, '../components/'),
  9.  
    'happy-ui/esm': path.resolve(__dirname, '../components/'),
  10.  
    'happy-ui': path.resolve(__dirname, '../components/'),
  11.  
    },
  12.  
    },
  13.  
    });
  14.  
    };

tsconfig.json 打包时需要忽略demo,避免组件库打包生成types时包含其中,同时增加paths属性用于 vscode 自动提示:

tsconfig.json

  1.  
    {
  2.  
    "compilerOptions": {
  3.  
    "baseUrl": "./",
  4.  
    "paths": {
  5.  
    "happy-ui": ["components/index.ts"],
  6.  
    "happy-ui/esm/*": ["components/*"],
  7.  
    "happy-ui/lib/*": ["components/*"]
  8.  
    },
  9.  
    "target": "esnext",
  10.  
    "module": "commonjs",
  11.  
    "jsx": "react",
  12.  
    "declaration": true,
  13.  
    "declarationDir": "lib",
  14.  
    "strict": true,
  15.  
    "moduleResolution": "node",
  16.  
    "allowSyntheticDefaultImports": true,
  17.  
    "esModuleInterop": true,
  18.  
    "resolveJsonModule": true
  19.  
    },
  20.  
    "include": ["components", "global.d.ts"],
  21.  
    - "exclude": ["node_modules"]
  22.  
    "exclude": ["node_modules", "**/demo/**"]
  23.  
    }
学新通

新的问题出现了,vscode 的 alias 提示依赖 tsconfig.json,忽略 demo 文件夹后,demo 内的文件模块类型找不到声明(paths 失效),所以不能将 demo 在 tsconfig.json 中移除:

  1.  
    {
  2.  
    - "exclude": ["node_modules", "**/demo/**"]
  3.  
    "exclude": ["node_modules"]
  4.  
    }

新建一个 tsconfig.build.json 文件:

tsconfig.build.json

  1.  
    {
  2.  
    "extends": "./tsconfig.json",
  3.  
    "exclude": ["**/demo/**", "node_modules"]
  4.  
    }

后续使用 tsc 生成类型声明文件指定tsconfig.build.json即可。

改造相关文件

components/alert/demo/1-demo-basic.tsx

  1.  
    - import Alert from '../alert';
  2.  
    import Alert from 'happy-ui/lib/alert';
  3.  
     
  4.  
    - import '../style';
  5.  
    import 'happy-ui/lib/alert/style';

components/alert/index.mdx

  1.  
    - import { Playground } from 'docz';
  2.  
    import { HappyBox } from '../../doc-comps';
  3.  
     
  4.  
    import BasicDemoCode from '!raw-loader!./demo/1-demo-basic.tsx';
  5.  
     
  6.  
    ...
  7.  
     
  8.  
    - <Playground>
  9.  
    - <BasicDemo />
  10.  
    - </Playground>
  11.  
     
  12.  
    <HappyBox code={BasicDemoCode} title="基本用法" desc="使用kind控制Alert类型">
  13.  
    <BasicDemo />
  14.  
    </HappyBox>

yarn start卡住时尝试删除根目录.docz文件夹,而后重新执行命令。

现在可以愉快地开发组件了。代码可以在仓库的chapter-2分支获取,若存在与本文内容不符的地方,以master分支以及文章为准。

组件库打包

宿主环境各不相同,需要将源码进行相关处理后发布至 npm。

明确以下目标:

  1. 导出类型声明文件
  2. 导出 umd/Commonjs module/ES module 等 3 种形式供使用者引入
  3. 支持样式文件 css 引入,而非只有less
  4. 支持按需加载

导出类型声明文件

既然是使用typescript编写的组件库,那么使用者应当享受到类型系统的好处。

我们可以生成类型声明文件,并在package.json中定义入口,如下:

package.json

  1.  
    {
  2.  
    "typings": "lib/index.d.ts", // 定义类型入口文件
  3.  
    "scripts": {
  4.  
    "build:types": "tsc -p tsconfig.build.json && cpr lib esm" // 执行tsc命令生成类型声明文件
  5.  
    }
  6.  
    }

值得注意的是:此处使用cprlib的声明文件拷贝了一份,重命名为esm。用于后面存放 ES module 形式的组件。这样做主要是为了用户手动按需引入组件时依旧可以有自动提示。

最开始使用的将声明文件单独打包成一个types文件夹,这样只通过'happy-ui'引入才可以有自动提示,但是'happy-ui/esm/xxx'和'happy-ui/lib/xxx'就无法提示。

tsconfig.build.json

  1.  
    {
  2.  
    "extends": "./tsconfig.json",
  3.  
    "compilerOptions": { "emitDeclarationOnly": true }, // 只生成声明文件
  4.  
    "exclude": ["**/__tests__/**", "**/demo/**", "node_modules", "lib", "esm"] // 排除示例、测试以及打包好的文件夹
  5.  
    }

执行yarn build:types,可以发现根目录下已经生成了lib文件夹(tsconfig.json中定义的declarationDir字段),目录结构与components文件夹保持一致,如下:

types

  1.  
    ├── alert
  2.  
    │ ├── alert.d.ts
  3.  
    │ ├── index.d.ts
  4.  
    │ ├── interface.d.ts
  5.  
    │ └── style
  6.  
    │ └── index.d.ts
  7.  
    └── index.d.ts

这样使用者引入npm 包时,便能得到自动提示,也能够复用相关组件的类型定义。

接下来将ts(x)等文件处理成js文件。

需要注意的是,我们需要输出Commonjs module以及ES module两种模块类型的文件(暂不考虑umd),以下使用cjs指代Commonjs moduleesm指代ES module。 对此有疑问的同学推荐阅读:import、require、export、module.exports 混合详解

导出 Commonjs 模块

其实完全可以使用babeltsc命令行工具进行代码编译处理(实际上很多工具库就是这样做的),但考虑到还要**处理样式及其按需加载**,我们借助 gulp 来串起这个流程。

babel 配置

首先安装babel及其相关依赖

  1.  
    yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime --dev
  2.  
    yarn add @babel/runtime-corejs3

新建.babelrc.js文件,写入以下内容:

.babelrc.js

  1.  
    module.exports = {
  2.  
    presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  3.  
    plugins: [
  4.  
    '@babel/proposal-class-properties',
  5.  
    [
  6.  
    '@babel/plugin-transform-runtime',
  7.  
    {
  8.  
    corejs: 3,
  9.  
    helpers: true,
  10.  
    },
  11.  
    ],
  12.  
    ],
  13.  
    };

关于@babel/plugin-transform-runtime@babel/runtime-corejs3

  • helpers选项设置为true,可抽离代码编译过程重复生成的 helper 函数(classCallCheck,extends等),减小生成的代码体积;
  • corejs设置为3,可引入不污染全局的按需polyfill,常用于类库编写(我更推荐:不引入polyfill,转而告知使用者需要引入何种polyfill,避免重复引入或产生冲突,后面会详细提到)。

更多参见官方文档-@babel/plugin-transform-runtime

配置目标环境

为了避免转译浏览器原生支持的语法,新建.browserslistrc文件,根据适配需求,写入支持浏览器范围,作用于@babel/preset-env

.browserslistrc

  1.  
    >0.2%
  2.  
    not dead
  3.  
    not op_mini all

很遗憾的是,@babel/runtime-corejs3无法在按需引入的基础上根据目标浏览器支持程度再次减少polyfill的引入,参见@babel/runtime for target environment 。

这意味着@babel/runtime-corejs3 甚至会在针对现代引擎的情况下注入所有可能的 polyfill:不必要地增加了最终捆绑包的大小。

对于组件库(代码量可能很大),个人建议将polyfill的选择权交还给使用者,在宿主环境进行polyfill。若使用者具有兼容性要求,自然会使用@babel/preset-env core-js .browserslistrc进行全局polyfill,这套组合拳引入了最低目标浏览器不支持API的全部 polyfill

业务开发中,将@babel/preset-envuseBuiltIns选项值设置为 usage,同时把node_modulesbabel-loaderexclude掉的同学可能想要这个特性:"useBuiltIns: usage" for node_modules without transpiling #9419,在未支持该issue提到的内容之前,还是乖乖地将useBuiltIns设置为entry,或者不要把node_modulesbabel-loaderexclude

所以组件库不用画蛇添足,引入多余的polyfill,写好文档说明,比什么都重要(就像zent和antd这样)。

现在@babel/runtime-corejs3更换为@babel/runtime,只进行helper函数抽离。

  1.  
    yarn remove @babel/runtime-corejs3
  2.  
     
  3.  
    yarn add @babel/runtime

.babelrc.js

  1.  
    module.exports = {
  2.  
    presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  3.  
    plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],
  4.  
    };

@babel/transform-runtimehelper选项默认为true

gulp 配置

再来安装gulp相关依赖

yarn add gulp gulp-babel --dev

新建gulpfile.js,写入以下内容:

gulpfile.js

  1.  
    const gulp = require('gulp');
  2.  
    const babel = require('gulp-babel');
  3.  
     
  4.  
    const paths = {
  5.  
    dest: {
  6.  
    lib: 'lib', // commonjs 文件存放的目录名 - 本块关注
  7.  
    esm: 'esm', // ES module 文件存放的目录名 - 暂时不关心
  8.  
    dist: 'dist', // umd文件存放的目录名 - 暂时不关心
  9.  
    },
  10.  
    styles: 'components/**/*.less', // 样式文件路径 - 暂时不关心
  11.  
    scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'], // 脚本文件路径
  12.  
    };
  13.  
     
  14.  
    function compileCJS() {
  15.  
    const { dest, scripts } = paths;
  16.  
    return gulp
  17.  
    .src(scripts)
  18.  
    .pipe(babel()) // 使用gulp-babel处理
  19.  
    .pipe(gulp.dest(dest.lib));
  20.  
    }
  21.  
     
  22.  
    // 并行任务 后续加入样式处理 可以并行处理
  23.  
    const build = gulp.parallel(compileCJS);
  24.  
     
  25.  
    exports.build = build;
  26.  
     
  27.  
    exports.default = build;
学新通

修改package.json

package.json

  1.  
    {
  2.  
    - "main": "index.js",
  3.  
    "main": "lib/index.js",
  4.  
    "scripts": {
  5.  
    ...
  6.  
    "clean": "rimraf lib esm dist",
  7.  
    "build": "npm run clean && npm run build:types && gulp",
  8.  
    ...
  9.  
    },
  10.  
    }

执行yarn build,得到如下内容:

lib

  1.  
    ├── alert
  2.  
    │ ├── alert.js
  3.  
    │ ├── index.js
  4.  
    │ ├── interface.js
  5.  
    │ └── style
  6.  
    │ └── index.js
  7.  
    └── index.js

观察编译后的源码,可以发现:诸多helper方法已被抽离至@babel/runtime中,模块导入导出形式也是commonjs规范。

lib/alert/alert.js

学新通lib/alert/alert.js

导出 ES module

生成ES module可以更好地进行tree shaking,基于上一步的babel配置,更新以下内容:

  1. 配置@babel/preset-envmodules选项为false,关闭模块转换;
  2. 配置@babel/plugin-transform-runtimeuseESModules选项为true,使用ES module形式引入helper函数。

.babelrc.js

  1.  
    module.exports = {
  2.  
    presets: [
  3.  
    [
  4.  
    '@babel/env',
  5.  
    {
  6.  
    modules: false, // 关闭模块转换
  7.  
    },
  8.  
    ],
  9.  
    '@babel/typescript',
  10.  
    '@babel/react',
  11.  
    ],
  12.  
    plugins: [
  13.  
    '@babel/proposal-class-properties',
  14.  
    [
  15.  
    '@babel/plugin-transform-runtime',
  16.  
    {
  17.  
    useESModules: true, // 使用esm形式的helper
  18.  
    },
  19.  
    ],
  20.  
    ],
  21.  
    };
学新通

目标达成,我们再使用环境变量区分esmcjs(执行任务时设置对应的环境变量即可),最终babel配置如下:

.babelrc.js

  1.  
    module.exports = {
  2.  
    presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  3.  
    plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],
  4.  
    env: {
  5.  
    esm: {
  6.  
    presets: [
  7.  
    [
  8.  
    '@babel/env',
  9.  
    {
  10.  
    modules: false,
  11.  
    },
  12.  
    ],
  13.  
    ],
  14.  
    plugins: [
  15.  
    [
  16.  
    '@babel/plugin-transform-runtime',
  17.  
    {
  18.  
    useESModules: true,
  19.  
    },
  20.  
    ],
  21.  
    ],
  22.  
    },
  23.  
    },
  24.  
    };
学新通

接下来修改gulp相关配置,抽离compileScripts任务,增加compileESM任务。

gulpfile.js

  1.  
    // ...
  2.  
     
  3.  
    /**
  4.  
    * 编译脚本文件
  5.  
    * @param {string} babelEnv babel环境变量
  6.  
    * @param {string} destDir 目标目录
  7.  
    */
  8.  
    function compileScripts(babelEnv, destDir) {
  9.  
    const { scripts } = paths;
  10.  
    // 设置环境变量
  11.  
    process.env.BABEL_ENV = babelEnv;
  12.  
    return gulp
  13.  
    .src(scripts)
  14.  
    .pipe(babel()) // 使用gulp-babel处理
  15.  
    .pipe(gulp.dest(destDir));
  16.  
    }
  17.  
     
  18.  
    /**
  19.  
    * 编译cjs
  20.  
    */
  21.  
    function compileCJS() {
  22.  
    const { dest } = paths;
  23.  
    return compileScripts('cjs', dest.lib);
  24.  
    }
  25.  
     
  26.  
    /**
  27.  
    * 编译esm
  28.  
    */
  29.  
    function compileESM() {
  30.  
    const { dest } = paths;
  31.  
    return compileScripts('esm', dest.esm);
  32.  
    }
  33.  
     
  34.  
    // 串行执行编译脚本任务(cjs,esm) 避免环境变量影响
  35.  
    const buildScripts = gulp.series(compileCJS, compileESM);
  36.  
     
  37.  
    // 整体并行执行任务
  38.  
    const build = gulp.parallel(buildScripts);
  39.  
     
  40.  
    // ...
学新通

执行yarn build,可以发现生成了lib/esm三个文件夹,观察esm目录,结构同lib一致,js 文件都是以ES module模块形式导入导出。

esm/alert/alert.js

学新通esm/alert/alert.js

别忘了给package.json增加相关入口。

package.json

  1.  
    {
  2.  
    "module": "esm/index.js"
  3.  
    }

处理样式文件

拷贝 less 文件

我们会将less文件包含在npm包中,用户可以通过happy-ui/lib/alert/style/index.js的形式按需引入less文件,此处可以直接将 less 文件拷贝至目标文件夹。

gulpfile.js中新建copyLess任务。

gulpfile.js

  1.  
    // ...
  2.  
     
  3.  
    /**
  4.  
    * 拷贝less文件
  5.  
    */
  6.  
    function copyLess() {
  7.  
    return gulp
  8.  
    .src(paths.styles)
  9.  
    .pipe(gulp.dest(paths.dest.lib))
  10.  
    .pipe(gulp.dest(paths.dest.esm));
  11.  
    }
  12.  
     
  13.  
    const build = gulp.parallel(buildScripts, copyLess);
  14.  
     
  15.  
    // ...
学新通

观察lib目录,可以发现 less 文件已被拷贝至alert/style目录下。

lib

  1.  
    ├── alert
  2.  
    │ ├── alert.js
  3.  
    │ ├── index.js
  4.  
    │ ├── interface.js
  5.  
    │ └── style
  6.  
    │ ├── index.js
  7.  
    │ └── index.less # less文件
  8.  
    └── index.js

可能有些同学已经发现问题:若使用者没有使用less预处理器,使用的是sass方案甚至原生css方案,那现有方案就搞不定了。经分析,有以下 3 种预选方案:

  1. 告知用户增加less-loader
  2. 打包出一份完整的 css 文件,进行**全量**引入;
  3. 单独提供一份style/css.js文件,引入的是组件 css样式文件依赖,而非 less 依赖,组件库底层抹平差异;
  4. 使用css in js方案。

方案 1 会导致业务方使用成本增加。

方案 2 无法进行按需引入。

方案 4 需要详细聊聊。

css in js除了赋予样式编写更多的可能性之外,在编写第三方组件库时更是利器。

如果我们写一个react-use这种hooks工具库,不涉及到样式,只需要在package.json中设置sideEffectsfalse,业务方使用 webpack 进行打包时,只会打包被使用到的 hooks(优先使用 ES module)。

入口文件index.js中导出的但未被使用的其他 hooks 会被tree shaking,第一次使用这个库的时候我很好奇,为什么没有按需引入的使用方式,结果打包分析时我傻了,原来人家天生支持按需引入。

可能常用的antd以及lodash都要配一配,导致产生了惯性思维。

回到正题。如果将样式使用javascript来编写,在某种维度上讲,组件库和工具库一致了,配好sideEffects,自动按需引入,美滋滋。

而且每个组件都与自己的样式绑定,不需要业务方或组件开发者去**维护样式依赖**,什么是样式依赖,后面会讲到。

缺点:

  1. 样式无法单独缓存;
  2. styled-components 自身体积较大;
  3. 复写组件样式需要使用属性选择器或者使用styled-components,麻烦了点。

需要看取舍了,偷偷说一句styled-components做主题定制也极其方便。

方案 3 是antd使用的这种方案。

在搭建组件库的过程中,有一个问题困扰了我很久:为什么需要alert/style/index.js引入less文件或alert/style/css.js引入css文件?

答案是**管理样式依赖**。

因为我们的组件是没有引入样式文件的,需要用户去手动引入。

假设存在以下场景:引入<Button /><Button />依赖了<Icon />,使用者需要手动去引入调用的组件的样式(<Button />)及其依赖的组件样式(<Icon />),遇到复杂组件极其麻烦,所以组件库开发者可以提供一份这样的js文件,使用者手动引入这个js文件,就能引入对应组件及其依赖组件的样式。

那么问题又来了,为什么组件不能自己去import './index.less'呢?

可以,不过业务方要配置less-loader,什么,业务方不想配,要你import './index.css'?🙃

可以,业务方爽了,组件开发方不爽。

所以我们要找一个大家都爽的方案:

  1. 开发方能够开心的使用预处理器;
  2. 业务方不需要额外的使用成本。

答案就是*css in js*单独提供一份style/css.js文件,引入的是组件 css样式文件依赖,而非 less 依赖,组件库底层抹平差异。

之前了解到father可以在打包的时候将index.less转成index.css,这倒是个好法子,但是一些重复引入的样式模块(比如动画样式),会被重复打包,不知道有没有好的解决方案。

生成 css 文件

安装相关依赖。

yarn add gulp-less gulp-autoprefixer gulp-cssnano --dev

less文件生成对应的css文件,在gulpfile.js中增加less2css任务。

  1.  
    // ...
  2.  
     
  3.  
    /**
  4.  
    * 生成css文件
  5.  
    */
  6.  
    function less2css() {
  7.  
    return gulp
  8.  
    .src(paths.styles)
  9.  
    .pipe(less()) // 处理less文件
  10.  
    .pipe(autoprefixer()) // 根据browserslistrc增加前缀
  11.  
    .pipe(cssnano({ zindex: false, reduceIdents: false })) // 压缩
  12.  
    .pipe(gulp.dest(paths.dest.lib))
  13.  
    .pipe(gulp.dest(paths.dest.esm));
  14.  
    }
  15.  
     
  16.  
    const build = gulp.parallel(buildScripts, copyLess, less2css);
  17.  
     
  18.  
    // ...
学新通

执行yarn build,组件style目录下已经存在css文件了。

接下来我们需要一个alert/style/css.js来帮用户引入css文件。

生成 css.js

此处参考antd-tools的实现方式:在处理scripts任务中,截住style/index.js,生成style/css.js,并通过正则将引入的less文件后缀改成css

安装相关依赖。

yarn add through2 --dev

gulpfile.js

  1.  
    // ...
  2.  
     
  3.  
    /**
  4.  
    * 编译脚本文件
  5.  
    * @param {*} babelEnv babel环境变量
  6.  
    * @param {*} destDir 目标目录
  7.  
    */
  8.  
    function compileScripts(babelEnv, destDir) {
  9.  
    const { scripts } = paths;
  10.  
    process.env.BABEL_ENV = babelEnv;
  11.  
    return gulp
  12.  
    .src(scripts)
  13.  
    .pipe(babel()) // 使用gulp-babel处理
  14.  
    .pipe(
  15.  
    through2.obj(function z(file, encoding, next) {
  16.  
    this.push(file.clone());
  17.  
    // 找到目标
  18.  
    if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
  19.  
    const content = file.contents.toString(encoding);
  20.  
    file.contents = Buffer.from(cssInjection(content)); // 文件内容处理
  21.  
    file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名
  22.  
    this.push(file); // 新增该文件
  23.  
    next();
  24.  
    } else {
  25.  
    next();
  26.  
    }
  27.  
    }),
  28.  
    )
  29.  
    .pipe(gulp.dest(destDir));
  30.  
    }
  31.  
     
  32.  
    // ...
学新通

cssInjection的实现:

gulpfile.js

  1.  
    /**
  2.  
    * 当前组件样式 import './index.less' => import './index.css'
  3.  
    * 依赖的其他组件样式 import '../test-comp/style' => import '../test-comp/style/css.js'
  4.  
    * 依赖的其他组件样式 import '../test-comp/style/index.js' => import '../test-comp/style/css.js'
  5.  
    * @param {string} content
  6.  
    */
  7.  
    function cssInjection(content) {
  8.  
    return content
  9.  
    .replace(/\/style\/?'/g, "/style/css'")
  10.  
    .replace(/\/style\/?"/g, '/style/css"')
  11.  
    .replace(/\.less/g, '.css');
  12.  
    }

再进行打包,可以看见组件style目录下生成了css.js文件,引入的也是上一步less转换而来的css文件。

lib/alert

  1.  
    ├── alert.js
  2.  
    ├── index.js
  3.  
    ├── interface.js
  4.  
    └── style
  5.  
    ├── css.js # 引入index.css
  6.  
    ├── index.css
  7.  
    ├── index.js
  8.  
    └── index.less

按需加载

在 package.json 中增加sideEffects属性,配合ES module达到tree shaking效果(将样式依赖文件标注为side effects,避免被误删除)。

  1.  
    // ...
  2.  
    "sideEffects": [
  3.  
    "dist/*",
  4.  
    "esm/**/style/*",
  5.  
    "lib/**/style/*",
  6.  
    "*.less"
  7.  
    ],
  8.  
    // ...

使用以下方式引入,可以做到js部分的按需加载,但需要手动引入样式:

  1.  
    import { Alert } from 'happy-ui';
  2.  
    import 'happy-ui/esm/alert/style';

也可以使用以下方式引入:

  1.  
    import Alert from 'happy-ui/esm/alert'; // or import Alert from 'happy-ui/lib/alert';
  2.  
    import 'happy-ui/esm/alert/style'; // or import Alert from 'happy-ui/lib/alert';

以上引入样式文件的方式不太优雅,直接入口处引入**全量**样式文件又和按需加载的本意相去甚远。

使用者可以借助babel-plugin-import来进行辅助,减少代码编写量(说好的不加入其他使用成本的呢~)。

import { Alert } from 'happy-ui';

⬇️

  1.  
    import Alert from 'happy-ui/lib/alert';
  2.  
    import 'happy-ui/lib/alert/style';

生成 umd

没用上,这一块标记为 todo 吧。

本节代码可以在仓库的chapter-3分支获取,若存在与本文内容不符的地方,以master分支以及文章为准。

组件测试

与软件操作行为越接近的测试,越能给予你信心。

本节主要讲述如何在组件库中引入jest以及@testing-library/react,而不会深入单元测试的学习。

如果你对下列问题感兴趣:

  1. What-单元测试是什么?
  2. Why-为什么要写单元测试?
  3. How-编写单元测试的最佳实践?

那么可以看看以下文章:

  • Test React apps with React Testing Library:通过一个`的例子延伸,阐述了选择React Testing Library而非Enzyme`的理由,并对其进行了一些入门教学;
  • React Testing Library:@testing-library/react的官方文档,该库提供的 API 在某个程度上就是在指引开发者进行单元测试的最佳实践;
  • React Testing Library-examples:@testing-library/react的一些实例,提供了各种常见场景的测试;
  • React 单元测试策略及落地:如标题所示,值得一看。

相关配置

安装依赖:

yarn add jest ts-jest @testing-library/react @testing-library/jest-dom identity-obj-proxy @types/jest @types/testing-library__react --dev
  • jest: JavaScript 测试框架,专注于简洁明快;
  • ts-jest:为TypeScript编写jest测试用例提供支持;
  • @testing-library/react:简单而完整的React DOM测试工具,鼓励良好的测试实践;
  • @testing-library/jest-dom:自定义的jest匹配器(matchers),用于测试DOM的状态(即为jestexcept方法返回值增加更多专注于DOMmatchers);
  • identity-obj-proxy:一个工具库,此处用来mock样式文件。

新建jest.config.js,并写入相关配置,更多配置可参考jest 官方文档-配置,只看几个常用的就可以。

jest.config.js

  1.  
    module.exports = {
  2.  
    verbose: true,
  3.  
    roots: ['<rootDir>/components'],
  4.  
    moduleNameMapper: {
  5.  
    '\\.(css|less|scss)$': 'identity-obj-proxy',
  6.  
    '^components$': '<rootDir>/components/index.tsx',
  7.  
    '^components(.*)$': '<rootDir>/components/$1',
  8.  
    },
  9.  
    testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$',
  10.  
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  11.  
    testPathIgnorePatterns: ['/node_modules/', '/lib/', '/esm/', '/dist/'],
  12.  
    preset: 'ts-jest',
  13.  
    testEnvironment: 'jsdom',
  14.  
    };

修改package.json,增加测试相关命令,并且代码提交前,跑测试用例,如下:

package.json

  1.  
    "scripts": {
  2.  
    ...
  3.  
    "test": "jest", # 执行jest
  4.  
    "test:watch": "jest --watch", # watch模式下执行
  5.  
    "test:coverage": "jest --coverage", # 生成测试覆盖率报告
  6.  
    "test:update": "jest --updateSnapshot" # 更新快照
  7.  
    },
  8.  
    ...
  9.  
    "lint-staged": {
  10.  
    "components/**/*.ts?(x)": [
  11.  
    "prettier --write",
  12.  
    "eslint --fix",
  13.  
    "jest --bail --findRelatedTests",
  14.  
    "git add"
  15.  
    ],
  16.  
    ...
  17.  
    }
学新通

修改gulpfile.js以及tsconfig.json,避免打包时,把测试文件一并处理了。

gulpfile.js

  1.  
    const paths = {
  2.  
    ...
  3.  
    - scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'],
  4.  
    scripts: [
  5.  
    'components/**/*.{ts,tsx}',
  6.  
    '!components/**/demo/*.{ts,tsx}',
  7.  
    '!components/**/__tests__/*.{ts,tsx}',
  8.  
    ],
  9.  
    };

tsconfig.json

  1.  
    {
  2.  
    - "exclude": ["components/**/demo"]
  3.  
    "exclude": ["components/**/demo", "components/**/__tests__"]
  4.  
    }

编写测试用例

<Alert />比较简单,此处只作示例用,简单进行一下快照测试。

在对应组件的文件夹下新建__tests__文件夹,用于存放测试文件,其内新建index.test.tsx文件,写入以下测试用例:

components/alert/**tests/index.test.tsx**

  1.  
    import React from 'react';
  2.  
    import { render } from '@testing-library/react';
  3.  
    import Alert from '../alert';
  4.  
     
  5.  
    describe('<Alert />', () => {
  6.  
    test('should render default', () => {
  7.  
    const { container } = render(<Alert>default</Alert>);
  8.  
    expect(container).toMatchSnapshot();
  9.  
    });
  10.  
     
  11.  
    test('should render alert with type', () => {
  12.  
    const kinds: any[] = ['info', 'warning', 'positive', 'negative'];
  13.  
     
  14.  
    const { getByText } = render(
  15.  
    <>
  16.  
    {kinds.map(k => (
  17.  
    <Alert kind={k} key={k}>
  18.  
    {k}
  19.  
    </Alert>
  20.  
    ))}
  21.  
    </>,
  22.  
    );
  23.  
     
  24.  
    kinds.forEach(k => {
  25.  
    expect(getByText(k)).toMatchSnapshot();
  26.  
    });
  27.  
    });
  28.  
    });
学新通

更新一下快照:

yarn test:update

可以看见同级目录下新增了一个__snapshots__文件夹,里面存放对应测试用例的快照文件。

学新通生成的快照文件

再执行测试用例:

yarn test

学新通通过测试用例

可以发现我们通过了测试用例。。。额,这里当然能通过,主要是后续我们进行迭代重构时,都会重新执行测试用例,与最近的一次快照进行比对,如果与快照不一致(结构发生了改变),那么相应的测试用例就无法通过。

对于快照测试,褒贬不一,这个例子也着实简单得很,甚至连扩展的 jest-dom提供的 matchers 都没用上。

如何编写优秀的测试用例,我也是一个新手,只能说多看多写多尝试,前面推荐的文章很不错。

本节代码可以在仓库的chapter-4分支获取,若存在与本文内容不符的地方,以master分支以及文章为准。

标准化发布流程

本节主要是讲解如何通过一行命令完成以下六点内容:

  1. 版本更新
  2. 生成 CHANGELOG
  3. 推送至 git 仓库
  4. 组件库打包
  5. 发布至 npm
  6. 打 tag 并推送至 git

如果你不想代码,很好,用np(如果我一开始就知道这个工具,我也不会去写代码,我真傻,真的)。

package.json

  1.  
    "scripts": {
  2.  
    "release": "ts-node ./scripts/release.ts"
  3.  
    },

直接甩代码吧,实在不复杂。

  1.  
    /* eslint-disable import/no-extraneous-dependencies,@typescript-eslint/camelcase, no-console */
  2.  
    import inquirer from 'inquirer';
  3.  
    import fs from 'fs';
  4.  
    import path from 'path';
  5.  
    import child_process from 'child_process';
  6.  
    import util from 'util';
  7.  
    import chalk from 'chalk';
  8.  
    import semverInc from 'semver/functions/inc';
  9.  
    import { ReleaseType } from 'semver';
  10.  
     
  11.  
    import pkg from '../package.json';
  12.  
     
  13.  
    const exec = util.promisify(child_process.exec);
  14.  
     
  15.  
    const run = async (command: string) => {
  16.  
    console.log(chalk.green(command));
  17.  
    await exec(command);
  18.  
    };
  19.  
     
  20.  
    const currentVersion = pkg.version;
  21.  
     
  22.  
    const getNextVersions = (): { [key in ReleaseType]: string | null } => ({
  23.  
    major: semverInc(currentVersion, 'major'),
  24.  
    minor: semverInc(currentVersion, 'minor'),
  25.  
    patch: semverInc(currentVersion, 'patch'),
  26.  
    premajor: semverInc(currentVersion, 'premajor'),
  27.  
    preminor: semverInc(currentVersion, 'preminor'),
  28.  
    prepatch: semverInc(currentVersion, 'prepatch'),
  29.  
    prerelease: semverInc(currentVersion, 'prerelease'),
  30.  
    });
  31.  
     
  32.  
    const timeLog = (logInfo: string, type: 'start' | 'end') => {
  33.  
    let info = '';
  34.  
    if (type === 'start') {
  35.  
    info = `=> 开始任务:${logInfo}`;
  36.  
    } else {
  37.  
    info = `✨ 结束任务:${logInfo}`;
  38.  
    }
  39.  
    const nowDate = new Date();
  40.  
    console.log(
  41.  
    `[${nowDate.toLocaleString()}.${nowDate
  42.  
    .getMilliseconds()
  43.  
    .toString()
  44.  
    .padStart(3, '0')}] ${info}
  45.  
    `,
  46.  
    );
  47.  
    };
  48.  
     
  49.  
    /**
  50.  
    * 获取下一次版本号
  51.  
    */
  52.  
    async function prompt(): Promise<string> {
  53.  
    const nextVersions = getNextVersions();
  54.  
    const { nextVersion } = await inquirer.prompt([
  55.  
    {
  56.  
    type: 'list',
  57.  
    name: 'nextVersion',
  58.  
    message: `请选择将要发布的版本 (当前版本 ${currentVersion})`,
  59.  
    choices: (Object.keys(nextVersions) as Array<ReleaseType>).map(level => ({
  60.  
    name: `${level} => ${nextVersions[level]}`,
  61.  
    value: nextVersions[level],
  62.  
    })),
  63.  
    },
  64.  
    ]);
  65.  
    return nextVersion;
  66.  
    }
  67.  
     
  68.  
    /**
  69.  
    * 更新版本号
  70.  
    * @param nextVersion 新版本号
  71.  
    */
  72.  
    async function updateVersion(nextVersion: string) {
  73.  
    pkg.version = nextVersion;
  74.  
    timeLog('修改package.json版本号', 'start');
  75.  
    await fs.writeFileSync(path.resolve(__dirname, './../package.json'), JSON.stringify(pkg));
  76.  
    await run('npx prettier package.json --write');
  77.  
    timeLog('修改package.json版本号', 'end');
  78.  
    }
  79.  
     
  80.  
    async function generateChangelog() {
  81.  
    timeLog('生成CHANGELOG.md', 'start');
  82.  
    await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0');
  83.  
    timeLog('生成CHANGELOG.md', 'end');
  84.  
    }
  85.  
     
  86.  
    /**
  87.  
    * 将代码提交至git
  88.  
    */
  89.  
    async function push(nextVersion: string) {
  90.  
    timeLog('推送代码至git仓库', 'start');
  91.  
    await run('git add package.json CHANGELOG.md');
  92.  
    await run(`git commit -m "v${nextVersion}" -n`);
  93.  
    await run('git push');
  94.  
    timeLog('推送代码至git仓库', 'end');
  95.  
    }
  96.  
     
  97.  
    /**
  98.  
    * 组件库打包
  99.  
    */
  100.  
    async function build() {
  101.  
    timeLog('组件库打包', 'start');
  102.  
    await run('npm run build');
  103.  
    timeLog('组件库打包', 'end');
  104.  
    }
  105.  
     
  106.  
    /**
  107.  
    * 发布至npm
  108.  
    */
  109.  
    async function publish() {
  110.  
    timeLog('发布组件库', 'start');
  111.  
    await run('npm publish');
  112.  
    timeLog('发布组件库', 'end');
  113.  
    }
  114.  
     
  115.  
    /**
  116.  
    * 打tag提交至git
  117.  
    */
  118.  
    async function tag(nextVersion: string) {
  119.  
    timeLog('打tag并推送至git', 'start');
  120.  
    await run(`git tag v${nextVersion}`);
  121.  
    await run(`git push origin tag v${nextVersion}`);
  122.  
    timeLog('打tag并推送至git', 'end');
  123.  
    }
  124.  
     
  125.  
    async function main() {
  126.  
    try {
  127.  
    const nextVersion = await prompt();
  128.  
    const startTime = Date.now();
  129.  
    // =================== 更新版本号 ===================
  130.  
    await updateVersion(nextVersion);
  131.  
    // =================== 更新changelog ===================
  132.  
    await generateChangelog();
  133.  
    // =================== 代码推送git仓库 ===================
  134.  
    await push(nextVersion);
  135.  
    // =================== 组件库打包 ===================
  136.  
    await build();
  137.  
    // =================== 发布至npm ===================
  138.  
    await publish();
  139.  
    // =================== 打tag并推送至git ===================
  140.  
    await tag(nextVersion);
  141.  
    console.log(`✨ 发布流程结束 共耗时${((Date.now() - startTime) / 1000).toFixed(3)}s`);
  142.  
    } catch (error) {
  143.  
    console.log('💣 发布失败,失败原因:', error);
  144.  
    }
  145.  
    }
  146.  
     
  147.  
    main();
学新通

初始化组件

每次初始化一个组件就要新建许多文件以及文件夹,复制粘贴也可,不过还可以使用更高级一点的偷懒方式。

常规思路,新建一个组件模板文件夹,里面包含一个组件所需要的所有文件,同时写好文件内容。

至于一些动态内容,譬如组件中英文名称,选一个你喜欢的模板语言(如 handlebars),用其方式留空{{componentName}}

package.json

  1.  
    "scripts": {
  2.  
    "new": "ts-node ./scripts/new.ts"
  3.  
    },

接下来我们在new.ts中编写相关步骤,无非是:

  1. 基于inquirer.js询问一些基本组件信息
  2. 结合信息,渲染模板(填空)至组件文件夹
  3. 向 components/index.ts 插入导出语句

你以为我会写new.ts吗,不,我不会(虽然我真写过)。

主要是使用metalsmith进行数据与模板结合,写脚手架的同学可能比较熟悉。

自从我知道了plop.js这个库,那么又可以偷懒了(为什么之前没有人告诉我有这么多好用的工具???)

  1.  
    "scripts": {
  2.  
    - "new": "ts-node ./scripts/new.ts",
  3.  
    "new": "plop --plopfile ./scripts/plopfile.ts",
  4.  
    },

于是上述流程可以大大简化,不需要写代码去询问,不需要手动渲染模板,我们要做的就是写好模板,并且配置好问题以及渲染目的地。

详情可见:

  • 配置文件:scripts/plopfile.ts
  • 模板文件:templates/component

结语

文章很长,也是我个人学习中的总结,如果本文帮助到了你请给仓库一颗 ✨✨ 和本文一个赞。

如果有错误烦请在评论区指正交流,谢谢。

仓库地址:https://github.com/worldzhao/react-ui-library-tutorial

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfifcgg
系列文章
更多 icon
同类精品
更多 icon
继续加载