可能是最詳細的React組件庫搭建總結

在線預覽:戳我 ??

本地預覽:

git clone git@github.com:worldzhao/react-ui-library-tutorial.git
cd react-ui-library-tutorial
yarn
yarn start

按順序執(zhí)行完命令后芜辕,即可在 localhost:3000 端口看到以下內容:

preview.png


概覽

本文包含以下內容:

  • prepare: 組件庫前期開發(fā)準備工作。eslint/commit lint/typescript等等;
  • dev: 使用docz進行開發(fā)調試以及文檔編寫;
  • build: umd/cjs/esm拴竹、types碎捺、polyfill 以及按需加載;
  • test: 組件測試狰晚;
  • release: 組件庫發(fā)布流程;
  • deploy: 使用now部署文檔站點缴啡,待補充壁晒;
  • other: 使用plop.js快速創(chuàng)建組件模板。

如果本文幫助到了你請給倉庫 一顆 ??业栅。

如果有錯誤煩請在評論區(qū)指正交流秒咐,謝謝谬晕。

倉庫地址

準備工作

初始化項目

新建一個happy-ui文件夾,并初始化携取。

mkdir happy-ui

cd happy-ui

npm init --y

mkdir components && cd components && touch index.ts # 新建源碼文件夾以及入口文件

代碼規(guī)范

此處直接使用@umijs/fabric的配置攒钳。

yarn add @umijs/fabric --dev

yarn add prettier --dev # 因為@umijs/fabric沒有將prettier作為依賴 所以我們需要手動安裝

.eslintrc.js

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

.prettierrc.js

const fabric = require('@umijs/fabric');

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

.stylelintrc.js

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

想自行配置的同學可以參考以下文章:

Commit Lint

進行pre-commit代碼規(guī)范檢測。

yarn add husky lint-staged --dev

package.json

"lint-staged": {
  "components/**/*.ts?(x)": [
    "prettier --write",
    "eslint --fix",
    "git add"
  ],
  "components/**/*.less": [
    "stylelint --syntax less --fix",
    "git add"
  ]
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
}

進行 Commit Message 檢測雷滋。

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

新增.commitlintrc.js寫入以下內容

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

package.json 寫入以下內容:

// ...
"scripts": {
  "commit": "git-cz",
}
// ...
"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
    "pre-commit": "lint-staged"
  }
},
"config": {
  "commitizen": {
    "path": "cz-conventional-changelog"
  }
}

后續(xù)使用 yarn commit 替代 git commit生成規(guī)范的 Commit Message不撑,當然為了效率你可以選擇手寫,但是要符合規(guī)范晤斩。

TypeScript

yarn add typescript --dev

新建tsconfig.json并寫入以下內容

{
  "compilerOptions": {
    "baseUrl": "./",
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "declaration": true,
    "declarationDir": "lib",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["components", "global.d.ts"],
  "exclude": ["node_modules"]
}

測試

components文件夾下新建alert文件夾燎孟,目錄結構如下:

alert
    ├── alert.tsx           # 源文件
    ├── index.ts            # 入口文件
    ├── interface.ts        # 類型聲明文件
    └── style
        ├── index.less      # 樣式文件
        └── index.ts        # 樣式文件里為什么存在一個index.ts - 按需加載樣式 管理樣式依賴 后面章節(jié)會提到

安裝React相關依賴:

yarn add react react-dom @types/react @types/react-dom --dev # 開發(fā)時依賴,宿主環(huán)境一定存在

yarn add prop-types            # 運行時依賴尸昧,宿主環(huán)境可能不存在 安裝本組件庫時一起安裝

此處依舊安裝了prop-types這個庫揩页,因為無法保證宿主環(huán)境也使用typescript,從而能夠進行靜態(tài)檢查烹俗,故使用prop-types保證javascript用戶也能得到友好的運行時報錯信息爆侣。

components/alert/interface.ts

export type Kind = 'info' | 'positive' | 'negative' | 'warning';
export type KindMap = Record<Kind, string>;

export interface AlertProps {
  /**
   * Set this to change alert kind
   * @default info
   */
  kind?: 'info' | 'positive' | 'negative' | 'warning';
}

components/alert/alter.tsx

import React from 'react';
import t from 'prop-types';

import { AlertProps, KindMap } from './interface';

const prefixCls = 'happy-alert';

const kinds: KindMap = {
  info: '#5352ED',
  positive: '#2ED573',
  negative: '#FF4757',
  warning: '#FFA502',
};

const Alert: React.FC<AlertProps> = ({ children, kind = 'info', ...rest }) => (
  <div
    className={prefixCls}
    style={{
      background: kinds[kind],
    }}
    {...rest}
  >
    {children}
  </div>
);

Alert.propTypes = {
  kind: t.oneOf(['info', 'positive', 'negative', 'warning']),
};

export default Alert;

components/alert/index.ts

import Alert from './alert';

export default Alert;

export * from './interface';

components/alert/style/index.less

@popupPrefix: happy-alert;

.@{popupPrefix} {
  padding: 20px;
  background: white;
  border-radius: 3px;
  color: white;
}

components/alert/style/index.ts

import './index.less';

components/index.ts

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

此處組件參考的docz項目typescript以及less示例。

git 一把梭幢妄,可以看到控制臺已經(jīng)進行鉤子檢測了兔仰。

git add .

yarn commit  # 或 git commit -m'feat: chapter-1 準備工作'

git push

準備工作完成。代碼可以在倉庫chapter-1分支獲取蕉鸳,若存在與本文內容不符的地方乎赴,以master分支以及文章為準。

開發(fā)與調試

本節(jié)解決開發(fā)組件時的預覽以及調試問題潮尝,順路解決文檔編寫榕吼。

此處選擇docz來輔助預覽調試。

docz基于MDX(Markdown + JSX)勉失,可以在 Markdown 中引入 React 組件羹蚣,使得一邊編寫文檔,一邊預覽調試成為了可能乱凿。而且得益于 React 組件生態(tài)顽素,我們可以像編寫應用一般編寫文檔,不僅僅是枯燥的文字徒蟆。docz 也內置了一些組件胁出,比如<Playground>

安裝 docz 以及自定義配置

yarn add docz --dev

yarn add rimraf --dev # 清空目錄的一個輔助庫

增加 npm scriptspackage.json段审。

"scripts": {
  "dev": "docz dev", // 啟動本地開發(fā)環(huán)境
  "start": "npm run dev", // dev命令別名
  "build:doc": "rimraf doc-site && docz build", // 后續(xù)會配置打包出來的文件目錄名為doc-site全蝶,故每次build前刪除
  "preview:doc": "docz serve" // 預覽文檔站點
},

注意:本節(jié)所有操作都是針對站點應用。打包指代文檔站點打包,而非組件庫裸诽。

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

doczrc.js

export default {
  files: './components/**/*.{md,markdown,mdx}', // 識別的文件后綴
  dest: 'doc-site', // 打包出來的文件目錄名
  title: 'happy-ui', // 站點標題
  typescript: true, // 組件源文件是通過typescript開發(fā)型凳,需要打開此選項
};

由于使用了less作為樣式預處理器丈冬,故需要安裝 less 插件。

yarn add less gatsby-plugin-less --dev

新建gatsby-config.js甘畅,并寫入以下內容:

gatsby-config.js

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

編寫文檔

新建components/alert/index.mdx埂蕊,并寫入以下內容:

---
name: Alert 警告提示
route: /Alert
menu: 組件
---

import { Playground } from 'docz'; import Alert from './alert'; // 引入組件 import './style'; // 引入組件樣式

# Alert 警告提示

警告提示,展現(xiàn)需要關注的信息疏唾。

## 代碼演示

### 基本用法

<Playground>
  <Alert kind="warning">這是一條警告提示</Alert>
</Playground>

## API

| 屬性 | 說明     | 類型                                         | 默認值 |
| ---- | -------- | -------------------------------------------- | ------ |
| kind | 警告類型 | 'info'/'positive'/'negative'/'warning'非必填 | 'info' |

執(zhí)行腳本命令:

yarn start # or yarn dev

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

文檔站點

現(xiàn)在可以在index.mdx中愉快地進行文檔編寫和調試了蓄氧!

優(yōu)化文檔編寫

代碼演示部分的demo較多(比如基本用法、高級用法以及各種用法等等)槐脏,在組件復雜的情況下喉童,會導致文檔源文件很長難以維護。那就抽離吧顿天。

components/alert/文件夾下新建demo文件夾堂氯,存放我們在編寫文檔時需要引用的 demo

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

import React from 'react';
import Alert from '../alert';
import '../style';

export default () => <Alert kind="warning"></Alert>;

components/alert/index.mdx

- import Alert from './alert'; // 引入組件
- import './style'; // 引入組件樣式
+ import BasicDemo from './demo/1-demo-basic';

...

<Playground>
- <Alert kind="warning">這是一條警告提示</Alert>
+ <BasicDemo />
</Playground>

這樣我們就將 demo 與文檔進行了分隔牌废。預覽如下:

文檔重構

等等咽白,代碼區(qū)域顯示的是<BasicDemo />,而非demo源碼鸟缕。

<Playground />組件暫時無法支持上述形式的展示:自定義下方展示的代碼晶框,而非<Playground />內部的代碼。相關討論如下:

其實第一條 PR 已經(jīng)解決了問題懂从,但是被關閉了授段,無奈。

不過既然都能引入 React 組件了番甩,在MDX的環(huán)境下自定義一個Playground組件又有何難呢畴蒲,無非就是渲染組件(MDX 自帶)和展示源碼,簡單開放的東西大家都是喜聞樂見的对室,就叫HappyBox吧模燥。

優(yōu)化代碼展示

編寫 <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 - 代碼展示區(qū)域
  • prismjs - 代碼高亮
  • raw-loader - 將源碼轉成字符串
  • react-copy-to-clipboard - 讓用戶爸爸們能夠 copy demo 代碼
  • react-tooltip/react-feather 輔助組件
  • styled-components 方便在文檔示例中讓用戶看到樣式掩宜,也用作文檔組件的樣式處理

這些依賴都是服務于文檔站點應用蔫骂,和組件庫自身毫無關聯(lián)。

最終效果如下:

最終效果

根目錄下新建doc-comps文件夾牺汤,存放文檔中使用的一些工具組件辽旋,比如<HappyBox />

doc-comps

├── happy-box
│   ├── style.ts
│   └── index.tsx
└── index.ts

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

<details>
<summary>展開查看代碼</summary>

import React from 'react';
import Editor from 'react-simple-code-editor';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useToggle } from 'react-use';
import ReactTooltip from 'react-tooltip';
import IconCopy from 'react-feather/dist/icons/clipboard';
import IconCode from 'react-feather/dist/icons/code';
import { highlight, languages } from 'prismjs/components/prism-core';
import { StyledContainer, StyledIconWrapper } from './style';

import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-markup';

require('prismjs/components/prism-jsx');

interface Props {
  code: string;
  title?: React.ReactNode;
  desc?: React.ReactNode;
}

export const HappyBox: React.FC<Props> = ({ code, title, desc, children }) => {
  const [isEditVisible, toggleEditVisible] = useToggle(false);

  return (
    <StyledContainer>
      <section className="code-box-demo"> {children}</section>
      <section className="code-box-meta">
        <div className="text-divider">
          <span>{title || '示例'}</span>
        </div>
        <div className="code-box-description">
          <p>{desc || '暫無描述'}</p>
        </div>
        <div className="divider" />
        <div className="code-box-action">
          <CopyToClipboard text={code} onCopy={() => alert('復制成功')}>
            <IconCopy data-place="top" data-tip="復制代碼" />
          </CopyToClipboard>

          <StyledIconWrapper onClick={toggleEditVisible}>
            <IconCode data-place="top" data-tip={isEditVisible ? '收起代碼' : '顯示代碼'} />
          </StyledIconWrapper>
        </div>
      </section>
      {renderEditor()}
      <ReactTooltip />
    </StyledContainer>
  );

  function renderEditor() {
    if (!isEditVisible) return null;
    return (
      <div className="container_editor_area">
        <Editor
          readOnly
          value={code}
          onValueChange={() => {}}
          highlight={code => highlight(code, languages.jsx)}
          padding={10}
          className="container__editor"
          style={{
            fontFamily: '"Fira code", "Fira Mono", monospace',
            fontSize: 14,
          }}
        />
      </div>
    );
  }
};

export default HappyBox;

</details>

components/doc-comps/happy-box/style.ts

<details>
<summary>展開查看代碼</summary>

import styled from 'styled-components';

export const StyledIconWrapper = styled.div`
  display: flex;
  align-items: center;
  margin-left: 10px;
`;

export const StyledContainer = styled.div`
  position: relative;
  display: inline-block;
  width: 100%;
  margin: 0 0 16px;
  border: 1px solid #ebedf0;
  border-radius: 2px;
  transition: all 0.2s;

  .text-divider {
    display: table;

    &::before,
    &::after {
      content: '';
      position: relative;
      display: table-cell;
      transform: translateY(50%);
      content: '';
      border-top: 1px solid #e8e8e8;
    }

    &::before {
      top: 50%;
      width: 5%;
    }

    &::after {
      width: 95%;
      top: 50%;
      width: 95%;
    }

    & > span {
      display: inline-block;
      padding: 0 10px;
      font-weight: 500;
      font-size: 16px;
      white-space: nowrap;
      text-align: center;
      font-variant: tabular-nums;
      line-height: 1.5;
    }
  }

  .divider {
    margin: 0;
    background: none;
    border: dashed #e8e8e8;
    border-width: 1px 0 0;
    display: block;
    clear: both;
    width: 100%;
    min-width: 100%;
    height: 1px;
    position: relative;
    top: -0.06em;
    box-sizing: border-box;
    padding: 0;
    font-size: 14px;
    font-variant: tabular-nums;
    line-height: 1.5;
    list-style: none;
    font-feature-settings: 'tnum';
  }

  .code-box-demo {
    transition: all 0.2s;
    padding: 42px 24px 50px;
  }

  .code-box-meta {
    font-size: 14px;
    line-height: 2;
  }

  .code-box .ant-divider {
    margin: 0;
  }

  .code-box-description {
    padding: 18px 24px 12px;
  }

  .code-box-action {
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
  }

  .code-box-action .anticon {
    margin: 0 8px;
    cursor: pointer;
  }

  .container_editor_area {
    border-top: 1px solid rgb(232, 232, 232);
    padding: 16px;
  }

  .container__editor {
    font-variant-ligatures: common-ligatures;
    border-radius: 3px;
  }

  .container__editor textarea {
    outline: 0;
    background-color: none;
  }

  .button {
    display: inline-block;
    padding: 0 6px;
    text-decoration: none;
    background: #000;
    color: #fff;
  }

  .button:hover {
    background: linear-gradient(45deg, #e42b66, #e2433f);
  }

  /* Syntax highlighting */
  .token.comment,
  .token.prolog,
  .token.doctype,
  .token.cdata {
    color: #90a4ae;
  }
  .token.punctuation {
    color: #9e9e9e;
  }
  .namespace {
    opacity: 0.7;
  }
  .token.property,
  .token.tag,
  .token.boolean,
  .token.number,
  .token.constant,
  .token.symbol,
  .token.deleted {
    color: #e91e63;
  }
  .token.selector,
  .token.attr-name,
  .token.string,
  .token.char,
  .token.builtin,
  .token.inserted {
    color: #4caf50;
  }
  .token.operator,
  .token.entity,
  .token.url,
  .language-css .token.string,
  .style .token.string {
    color: #795548;
  }
  .token.atrule,
  .token.attr-value,
  .token.keyword {
    color: #3f51b5;
  }
  .token.function {
    color: #f44336;
  }
  .token.regex,
  .token.important,
  .token.variable {
    color: #ff9800;
  }
  .token.important,
  .token.bold {
    font-weight: bold;
  }
  .token.italic {
    font-style: italic;
  }
  .token.entity {
    cursor: help;
  }
`;

</details>

相關配置

  • 增加 alias別名,樣例源碼展示相對路徑不夠友好补胚,讓用戶直接拷貝才夠省心

新建gatsby-node.js码耐,寫入以下內容以開啟alias

const path = require('path');

exports.onCreateWebpackConfig = args => {
  args.actions.setWebpackConfig({
    resolve: {
      modules: [path.resolve(__dirname, '../src'), 'node_modules'],
      alias: {
        'happy-ui/lib': path.resolve(__dirname, '../components/'),
        'happy-ui/esm': path.resolve(__dirname, '../components/'),
        'happy-ui': path.resolve(__dirname, '../components/'),
      },
    },
  });
};

tsconfig.json 打包時需要忽略demo,避免組件庫打包生成types時包含其中溶其,同時增加paths屬性用于 vscode 自動提示:

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./",
+   "paths": {
+     "happy-ui": ["components/index.ts"],
+     "happy-ui/esm/*": ["components/*"],
+     "happy-ui/lib/*": ["components/*"]
+    },
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "declaration": true,
    "declarationDir": "lib",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["components", "global.d.ts"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules",  "**/demo/**"]
}

新的問題出現(xiàn)了骚腥,vscode 的 alias 提示依賴 tsconfig.json,忽略 demo 文件夾后瓶逃,demo 內的文件模塊類型找不到聲明(paths 失效)束铭,所以不能將 demo 在 tsconfig.json 中移除:

{
- "exclude": ["node_modules",  "**/demo/**"]
+ "exclude": ["node_modules"]
}

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

tsconfig.build.json

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

后續(xù)使用 tsc 生成類型聲明文件指定tsconfig.build.json即可。

改造相關文件

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

- import Alert from '../alert';
+ import Alert from 'happy-ui/lib/alert';

- import '../style';
+ import 'happy-ui/lib/alert/style';

components/alert/index.mdx

- import { Playground } from 'docz';
+ import { HappyBox } from '../../doc-comps';

+ import BasicDemoCode from '!raw-loader!./demo/1-demo-basic.tsx';

...

- <Playground>
-   <BasicDemo />
- </Playground>

+ <HappyBox code={BasicDemoCode} title="基本用法" desc="使用kind控制Alert類型">
+  <BasicDemo />
+ </HappyBox>

yarn start卡住時嘗試刪除根目錄.docz文件夾厢绝,而后重新執(zhí)行命令契沫。

現(xiàn)在可以愉快地開發(fā)組件了。代碼可以在倉庫chapter-2分支獲取昔汉,若存在與本文內容不符的地方懈万,以master分支以及文章為準。

組件庫打包

宿主環(huán)境各不相同靶病,需要將源碼進行相關處理后發(fā)布至 npm钞速。

明確以下目標:

  1. 導出類型聲明文件;
  2. 導出 umd/Commonjs module/ES module 等 3 種形式供使用者引入嫡秕;
  3. 支持樣式文件 css 引入渴语,而非只有less,減少業(yè)務方接入成本昆咽;
  4. 支持按需加載驾凶。

導出類型聲明文件

既然是使用typescript編寫的組件庫,那么使用者應當享受到類型系統(tǒng)的好處掷酗。

我們可以生成類型聲明文件调违,并在package.json中定義入口,如下:

package.json

{
  "typings": "lib/index.d.ts", // 定義類型入口文件
  "scripts": {
    "build:types": "tsc -p tsconfig.build.json && cpr lib esm" // 執(zhí)行tsc命令生成類型聲明文件
  }
}

值得注意的是:此處使用cpr(需要手動安裝)將lib的聲明文件拷貝了一份泻轰,并將文件夾重命名為esm技肩,用于后面存放 ES module 形式的組件。這樣做的原因是保證用戶手動按需引入組件時依舊可以獲取自動提示浮声。

最開始的方式是將聲明文件單獨存放在types文件夾虚婿,但這樣只有通過'happy-ui'引入才可以獲取提示,而'happy-ui/esm/xxx'和'happy-ui/lib/xxx'就無法獲取提示泳挥。

tsconfig.build.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": { "emitDeclarationOnly": true }, // 只生成聲明文件
  "exclude": ["**/__tests__/**", "**/demo/**", "node_modules", "lib", "esm"] // 排除示例然痊、測試以及打包好的文件夾
}

執(zhí)行yarn build:types,可以發(fā)現(xiàn)根目錄下已經(jīng)生成了lib文件夾(tsconfig.json中定義的declarationDir字段)以及esm文件夾(拷貝而來)屉符,目錄結構與components文件夾保持一致剧浸,如下:

lib

├── alert
│   ├── alert.d.ts
│   ├── index.d.ts
│   ├── interface.d.ts
│   └── style
│       └── index.d.ts
└── index.d.ts

這樣使用者引入npm 包時锹引,便能得到自動提示,也能夠復用相關組件的類型定義唆香。

接下來將ts(x)等文件處理成js文件嫌变。

需要注意的是,我們需要輸出Commonjs module以及ES module兩種模塊類型的文件(暫不考慮umd)躬它,以下使用cjs指代Commonjs module腾啥,esm指代ES module
對此有疑問的同學推薦閱讀:import虑凛、require碑宴、export软啼、module.exports 混合詳解

導出 Commonjs 模塊

其實完全可以使用babeltsc命令行工具進行代碼編譯處理(實際上很多工具庫就是這樣做的)桑谍,但考慮到還要樣式處理及其按需加載,我們借助 gulp 來串起這個流程祸挪。

babel 配置

首先安裝babel及其相關依賴

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

新建.babelrc.js文件锣披,寫入以下內容:

.babelrc.js

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

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

  • helpers選項設置為true,可抽離代碼編譯過程重復生成的 helper 函數(shù)(classCallCheck,extends等)贿条,減小生成的代碼體積雹仿;
  • corejs設置為3,可引入不污染全局的按需polyfill整以,常用于類庫編寫(我更推薦:不引入polyfill胧辽,轉而告知使用者需要引入何種polyfill,避免重復引入或產(chǎn)生沖突公黑,后面會詳細提到)邑商。

更多參見官方文檔-@babel/plugin-transform-runtime

配置目標環(huán)境

為了避免轉譯瀏覽器原生支持的語法,新建.browserslistrc文件凡蚜,根據(jù)適配需求人断,寫入支持瀏覽器范圍,作用于@babel/preset-env朝蜘。

.browserslistrc

>0.2%
not dead
not op_mini all

很遺憾的是恶迈,@babel/runtime-corejs3無法在按需引入的基礎上根據(jù)目標瀏覽器支持程度再次減少polyfill的引入,參見@babel/runtime for target environment 谱醇。

這意味著@babel/runtime-corejs3 甚至會在針對現(xiàn)代引擎的情況下注入所有可能的 polyfill:不必要地增加了最終捆綁包的大小暇仲。

對于組件庫(代碼量可能很大),個人建議將polyfill的選擇權交還給使用者副渴,在宿主環(huán)境進行polyfill熔吗。若使用者具有兼容性要求,自然會使用@babel/preset-env + core-js + .browserslistrc進行全局polyfill佳晶,這套組合拳引入了最低目標瀏覽器不支持API的全部 polyfill桅狠。

順帶一提,業(yè)務開發(fā)中,若將@babel/preset-envuseBuiltIns選項值設置為 usage中跌,同時把node_modulesbabel-loaderexclude咨堤,會導致babel 無法檢測到nodes_modules中所需要的polyfill"useBuiltIns: usage" for node_modules without transpiling #9419漩符,在未支持該issue提到的內容之前一喘,請將useBuiltIns設置為entry,或者不要把node_modulesbabel-loaderexclude嗜暴。

所以組件庫不用畫蛇添足凸克,引入多余的polyfill,寫好文檔說明闷沥,比什么都重要(就像zentantd這樣)萎战。

現(xiàn)在@babel/runtime-corejs3更換為@babel/runtime,只進行helper函數(shù)抽離舆逃。

yarn remove @babel/runtime-corejs3

yarn add @babel/runtime

.babelrc.js

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

@babel/transform-runtimehelper選項默認為true蚂维。

gulp 配置

再來安裝gulp相關依賴

yarn add gulp gulp-babel --dev

新建gulpfile.js,寫入以下內容:

gulpfile.js

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

const paths = {
  dest: {
    lib: 'lib', // commonjs 文件存放的目錄名 - 本塊關注
    esm: 'esm', // ES module 文件存放的目錄名 - 暫時不關心
    dist: 'dist', // umd文件存放的目錄名 - 暫時不關心
  },
  styles: 'components/**/*.less', // 樣式文件路徑 - 暫時不關心
  scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'], // 腳本文件路徑
};

function compileCJS() {
  const { dest, scripts } = paths;
  return gulp
    .src(scripts)
    .pipe(babel()) // 使用gulp-babel處理
    .pipe(gulp.dest(dest.lib));
}

// 并行任務 后續(xù)加入樣式處理 可以并行處理
const build = gulp.parallel(compileCJS);

exports.build = build;

exports.default = build;

修改package.json

package.json

{
- "main": "index.js",
+ "main": "lib/index.js",
  "scripts": {
    ...
+   "clean": "rimraf lib esm dist",
+   "build": "npm run clean && npm run build:types && gulp",
    ...
  },
}

執(zhí)行yarn build,得到如下內容:

lib

├── alert
│   ├── alert.js
│   ├── index.js
│   ├── interface.js
│   └── style
│       └── index.js
└── index.js

觀察編譯后的源碼,可以發(fā)現(xiàn):諸多helper方法已被抽離至@babel/runtime中悍募,模塊導入導出形式也是commonjs規(guī)范垦页。

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函數(shù)。

.babelrc.js

module.exports = {
  presets: [
    [
      '@babel/env',
      {
        modules: false, // 關閉模塊轉換
      },
    ],
    '@babel/typescript',
    '@babel/react',
  ],
  plugins: [
    '@babel/proposal-class-properties',
    [
      '@babel/plugin-transform-runtime',
      {
        useESModules: true, // 使用esm形式的helper
      },
    ],
  ],
};

目標達成锰悼,我們再使用環(huán)境變量區(qū)分esmcjs(執(zhí)行任務時設置對應的環(huán)境變量即可)柳骄,最終babel配置如下:

.babelrc.js

module.exports = {
  presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],
  env: {
    esm: {
      presets: [
        [
          '@babel/env',
          {
            modules: false,
          },
        ],
      ],
      plugins: [
        [
          '@babel/plugin-transform-runtime',
          {
            useESModules: true,
          },
        ],
      ],
    },
  },
};

接下來修改gulp相關配置,抽離compileScripts任務箕般,增加compileESM任務耐薯。

gulpfile.js

// ...

/**
 * 編譯腳本文件
 * @param {string} babelEnv babel環(huán)境變量
 * @param {string} destDir 目標目錄
 */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  // 設置環(huán)境變量
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel()) // 使用gulp-babel處理
    .pipe(gulp.dest(destDir));
}

/**
 * 編譯cjs
 */
function compileCJS() {
  const { dest } = paths;
  return compileScripts('cjs', dest.lib);
}

/**
 * 編譯esm
 */
function compileESM() {
  const { dest } = paths;
  return compileScripts('esm', dest.esm);
}

// 串行執(zhí)行編譯腳本任務(cjs,esm) 避免環(huán)境變量影響
const buildScripts = gulp.series(compileCJS, compileESM);

// 整體并行執(zhí)行任務
const build = gulp.parallel(buildScripts);

// ...

執(zhí)行yarn build,可以發(fā)現(xiàn)生成了lib/esm兩個文件夾丝里,觀察esm目錄曲初,結構同lib一致,js 文件都是以ES module模塊形式導入導出杯聚。

esm/alert/alert.js

esm/alert/alert.js

別忘了給package.json增加相關入口臼婆。

package.json

{
+ "module": "esm/index.js"
}

處理樣式文件

拷貝 less 文件

我們會將less文件包含在npm包中,用戶可以通過happy-ui/lib/alert/style/index.js的形式按需引入less文件幌绍,此處可以直接將 less 文件拷貝至目標文件夾颁褂。

gulpfile.js中新建copyLess任務故响。

gulpfile.js

// ...

/**
 * 拷貝less文件
 */
function copyLess() {
  return gulp
    .src(paths.styles)
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(buildScripts, copyLess);

// ...

觀察lib目錄,可以發(fā)現(xiàn) less 文件已被拷貝至alert/style目錄下颁独。

lib

├── alert
│   ├── alert.js
│   ├── index.js
│   ├── interface.js
│   └── style
│       ├── index.js
│       └── index.less # less文件
└── index.js

可能有些同學已經(jīng)發(fā)現(xiàn)問題:若使用者沒有使用less預處理器彩届,使用的是sass方案甚至原生css方案,那現(xiàn)有方案就搞不定了誓酒。經(jīng)分析樟蠕,有以下 4 種預選方案:

  1. 告知業(yè)務方增加less-loader。會導致業(yè)務方使用成本增加靠柑;
  2. 打包出一份完整的 css 文件寨辩,進行全量引入。無法進行按需引入歼冰;
  3. css in js方案靡狞;
  4. 提供一份style/css.js文件,引入組件 css樣式依賴停巷,而非 less 依賴耍攘,組件庫底層抹平差異榕栏。

重點看一看方案 3 以及方案 4畔勤。

css in js除了賦予樣式編寫更多的可能性之外,在編寫第三方組件庫時更是利器扒磁。

如果我們寫一個react-use這種hooks工具庫庆揪,不涉及到樣式,只需要在package.json中設置sideEffectsfalse妨托,業(yè)務方使用 webpack 進行打包時缸榛,只會打包被使用到的 hooks(優(yōu)先使用 ES module)。

入口文件index.js中導出的但未被使用的其他 hooks 會被tree shaking兰伤,第一次使用這個庫的時候我很好奇内颗,為什么沒有按需引入的使用方式,結果打包分析時我傻了敦腔,原來人家天生支持按需引入均澳。

可能常用的antd以及lodash都要配一配,導致產(chǎn)生了慣性思維符衔。

回到正題找前。如果將樣式使用javascript來編寫,在某種維度上講判族,組件庫和工具庫一致了躺盛,配好sideEffects,自動按需引入形帮,美滋滋槽惫。

而且每個組件都與自己的樣式綁定周叮,不需要業(yè)務方或組件開發(fā)者去維護樣式依賴,什么是樣式依賴界斜,后面會講到则吟。

缺點:

  1. 樣式無法單獨緩存;
  2. styled-components 自身體積較大锄蹂;
  3. 復寫組件樣式需要使用屬性選擇器或者使用styled-components自帶方法氓仲。

需要看取舍了,偷偷說一句styled-components做主題定制也極其方便得糜。

方案 4 是antd使用的這種方案敬扛。

在搭建組件庫的過程中,有一個問題困擾了我很久:為什么需要alert/style/index.js引入less文件或alert/style/css.js引入css文件朝抖?

答案是管理樣式依賴啥箭。

因為我們的組件是沒有引入樣式文件的,需要使用者去手動引入治宣。

假設存在以下場景:使用者引入<Button />急侥,<Button />依賴了<Icon />,則需要手動去引入調用組件的樣式(<Button />)及其依賴的組件樣式(<Icon />)侮邀,遇到復雜組件極其麻煩坏怪,所以組件庫開發(fā)者可以提供一份這樣的js文件,使用者手動引入這個js文件绊茧,就能引入對應組件及其依賴組件的樣式铝宵。

那么問題又來了,為什么組件不能自己去import './index.less'呢华畏?

可以鹏秋,但業(yè)務方需要配置less-loader,什么亡笑,業(yè)務方不想配侣夷,要你import './index.css'???

可以仑乌,業(yè)務方爽了百拓,組件開發(fā)者不開心。

所以我們要找一個大家都爽的方案:

  1. 組件開發(fā)者能夠開心的使用預處理器绝骚;
  2. 業(yè)務方不需要額外的使用成本耐版。

答案就是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任務焙蚓。

// ...

/**
 * 生成css文件
 */
function less2css() {
  return gulp
    .src(paths.styles)
    .pipe(less()) // 處理less文件
    .pipe(autoprefixer()) // 根據(jù)browserslistrc增加前綴
    .pipe(cssnano({ zindex: false, reduceIdents: false })) // 壓縮
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(buildScripts, copyLess, less2css);

// ...

執(zhí)行yarn build纹冤,組件style目錄下已經(jīng)存在css文件了。

接下來我們需要一個alert/style/css.js來幫用戶引入css文件购公。

生成 css.js

此處參考antd-tools的實現(xiàn)方式:在處理scripts任務中萌京,截住style/index.js,生成style/css.js宏浩,并通過正則將引入的less文件后綴改成css知残。

安裝相關依賴。

yarn add through2 --dev

gulpfile.js

// ...

/**
 * 編譯腳本文件
 * @param {*} babelEnv babel環(huán)境變量
 * @param {*} destDir 目標目錄
 */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel()) // 使用gulp-babel處理
    .pipe(
      through2.obj(function z(file, encoding, next) {
        this.push(file.clone());
        // 找到目標
        if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
          const content = file.contents.toString(encoding);
          file.contents = Buffer.from(cssInjection(content)); // 文件內容處理
          file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名
          this.push(file); // 新增該文件
          next();
        } else {
          next();
        }
      }),
    )
    .pipe(gulp.dest(destDir));
}

// ...

cssInjection的實現(xiàn):

gulpfile.js

/**
 * 當前組件樣式 import './index.less' => import './index.css'
 * 依賴的其他組件樣式 import '../test-comp/style' => import '../test-comp/style/css.js'
 * 依賴的其他組件樣式 import '../test-comp/style/index.js' => import '../test-comp/style/css.js'
 * @param {string} content
 */
function cssInjection(content) {
  return content
    .replace(/\/style\/?'/g, "/style/css'")
    .replace(/\/style\/?"/g, '/style/css"')
    .replace(/\.less/g, '.css');
}

再進行打包比庄,可以看見組件style目錄下生成了css.js文件求妹,引入的也是上一步less轉換而來的css文件。

lib/alert

├── alert.js
├── index.js
├── interface.js
└── style
    ├── css.js # 引入index.css
    ├── index.css
    ├── index.js
    └── index.less

按需加載

在 package.json 中增加sideEffects屬性佳窑,配合ES module達到tree shaking效果(將樣式依賴文件標注為side effects制恍,避免被誤刪除)。

// ...
"sideEffects": [
  "dist/*",
  "esm/**/style/*",
  "lib/**/style/*",
  "*.less"
],
// ...

使用以下方式引入华嘹,可以做到js部分的按需加載吧趣,但需要手動引入樣式:

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

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

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

以上引入樣式文件的方式不太優(yōu)雅法竞,直接入口處引入全量樣式文件又和按需加載的本意相去甚遠耙厚。

使用者可以借助babel-plugin-import來進行輔助,減少代碼編寫量(說好的不加入其他使用成本的呢~)岔霸。

import { Alert } from 'happy-ui';

??

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

生成 umd

沒用上薛躬,這一塊標記為 todo 吧。

本節(jié)代碼可以在倉庫chapter-3分支獲取呆细,若存在與本文內容不符的地方型宝,以master分支以及文章為準。

組件測試

與軟件操作行為越接近的測試絮爷,越能給予你信心趴酣。

本節(jié)主要講述如何在組件庫中引入jest以及@testing-library/react,而不會深入單元測試的學習坑夯。

如果你對下列問題感興趣:

  1. What-單元測試是什么岖寞?
  2. Why-為什么要寫單元測試?
  3. How-編寫單元測試的最佳實踐柜蜈?

那么可以看看以下文章:

相關配置

安裝依賴:

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的狀態(tài)(即為jestexcept方法返回值增加更多專注于DOMmatchers)字逗;
  • identity-obj-proxy:一個工具庫京郑,此處用來mock樣式文件。

新建jest.config.js葫掉,并寫入相關配置些举,更多配置可參考jest 官方文檔-配置,只看幾個常用的就可以俭厚。

jest.config.js

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

修改package.json户魏,增加測試相關命令,并且代碼提交前挪挤,跑測試用例叼丑,如下:

package.json

"scripts": {
  ...
+  "test": "jest",                         # 執(zhí)行jest
+  "test:watch": "jest --watch",           # watch模式下執(zhí)行
+  "test:coverage": "jest --coverage",     # 生成測試覆蓋率報告
+  "test:update": "jest --updateSnapshot"  # 更新快照
},
...
"lint-staged": {
  "components/**/*.ts?(x)": [
    "prettier --write",
    "eslint --fix",
+   "jest --bail --findRelatedTests",
    "git add"
  ],
  ...
}

修改gulpfile.js以及tsconfig.json,避免打包時扛门,把測試文件一并處理了鸠信。

gulpfile.js

const paths = {
  ...
- scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'],
+ scripts: [
+   'components/**/*.{ts,tsx}',
+   '!components/**/demo/*.{ts,tsx}',
+   '!components/**/__tests__/*.{ts,tsx}',
+ ],
};

tsconfig.json

{
- "exclude": ["components/**/demo"]
+ "exclude": ["components/**/demo", "components/**/__tests__"]
}

編寫測試用例

<Alert />比較簡單,此處只作示例用论寨,簡單進行一下快照測試星立。

在對應組件的文件夾下新建__tests__文件夾,用于存放測試文件葬凳,其內新建index.test.tsx文件绰垂,寫入以下測試用例:

components/alert/tests/index.test.tsx

import React from 'react';
import { render } from '@testing-library/react';
import Alert from '../alert';

describe('<Alert />', () => {
  test('should render default', () => {
    const { container } = render(<Alert>default</Alert>);
    expect(container).toMatchSnapshot();
  });

  test('should render alert with type', () => {
    const kinds: any[] = ['info', 'warning', 'positive', 'negative'];

    const { getByText } = render(
      <>
        {kinds.map(k => (
          <Alert kind={k} key={k}>
            {k}
          </Alert>
        ))}
      </>,
    );

    kinds.forEach(k => {
      expect(getByText(k)).toMatchSnapshot();
    });
  });
});

更新一下快照:

yarn test:update

可以看見同級目錄下新增了一個__snapshots__文件夾,里面存放對應測試用例的快照文件火焰。

生成的快照文件

再執(zhí)行測試用例:

yarn test
通過測試用例

可以發(fā)現(xiàn)我們通過了測試用例劲装。。荐健。額酱畅,這里當然能通過琳袄,主要是后續(xù)我們進行迭代重構時,都會重新執(zhí)行測試用例纺酸,與最近的一次快照進行比對窖逗,如果與快照不一致(結構發(fā)生了改變),那么相應的測試用例就無法通過餐蔬。

對于快照測試碎紊,褒貶不一,這個例子也著實簡單得很樊诺,甚至連擴展的 jest-dom提供的 matchers 都沒用上仗考。

如何編寫優(yōu)秀的測試用例,我也是一個新手词爬,只能說多看多寫多嘗試秃嗜,前面推薦的文章很不錯。

本節(jié)代碼可以在倉庫chapter-4分支獲取顿膨,若存在與本文內容不符的地方锅锨,以master分支以及文章為準。

標準化發(fā)布流程

本節(jié)主要是講解如何通過一行命令完成以下六點內容:

  1. 版本更新
  2. 生成 CHANGELOG
  3. 推送至 git 倉庫
  4. 組件庫打包
  5. 發(fā)布至 npm
  6. 打 tag 并推送至 git

package.json

"scripts": {
+ "release": "ts-node ./scripts/release.ts"
},

<details>
<summary>展開查看代碼</summary>

/* eslint-disable  import/no-extraneous-dependencies,@typescript-eslint/camelcase, no-console */
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import child_process from 'child_process';
import util from 'util';
import chalk from 'chalk';
import semverInc from 'semver/functions/inc';
import { ReleaseType } from 'semver';

import pkg from '../package.json';

const exec = util.promisify(child_process.exec);

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

const currentVersion = pkg.version;

const getNextVersions = (): { [key in ReleaseType]: string | null } => ({
  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 timeLog = (logInfo: string, type: 'start' | 'end') => {
  let info = '';
  if (type === 'start') {
    info = `=> 開始任務:${logInfo}`;
  } else {
    info = `? 結束任務:${logInfo}`;
  }
  const nowDate = new Date();
  console.log(
    `[${nowDate.toLocaleString()}.${nowDate
      .getMilliseconds()
      .toString()
      .padStart(3, '0')}] ${info}
    `,
  );
};

/**
 * 詢問獲取下一次版本號
 */
async function prompt(): Promise<string> {
  const nextVersions = getNextVersions();
  const { nextVersion } = await inquirer.prompt([
    {
      type: 'list',
      name: 'nextVersion',
      message: `請選擇將要發(fā)布的版本 (當前版本 ${currentVersion})`,
      choices: (Object.keys(nextVersions) as Array<ReleaseType>).map(level => ({
        name: `${level} => ${nextVersions[level]}`,
        value: nextVersions[level],
      })),
    },
  ]);
  return nextVersion;
}

/**
 * 更新版本號
 * @param nextVersion 新版本號
 */
async function updateVersion(nextVersion: string) {
  pkg.version = nextVersion;
  timeLog('修改package.json版本號', 'start');
  await fs.writeFileSync(path.resolve(__dirname, './../package.json'), JSON.stringify(pkg));
  await run('npx prettier package.json --write');
  timeLog('修改package.json版本號', 'end');
}

/**
 * 生成CHANGELOG
 */
async function generateChangelog() {
  timeLog('生成CHANGELOG.md', 'start');
  await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0');
  timeLog('生成CHANGELOG.md', 'end');
}

/**
 * 將代碼提交至git
 */
async function push(nextVersion: string) {
  timeLog('推送代碼至git倉庫', 'start');
  await run('git add package.json CHANGELOG.md');
  await run(`git commit -m "v${nextVersion}" -n`);
  await run('git push');
  timeLog('推送代碼至git倉庫', 'end');
}

/**
 * 組件庫打包
 */
async function build() {
  timeLog('組件庫打包', 'start');
  await run('npm run build');
  timeLog('組件庫打包', 'end');
}

/**
 * 發(fā)布至npm
 */
async function publish() {
  timeLog('發(fā)布組件庫', 'start');
  await run('npm publish');
  timeLog('發(fā)布組件庫', 'end');
}

/**
 * 打tag提交至git
 */
async function tag(nextVersion: string) {
  timeLog('打tag并推送至git', 'start');
  await run(`git tag v${nextVersion}`);
  await run(`git push origin tag v${nextVersion}`);
  timeLog('打tag并推送至git', 'end');
}

async function main() {
  try {
    const nextVersion = await prompt();
    const startTime = Date.now();
    // =================== 更新版本號 ===================
    await updateVersion(nextVersion);
    // =================== 更新changelog ===================
    await generateChangelog();
    // =================== 代碼推送git倉庫 ===================
    await push(nextVersion);
    // =================== 組件庫打包 ===================
    await build();
    // =================== 發(fā)布至npm ===================
    await publish();
    // =================== 打tag并推送至git ===================
    await tag(nextVersion);
    console.log(`? 發(fā)布流程結束 共耗時${((Date.now() - startTime) / 1000).toFixed(3)}s`);
  } catch (error) {
    console.log('?? 發(fā)布失敗恋沃,失敗原因:', error);
  }
}

main();

</details>

如果你對這一節(jié)不感興趣必搞,也可以直接使用np進行發(fā)布,需要自定義配置一些鉤子囊咏。

初始化組件

每次初始化一個組件就要新建許多文件(夾)恕洲,復制粘貼也可,不過還可以使用更高級一點的偷懶方式梅割。

思路如下:

  1. 創(chuàng)建組件模板霜第,預留動態(tài)信息插槽(組件名稱,組件描述等等)炮捧;
  2. 基于inquirer.js詢問動態(tài)信息庶诡;
  3. 將信息插入模板,渲染至components文件夾下咆课;
  4. 向 components/index.ts 插入導出語句。

我們只需要配置好模板以及問題扯俱,至于詢問以及渲染就交給plop.js吧书蚪。

yarn add plop --dev

新增腳本命令。

package.json

"scripts": {
+ "new": "plop --plopfile ./scripts/plopfile.ts",
},

新增配置文件以及組件模板迅栅,詳情可見:

結語

文章很長殊校,也是我個人學習中的總結,如果本文幫助到了你請給倉庫一顆 ?? 和本文一個贊读存。

如果有錯誤煩請在評論區(qū)指正交流为流,謝謝呕屎。

倉庫地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市敬察,隨后出現(xiàn)的幾起案子秀睛,更是在濱河造成了極大的恐慌,老刑警劉巖莲祸,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹂安,死亡現(xiàn)場離奇詭異,居然都是意外死亡锐帜,警方通過查閱死者的電腦和手機田盈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缴阎,“玉大人允瞧,你說我怎么就攤上這事÷危” “怎么了瓷式?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長语泽。 經(jīng)常有香客問我贸典,道長,這世上最難降的妖魔是什么踱卵? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任廊驼,我火速辦了婚禮,結果婚禮上惋砂,老公的妹妹穿的比我還像新娘妒挎。我一直安慰自己,他們只是感情好西饵,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布酝掩。 她就那樣靜靜地躺著,像睡著了一般眷柔。 火紅的嫁衣襯著肌膚如雪期虾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天驯嘱,我揣著相機與錄音镶苞,去河邊找鬼。 笑死鞠评,一個胖子當著我的面吹牛茂蚓,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼聋涨,長吁一口氣:“原來是場噩夢啊……” “哼晾浴!你這毒婦竟也來了?” 一聲冷哼從身側響起牍白,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤脊凰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后淹朋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笙各,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年础芍,在試婚紗的時候發(fā)現(xiàn)自己被綠了杈抢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡仑性,死狀恐怖惶楼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情诊杆,我是刑警寧澤歼捐,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站晨汹,受9級特大地震影響豹储,放射性物質發(fā)生泄漏。R本人自食惡果不足惜淘这,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一剥扣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铝穷,春花似錦钠怯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宁脊,卻和暖如春断国,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朦佩。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工并思, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人语稠。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仙畦。 傳聞我的和親對象是個殘疾皇子输涕,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345