在線預覽:戳我 ??
本地預覽:
git clone git@github.com:worldzhao/react-ui-library-tutorial.git
cd react-ui-library-tutorial
yarn
yarn start
按順序執(zhí)行完命令后芜辕,即可在 localhost:3000 端口看到以下內容:
概覽
本文包含以下內容:
- 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')],
};
想自行配置的同學可以參考以下文章:
- Linting Your React+Typescript Project with ESLint and Prettier!
- 使用 ESLint+Prettier 規(guī)范 React+Typescript 項目
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 scripts
至 package.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钞速。
明確以下目標:
- 導出類型聲明文件;
- 導出
umd
/Commonjs module
/ES module
等 3 種形式供使用者引入嫡秕; - 支持樣式文件
css
引入渴语,而非只有less
,減少業(yè)務方接入成本昆咽; - 支持按需加載驾凶。
導出類型聲明文件
既然是使用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 模塊
其實完全可以使用babel
或tsc
命令行工具進行代碼編譯處理(實際上很多工具庫就是這樣做的)桑谍,但考慮到還要樣式處理及其按需加載,我們借助 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-env
的useBuiltIns
選項值設置為usage
中跌,同時把node_modules
從babel-loader
中exclude
咨堤,會導致babel
無法檢測到nodes_modules
中所需要的polyfill
。"useBuiltIns: usage" for node_modules without transpiling #9419漩符,在未支持該issue
提到的內容之前一喘,請將useBuiltIns
設置為entry
,或者不要把node_modules
從babel-loader
中exclude
嗜暴。
所以組件庫不用畫蛇添足凸克,引入多余的polyfill
,寫好文檔說明闷沥,比什么都重要(就像zent和antd這樣)萎战。
現(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-runtime
的helper
選項默認為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
導出 ES module
生成ES module
可以更好地進行tree shaking,基于上一步的babel
配置,更新以下內容:
- 配置
@babel/preset-env
的modules
選項為false
,關閉模塊轉換; - 配置
@babel/plugin-transform-runtime
的useESModules
選項為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ū)分esm
和cjs
(執(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
別忘了給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 種預選方案:
- 告知業(yè)務方增加
less-loader
。會導致業(yè)務方使用成本增加靠柑; - 打包出一份完整的
css
文件寨辩,進行全量引入。無法進行按需引入歼冰; -
css in js
方案靡狞; - 提供一份
style/css.js
文件,引入組件css
樣式依賴停巷,而非less
依賴耍攘,組件庫底層抹平差異榕栏。
重點看一看方案 3 以及方案 4畔勤。
css in js
除了賦予樣式編寫更多的可能性之外,在編寫第三方組件庫時更是利器扒磁。
如果我們寫一個react-use
這種hooks
工具庫庆揪,不涉及到樣式,只需要在package.json
中設置sideEffects
為false
妨托,業(yè)務方使用 webpack 進行打包時缸榛,只會打包被使用到的 hooks(優(yōu)先使用 ES module)。
入口文件index.js
中導出的但未被使用的其他 hooks 會被tree shaking
兰伤,第一次使用這個庫的時候我很好奇内颗,為什么沒有按需引入的使用方式,結果打包分析時我傻了敦腔,原來人家天生支持按需引入均澳。
可能常用的antd
以及lodash
都要配一配,導致產(chǎn)生了慣性思維符衔。
回到正題找前。如果將樣式使用javascript
來編寫,在某種維度上講判族,組件庫和工具庫一致了躺盛,配好sideEffects
,自動按需引入形帮,美滋滋槽惫。
而且每個組件都與自己的樣式綁定周叮,不需要業(yè)務方或組件開發(fā)者去維護樣式依賴,什么是樣式依賴界斜,后面會講到则吟。
缺點:
- 樣式無法單獨緩存;
- styled-components 自身體積較大锄蹂;
- 復寫組件樣式需要使用屬性選擇器或者使用
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ā)者不開心。
所以我們要找一個大家都爽的方案:
- 組件開發(fā)者能夠開心的使用預處理器绝骚;
- 業(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,而不會深入單元測試的學習坑夯。
如果你對下列問題感興趣:
- What-單元測試是什么岖寞?
- Why-為什么要寫單元測試?
- How-編寫單元測試的最佳實踐柜蜈?
那么可以看看以下文章:
-
Test React apps with React Testing Library:通過一個
<Counter />
的例子延伸仗谆,闡述了選擇React Testing Library
而非Enzyme
的理由指巡,并對其進行了一些入門教學; -
React Testing Library:
@testing-library/react
的官方文檔隶垮,該庫提供的 API 在某個程度上就是在指引開發(fā)者進行單元測試的最佳實踐藻雪; -
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
的狀態(tài)(即為jest
的except
方法返回值增加更多專注于DOM
的matchers
)字逗; -
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é)主要是講解如何通過一行命令完成以下六點內容:
- 版本更新
- 生成 CHANGELOG
- 推送至 git 倉庫
- 組件庫打包
- 發(fā)布至 npm
- 打 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ā)布,需要自定義配置一些鉤子囊咏。
初始化組件
每次初始化一個組件就要新建許多文件(夾)恕洲,復制粘貼也可,不過還可以使用更高級一點的偷懶方式梅割。
思路如下:
- 創(chuàng)建組件模板霜第,預留動態(tài)信息插槽(組件名稱,組件描述等等)炮捧;
- 基于
inquirer.js
詢問動態(tài)信息庶诡; - 將信息插入模板,渲染至
components
文件夾下咆课; - 向 components/index.ts 插入導出語句。
我們只需要配置好模板以及問題扯俱,至于詢問以及渲染就交給plop.js吧书蚪。
yarn add plop --dev
新增腳本命令。
package.json
"scripts": {
+ "new": "plop --plopfile ./scripts/plopfile.ts",
},
新增配置文件以及組件模板迅栅,詳情可見:
- 配置文件:scripts/plopfile.ts
- 模板文件:templates/component
結語
文章很長殊校,也是我個人學習中的總結,如果本文幫助到了你請給倉庫一顆 ?? 和本文一個贊读存。
如果有錯誤煩請在評論區(qū)指正交流为流,謝謝呕屎。