一些廢話
在前端的飛速發展下,這十年里,前端從美工切圖仔演變成如今的大前端,在互聯網時代中占據越來越重要的位置。前端工程化,模塊化成為前端提效利器。越來越多公司也開始重視,開始搭建適用於公司內部,或者業務線內部的組件庫。這篇文章將遵循 是什么 為什么 怎么做 來一起搭建業務組件庫,或者太長不想看直接看代碼
npm 包主頁
【注意】這里用到的是react react-dom antd都是較新版本,如果你的項目還是老的版本不支持新版本的一些用法可能會出現報錯,請酌情考慮升級項目或降級組件庫依賴。
UI 組件庫有哪些
常見的基於 react 的 antd 組件庫,基於 vue 的 element ,這些都是前端在使用 react 或 vue 的時候首選的 UI 組件庫,這類 UI 組件庫是集成了大部分場景里通用的 UI 組件。
業務組件庫是什么?
基於業務場景和設計規范抽出通用模塊,來形成業務線的組件庫,其實也類似於UI組件庫,只不過是多了一層封裝和自定義,更適合自己的業務場景。封裝一些通用模塊。
比如一個圖片預覽的組件,有單張圖片預覽和多張圖片預覽,這種組件邏輯上和業務不是強關聯的,但是又是全站通用的,就很適合組裝成業務組件,以后一旦遇到這種場景就直接拿來用就行。
為什么要搭建業務組件庫?
一般來說,如果某條業務線下有多個前端項目,他們用的技術棧都是一致的,且設計規范一致,甚至有很多通用的組件,那么就需要在每個項目里都寫組件,寫起來重復,維護起來也麻煩,這個時候就需要抽出一些常見業務場景的通用組件,打包發布到 npm 上,這樣每個項目里只需要通過npm包來引用,修改的時候只需要修改組件庫內容,大大降低維護成本。
怎么搭建業務組件庫?
1. 確定技術棧
看業務線的技術棧是什么,如果是 react+antd 或者是vue+element 或者其他,這些視實際情況而定。本次以 react+antd 為例,手把手一起從零到一搭建業務組件庫。
好了技術棧定好了,作為一個優秀的前端配置工程師,接下來就是開啟配置表演了😺
首先是 package.json 的配置,除了一些常規配置外,作為組件庫,則需要配置打包文件輸出地址
{
"main": "./lib/index.js",
"module": "./lib/index.js",
}
main 這個字段的值是你程序主入口。如果其他用戶需要你的包,當用戶調用require()方法時,返回的就是這個模塊的導出(exports)
module 配置的是以模塊化方式導出,也就是下面 rollup.config.js 文件里 es module 導出,便於 tree-shaking
相當於在一個包內同時發布了兩種模塊規范的版本
如果它已經支持 pkg.module 字段則會優先使用 ES6 模塊規范的版本,這樣可以啟用 Tree Shaking 機制。
如果它還不識別 pkg.module 字段則會使用我們已經編譯成 CommonJS 規范的版本,也不會阻礙打包流程。
2. 配置組件庫文檔
一個優秀的組件庫一定少不了簡潔且可讀性高文檔。
文檔又一定要有代碼和效果示例才算簡單易用。很多根據注釋生成文檔的工具,這里用的是 react-styleguidist
,vue 有 vue-styleguidist
其他還有很多例如 docz storybook 等,看自己怎樣使用習慣啦。
react-styleguidist 的使用
首先安裝支持文檔庫的模塊,由於組件庫是用ts寫的所以還需要裝react-docgen-typescript
yarn add react-docgen-typescript react-styleguidist --dev
or
npm i react-docgen-typescript react-styleguidist --save-dev
安裝完成之后,就是配置了,在項目根目錄下新建 styleguide.config.js
const path = require('path');
const packagePath = path.resolve(__dirname, 'package.json');
const packageFile = require(packagePath);
module.exports = {
title: 'React 業務組件庫',
version: packageFile.version, // 同上 使用 package.json 的 version
usageMode: 'expand', // 自動打開文檔的縮放
pagePerSection: true, // 是否每頁一個組件顯示
styleguideDir: 'dist_docs', // 打包的目錄
components: 'components/**/*.tsx', // 寫入對應目錄的文檔
exampleMode: 'expand', // 表示示例代碼是否展開或者合上文檔中代碼示例的標簽初始化狀態,決定是否展開
webpackConfig: require('./webpack.config'),
propsParser: require('react-docgen-typescript').withCustomConfig('./tsconfig.json').parse, // 用來支持 tsx
verbose: true, // 打印詳細信息
compilerConfig: {
target: { ie: 11 },
transforms: {
modules: false,
// to make async/await work by default (no transformation)
asyncAwait: false,
},
},
updateDocs(docs, file) {
if (docs.doclets.version) {
const version = packageFile.version;
docs.doclets.version = version;
docs.tags.version[0].description = version;
}
return docs;
}, // 在使用 @version 時 使用 package.json 的 version
};
上面是配置文件的內容,注意這里有個webpack的配置,webpackConfig: require('./webpack.config'), 主要是因為並沒有默認支持ts和less的編譯,https://github.com/styleguidist/react-styleguidist/blob/master/styleguide.config.js 這里的配置可以看到只對 jsx 和 css文件進行了加載處理
我們這里如果用到 tsx 和 less ,所以就需要經過處理,webpackConfig: require('./webpack.config'),這個配置就是為了擴展 styleguidist 的 webpack 配置,以及這里要配置antd的按需加載引入,webpack.config.js 文件如下
module.exports = {
entry: './components/index.js', // 這個是組件的總的入口文件,在這個文件里會把所有組件導出
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)?$/,
loader: require.resolve('babel-loader'),
query: {
cacheDirectory: true,
plugins: [['import', { libraryName: 'antd', style: 'css' }]],
},
},
{
test: /\.(css|less)?$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};
還需要注意下為了支持 tsx 還需要配置 tsconfig.json
npm scripts 里對啟動和打包文檔的配置
"scripts": {
"doc": "styleguidist server",
"build_doc": "styleguidist build
}
npm run doc 是開啟本地服務,會默認開啟本地 6060 端口,方便看組件效果,以及注釋生成文檔,npm run build_doc 是打包文檔庫,可以把打包后的文檔庫部署到服務器方便查看。
其他把一些組件需要的一些模塊安裝完成后,就可以開始 npm run doc 開啟本地文檔服務開發調試組件了
文檔打包部署到 github pages上, 可以戳這里查看示例
文檔庫持續集成部署-添加Github Actions持續部署到 github pages 上
用的是這個actions
可以到這里找一些其他人寫好的 actions
3. 打包
文檔打包工具因為是用的是 react-styleguidist
,這個工具用的是 webpack 所以針對文檔的編譯處理就用webpack來實現,具體的文檔打包配置集成到styleguidist命令了
至於業務組件庫的打包輸出則用的是 rollup
選擇使用 rollup 是因為配置簡單,更小巧,默認自動開啟 tree-shaking
rollup的優缺點分別如下
優:
輸出結果更扁平
自動移除未引用代碼
打包結果依然完全可讀
缺:
加載非ESM第三方模塊比較復雜
模塊最終都被打包到一個函數中,無法實現HMR(熱替換)
瀏覽器環境中,代碼拆分功能依賴AMD庫
開發應用 webpack 更全更常見
開發框架類庫 rollup
這里由於是組件庫,優點很有用,缺點可以忽略不計,所以用了rollup
rollup 的打包配置,根目錄下新建 rollup.config.js, 包含一些編譯文件和打包入口文件以及輸出目錄
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import { DEFAULT_EXTENSIONS } from '@babel/core';
const isProd = process.env.NODE_ENV === 'production';
const pkg = require('./package.json');
const dependencies = Object.keys(pkg.peerDependencies);
export default {
input: './components/index.ts', // 入口文件
output: [
{
format: 'umd',
name: pkg.name,
sourcemap: isProd ? false : true,
dir: 'lib',
globals: {
antd: 'antd',
react: 'react',
'react-dom': 'react-dom',
},
},
],
plugins: [
typescript({ include: ['components/**'], lib: ['es5', 'es6', 'dom'], target: 'es5' }),
babel({
exclude: 'node_modules/**',
babelHelpers: 'runtime',
// babel 默認不支持 ts 需要手動添加
extensions: [...DEFAULT_EXTENSIONS, '.ts', 'tsx'],
}),
nodeResolve({
mainField: ['jsnext:main', 'browser', 'module', 'main'],
browser: true,
}),
// 使得 rollup 支持 commonjs 規范,識別 commonjs 規范的依賴
commonjs(),
json(),
postcss({
// Minimize CSS, boolean or options for cssnano.
minimize: isProd,
// Enable sourceMap.
sourceMap: !isProd,
// This plugin will process files ending with these extensions and the extensions supported by custom loaders.
extensions: ['.less', '.css'],
use: [['less', { javascriptEnabled: true }]],
}),
isProd && terser(), // 壓縮js
],
// 指出應將哪些模塊視為外部模塊,如 Peer dependencies 中的依賴
external: dependencies,
};
npm scripts 打包命令
"scripts": {
"build": "rimraf dist && cross-env NODE_ENV=production rollup -c",
"build:watch": "rimraf dist && rollup -c -w", // 監聽文件變化打包
"build:dev": "rimraf dist && rollup -c"
}
到這里遇到一個按需引入的問題,rollup 打包 js 沒啥問題,import { BaseButton } from 'sum-react'
可以按需加載 js,但是到了 css 就不能做到按需加載,必須對css全量引入,在引入這個包的時候要引入所有的css。如果是后續這個庫很大,就會造成不少浪費。比如在某個項目中只想用其中某一個組件,但就一定要引入組件庫的所有css才行。
組件庫的按需加載有兩種思路:一種是分成多入口打包,打包成多個組件,然后借助如 babel-plugin-import 這類babel插件
還有一種就是rollup打包,成js, css注入到js
第一種引入的時候直接 import { BaseButton } from 'sum-react'
經過插件轉換實際引入路徑,從而實現按需引入
可以在 webpack babel中配置,和antd的按需加載保持一致 這種看這里
[
"import",
{
"libraryName": "react_components",
"style": name=>`${name}/index.css`, // 配置組件庫打包后的css路徑 注意如果是.babelrc json文件是不能寫變量的 所以要改成js文件.babelrcjs 然后export導出json就可以
"camel2DashComponentName": false // 關閉駝峰轉換
},
"react_components"
]
第二種無需配置插件,直接使用(建議使用) 這種看這里
import { BaseButton } from 'react_components
4. 提交前檢查、生成log等
最后,代碼規范少不了,結合 eslint prettier 等做代碼規范
lint-staged husky 做 git 提交前檢查
commitizen cz-conventional-changelog 做生成log等
這里就不細講了
5. 根據模板快速生成文件
既然為了工程化,那就要工程化到底,能利用工具快速生成的就絕不手動執行😁
在開發過程中,要新加一個組件的時候需要加一個文件夾,文件夾下面三個文件,還要修改 components 下的默認導出語句,就算是是copy之后再去修改文件名和其他一些命名,這樣也要花上一兩分鍾。
為了節省這個時間,添加一個小型自動化工具 plop 來實現一步到位
首先安裝 plop
yarn add plop --dev
or
npm i plop --save-dev
安裝完成之后,在根目錄下新建 templates 文件夾,下面放三個文件,index.tsx,index.md,index.less 文件里面需要用 {{name}} 替換用到組件名的地方
然后在根目錄下新建 plopfile.js
在 components 下的 index.js 首行添加注釋 // -- APPEND ITEMS HERE -- 這是為了 plop 匹配到這個位置,然后插入特定語句 詳情見下文件
module.exports = (plop) => {
plop.setGenerator('component', {
description: '生成組件',
prompts: [
{
type: 'input',
name: 'name',
message: '請輸入組件名?',
},
],
actions: [
{
type: 'add',
path: 'components/{{name}}/index.less',
templateFile: 'templates/index.less',
},
{
type: 'add',
path: 'components/{{name}}/index.md',
templateFile: 'templates/index.md',
},
{
type: 'add',
path: 'components/{{name}}/index.tsx',
templateFile: 'templates/index.tsx',
},
{
type: 'append',
path: 'components/index.js',
pattern: /(\/\/ -- APPEND ITEMS HERE --)/gi,
template: "export { default as {{name}} } from './{{name}}' ",
},
],
});
};
好了,到這里一個簡單的工具就完成了,添加組件的時候只需要執行命令,就能生成想要的組件了,不需要做其他額外操作,只需要在組件里開始開發了
yarn plop <ComponentName>
or
npx plop <ComponentName>
6. 添加單元測試
用 Enzyme 給組件添加單元測試,示例
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import BaseButton from '../components/BaseButton';
import BaseModal from '../components/BaseModal';
Enzyme.configure({ adapter: new Adapter() });
describe('BaseButton', () => {
const handleClick = jest.fn();
const buttonwrapper = shallow(<BaseButton onClick={handleClick}>hello</BaseButton>);
const modalwrapper = Enzyme.shallow(
<BaseModal>
<p>Some contents...</p>
</BaseModal>,
);
it('should have rendered BaseButton', () => {
expect(buttonwrapper).toMatchSnapshot();
});
it('should have rendered BaseModal', () => {
expect(modalwrapper).toMatchSnapshot();
});
it('should excute click event', () => {
buttonwrapper.find('.base-btn').simulate('click');
expect(handleClick).toBeCalled();
});
});
運行yarn test
進行單測
7. 體驗demo
安裝npm 包
yarn add sum-react
or
npm install sum-react
組件里使用
import React from 'react';
import { BaseButton, BaseModal } from 'sum-react'
function App() {
const [visible, setVisible] = React.useState(false);
const showModal = () => {
console.log('showmodal');
setVisible(true);
};
const closeModal = () => {
console.log('closemodal');
setVisible(false);
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<BaseButton onClick={showModal}>點擊我展示彈窗</BaseButton>
<BaseModal title="Basic Modal" visible={visible} onOk={closeModal} onCancel={closeModal}>
<p>Some contents...</p>
</BaseModal>
</header>
</div>
);
}
export default App;
8. 注意點
組件庫可以多入口打包,也可全部打包到一個文件,兩種打包方式區別
x.d.ts為生成的 ts 聲明文件
打包方式 | 打包結果 | 優缺點 | sum-react版本 | 代碼地址 |
---|---|---|---|---|
單入口打包 | ![]() |
優點:打包后的結果是一個 js 文件,css 樣式等都注入到 js 了,按需引入無需使用插件。 缺點:只能在組件庫里定義主題樣式,因為打包后的結果已經是 css 非 less,如果在項目里通過 less 修改主題色會不成功 | 最新版本 | https://github.com/leitingting08/sum-react |
多入口打包 | ![]() |
優點:可以按需引入的時候使用 less,在項目里通過 less modifyVars 動態修改主題色。 缺點:按需引入需要使用babel-plugin-import插件,組件庫里樣式需要導入用到的 antd 組件的less,比較繁瑣 | 0.4.0 | https://github.com/leitingting08/sum-react/tree/release-0.0.1 (babel-plugin-import的使用方式附readme) |
9. 發布到 npm
最后業務組件庫寫好了一些組件,就可以發布到 npm 了,執行 npm 的發布流程就好了
如果是發布到公司內部的私有 npm
需要在 package.json 中配置registry
"publishConfig": {
"registry": "http://xxx"
},
登錄到npm 之后執行 npm publish 就能把自己的包發布到 npm 上,在項目中就可以愉快的通過 npm 安裝使用了
至此,一個簡單的業務組件庫框架就搭建完成了,后續可以根據需求和業務,往庫里不斷填充組件,后續可給組件添加組件單元測試,完善組件庫~
完整代碼在 這里
有問題歡迎提出~