本系列目錄如下:
- 專案初始化搭建+程式碼規範整合
- 元件庫多產物編譯及文件編寫
上篇文章我們將元件庫的基本結構和規範進行了整理,本篇的核心基本全在 components 資料夾下
本篇的打包參考了文章 https://github.com/worldzhao/blog/issues/5 ,強烈建議閱讀一下此文章,而且討論區也能讓你收穫良多。
本章節分成了兩次 commit
- 打包 es 和 lib 以及樣式:https://github.com/json-q/rc-library-templete/commit/c9d8a53c26d1189cd05939f57d07b07bfd2c1642
- 初始化 storybook 測試打包產物:https://github.com/json-q/rc-library-templete/commit/1af4b5316ee2da0ed4febc69e28c19e166d6b5c9
安裝元件開發依賴
react 和 react-dom 依賴整合
package.json
檔案的 peerDependencies 寫入 react react-dom
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
執行 pnpm install
為什麼是 peerDependencies
而不是 devDependencies
?
peerDependencies
可以約束使用該包的宿主環境,控制其相容依賴的版本在指定範圍內,而 devDependencies
則是純粹的開發依賴
- 安裝
react
和react-dom
的 types 型別包
pnpm i @types/react @types/react-dom -D
clsx
安裝 clsx
作為樣式開發的庫,其它用到了再安裝即可
pnpm i clsx -D
編寫 less 樣式
- 在
componets/src
下新建style
資料夾 - 新建
index.less
作為入口檔案 - 新建
normalize.less
重置樣式,直接去 arco design 倉庫 複製下來即可。 style
資料夾下新建themes
資料夾,在themes
下新建default.less
,宣告一些預設 less 變數
// packages/components/src/style/themes/default.less
@prefix: rclt;
@font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
PingFang SC,
Hiragino Sans GB,
noto sans,
Microsoft YaHei,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
@font-size-body: 14px;
@line-height-base: 1.5715;
- 在
index.less
中引入default.less
和normalize.less
@import './themes/default.less';
@import './normalize.less';
此時的檔案結構應該是這樣的
- packages
- components
- src
- style
- themes
- default.less
- index.less
- normalize.less
寫這麼多是為了在後續編譯 less 樣式時能看出明顯的效果。
編寫測試元件 Button
程式碼我就不列出來了,大家可以自行編寫,或者去倉庫看也可以,就是一個簡單的示例元件,編寫完成後,目錄結構如下
- 由於樣式後續要做按需匯入,所以必須確保每個元件的樣式 style 有一個匯出的 index.(ts|js) 檔案和 index.less 檔案
- index.(ts|js) 的內容就是
@import xxx.less
在 src/index.ts
中統一匯出元件
export type { ButtonProps } from './button/interface';
export { default as Button } from './button';
打包編譯
- 匯出型別宣告檔案
- 匯出 umd esmodule commonjs 3 種形式產物供使用者引入;
- 支援樣式檔案 css 引入,而非只有 less,減少使用者的接入成本;
- 支援按需載入。
匯出型別宣告檔案
tsconfig.json
上一節已經寫好了,直接使用 tsc 編譯即可。
執行命令之前需按照 cpr
,執行 pnpm i cpr -D
"scripts": {
"build:types": "tsc -p tsconfig.json --outDir es && cpr es lib"
},
這個命令就是單純用 tsc 編譯出了型別 d.ts
檔案匯出到 es
資料夾裡,cpr es lib
就是將 es 中的 d.ts
檔案複製到了 lib
目錄下,此時執行 pnpm build:types
即可看到生成這兩個資料夾
其實大部分 npm 包需要做的都是編譯,打包基本用不到。
匯出 commonjs 程式碼
打包這個工作,rollup
也可以,不過我們是編譯,不是打成 bundler,而且還要處理 css,rollup
就不是很容易處理了。如果是直接打 bundler 的話,rollup
更簡單,而且 webpack
在這方面也很在行。
babel 相關配置
安裝 babel 及其相關依賴,這一部分解釋性的話術引用自開頭推薦的文章內容。
pnpm i @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime -D
pnpm i @babel/runtime-corejs3
新建 .babelrc.js
,寫以下內容
module.exports = {
presets: ['@babel/env', '@babel/typescript', '@babel/react'],
// @babel/plugin-transform-runtime 的 helper 選項預設為 true
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
helpers: true,
},
],
],
};
關於 @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
。(該檔案最終只會作用於 css,後續會有相關解釋)
> 1%
last 2 versions
Firefox ESR
not dead
IE 11
not IE 10
polyfill 相關思考
很遺憾的是,@babel/runtime-corejs3
無法在按需引入的基礎上根據目標瀏覽器支援程度再次減少 polyfill 的引入。
這意味著 @babel/runtime-corejs3
甚至會在針對現代引擎的情況下注入所有可能的 polyfill:不必要地增加了最終捆綁包的大小。
對於元件庫(程式碼量可能很大),建議將 polyfill 的選擇權交還給使用者,在宿主環境進行 polyfill
。若使用者具有相容性要求,自然會使用 @babel/preset-env
+ core-js
+ .browserslistrc
進行全域性 polyfill,這套組合拳引入了最低目標瀏覽器不支援 API 的全部 polyfill。
所以元件庫不用畫蛇添足,引入多餘的 polyfill,寫好文件說明,比什麼都重要。
現在 @babel/runtime-corejs3
更換為 @babel/runtime
,只進行 helper 函式抽離。
pnpm uni @babel/runtime-corejs3
pnpm i @babel/runtime
.babelrc.js
module.exports = {
presets: ['@babel/env', '@babel/typescript', '@babel/react'],
// @babel/transform-runtime 的 helper 選項預設為 true
plugins: ['@babel/plugin-transform-runtime'],
};
gulp 任務編排
安裝 gulp 相關依賴
pnpm i gulp gulp-babel @types/gulp @types/gulp-babel -D
注意: gulp-babel
目前存在問題,當 .browserslistrc
內不支援 IE 時會報錯,詳情可見 issue
新建 gulpfile.js
,寫入以下內容
const gulp = require('gulp');
const babel = require('gulp-babel');
const paths = {
dest: {
lib: 'lib',
esm: 'es',
dist: 'dist',
},
compileStyles: 'src/**/index.less', // 編譯樣式的入口檔案,後續會解釋為什麼樣式處理分成兩部分
copyStyles: 'src/**/*.less', // 樣式檔案路徑
scripts: ['src/**/*.{ts,tsx,js,jsx}'], // 指令碼檔案路徑
};
function compileCJS() {
const { dest, scripts } = paths;
return gulp
.src(scripts)
.pipe(babel())
.pipe(gulp.dest(dest.lib));
}
// 並行任務 後續加入樣式處理 可以並行處理
const build = gulp.parallel(compileCJS);
exports.build = build;
exports.default = build;
有 eslint 報錯,不允許 require
匯入,去 eslint.config.mjs
關閉一下
rules: {
// ...
'@typescript-eslint/no-require-imports': 'off',
},
去 package.json
新增指令碼命令 clean:build
build
"scripts": {
"build:types": "tsc -p tsconfig.json --outDir es && cpr es lib",
"clean:build": "rimraf lib es dist",
"build": "npm run clean:build && npm run build:types && gulp"
},
由於 rimraf
基本子包都要使用,就安裝的根目錄的依賴下全域性共享
pnpm i rimraf -D -w
然後執行命令 pnpm build
,就能在 lib 目錄下看到 commonjs 的程式碼了,且諸多 helper 方法已被抽離至 @babel/runtime
中
匯出 ESM 程式碼
修改 babel 配置
為了讓 ES Module 更好的支援 Tree Shaking,需要對 babel 配置做一些改動
- 關閉
@babel/preset-env
對模組語法的轉換,即設定modules
為 false - 但是又需要只針對 esm,所以就根據環境變數做一個區分,esm 環境下才關閉(當任務執行時,設定當前的執行環境即可)
module.exports = {
presets: ['@babel/env', '@babel/typescript', '@babel/react'],
// @babel/plugin-transform-runtime 的 helper 選項預設為 true
plugins: ['@babel/plugin-transform-runtime'],
env: {
esm: {
presets: [['@babel/env', { modules: false }]],
},
},
};
gulp 新增 esm 構建任務
esm 和 cjs 都走 babel 編譯,流程基本一致,可以將編譯方法抽離出去,兩個任務共用
// ...
/**
* 編譯指令碼檔案
* @param {("esm"|"cjs")} babelEnv babel環境變數
* @param {String} destDir 目標目錄
*/
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(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);
}
// 序列執行編譯指令碼任務(cjs,esm) 避免環境變數影響 gulp.series(compileCJS, compileESM)
// 並行任務 後續加入樣式處理 可以並行處理 gulp.parallel(...)
const build = gulp.parallel(gulp.series(compileCJS, compileESM));
// ...
執行 pnpm build
,觀察 es 目錄下的編譯結果,都是 import
的 esm 寫法
Less 樣式處理
- 瞭解流行元件庫的樣式為什麼會是那樣的結構
- 模仿流行元件庫的樣式結構進行編譯,實現樣式的按需載入
為什麼是 less?而非 sass 或 css in js?
- 個人更對 less 熟悉一些,所以是 less 而不是 sass
- 對於 cssinjs,個人不喜歡
- 好處就是定製化極為方便,而且由於是 js,完全不用在編譯時處理樣式,自動帶有
tree shaking
,文章後續對樣式的編譯處理什麼的統統沒有 - 被人詬病的就是效能,雖說拋開劑量談毒量都是扯淡,但真拿劑量說事兒時一問一個不吱聲。
- 此處就稍微吐槽一下
antd5
,cssinjs 效能做得真的很有問題,元件文件開啟速度相對於 v4 來說不知道慢了多少倍(載入時大量的 style 標籤動態插入),issue 裡也經常有人反映效能問題。 - 雖說有些零執行時的 cssinjs,但是那些隨機類名個人看著也確實不舒服。
- 好處就是定製化極為方便,而且由於是 js,完全不用在編譯時處理樣式,自動帶有
瞭解流行元件庫的樣式打包構成
這裡就舉例國內採用同樣技術的元件庫:antd 4.x、arco design、tdesign ,可以參考一下他們的打包後的結構目錄
可以看到以上元件庫的樣式結構基本都一樣(tdesign 直接使用 cssvar
,但是對外提供了 less 能力),提供了 index.js
(內部是 less 檔案的匯入)、css.js
(內部是 css 檔案的匯入)index.css
(內部是合併後的純 css)以及原樣的 less 檔案
為什麼要做的這麼麻煩,給使用者提供這麼多種格式的 css/less 樣式檔案?元件庫底層抹平差異,改善使用者體驗。
- 提供 less 檔案是為了給使用 less 的使用者提供主題定製的能力(變數覆蓋)
- 提供 css 是為了相容非 less 使用者的使用,可以直接匯入 css 而無需額外裝
less-loader
,屬於 dx 最佳化。 - 靈活的樣式型別拆分可以給開發者更多的選擇,選擇 less 還是 css 進行開發都是可以的。
- 拆分成多種型別的入口檔案,還可以讓使用者做按需匯入,
- 由於要靈活配置,所以開發的元件內部是不能直接匯入樣式的(不然的話就是寫死樣式,就不存在多個樣式型別的引入方式),樣式匯入交給使用者去做
複製 less 檔案至打包目錄
將上述的 less 打包結構理解之後,就可以按照這種結構開始打包樣式了。
將開發中使用的 less 檔案複製至 npm 包中,使用者使用時,就可以按需引入 less 檔案,也可以做 less 變數的覆蓋。
在 gulpfile.js
中新建 copyLess
任務
/**
* 複製less檔案
*/
function copyLess() {
return gulp.src(paths.copyStyles)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
// gulp.parallel 的 args 是同時執行,gulp.series 的 args 是一個執行完畢執行下一個
const build = gulp.parallel(gulp.series(compileCJS, compileESM), copyLess);
可以看到 less 樣式已經按照原來的結構 copy 到 es 和 lib 包中,然後就是生成 css 的步驟。
less 編譯成 css
安裝相關依賴,gulp-less
將 less 編譯成 css,gulp-autoprefixer
新增 css 字首,由於之前設定了 .browserslistrc
,gulp-autoprefixer
會自動識別相容的版本去新增字首
pnpm i gulp-less gulp-autoprefixer@^8 @types/gulp-less @types/gulp-autoprefixer -D
必須安裝 gulp-autoprefixe
8.x,9.x 只支援 esm 匯入。
在 gulpfile.js
中新增編譯方法
/**
* 生成css檔案
*/
function less2css() {
return gulp
.src(paths.compileStyles)
.pipe(less()) // 編譯 less 檔案
.pipe(autoprefixer()) // 根據 browserslistrc 增加字首
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
const build = gulp.parallel(gulp.series(compileCJS, compileESM), copyLess, less2css);
執行 pnpm build
,檢查打包檔案,如圖所示就是成功的。
這裡沒有對 css 進行壓縮,esm 和 lib 會被使用者以 npm 方式使用,使用者打包時,自然會對 css 進行壓縮。
為什麼只對 index.less
進行編譯?
- 每個元件的樣式都需要一個合併起來的入口檔案,這個檔案裡引入了該元件所需的所有 less 樣式,方便開發者匯入。
- 如果每個 less 都進行編譯,那
index.less
編譯出來的就是該元件所有的 css,然後其餘的拆分元件也會再編譯對應的 css,相當於重複編譯了index.less
編譯後具有全量的該元件 cssindex.less
引用了a.less
,a.less
再次被編譯成 css,這個是沒有意義的,反而造成了重複編譯- 最大的問題是,如果所有
less
都進行編譯,在編譯完index.less
後,會去單獨編譯其它的 less 檔案,如果這些檔案內使用了的 less 變數是透過index.less
間接引入的,而gulp-less
將其視為獨立檔案,就會產生 less 變數未定義的錯誤。
如果以上文字解釋看不太明白,可以看如下舉例:
// var.less
@font-size: 14px
// component.less
.cp{
font-size: @font-size;
}
// index.less
@import "./var.less";
@import "./component.less";
這種情況下,component.less
使用了 var.less
的變數,但檔案內沒有直接匯入 var.less
,而是使用 index.less
做了間接使用,所以 component.les
是不具備獨立編譯的能力的。
如果全部 less 檔案都做編譯
gulp-less
在編譯index.less
時,由於有引入順序,相當於把var.less
和component.less
做了合併,這種是完全沒問題的,可以正常編譯。gulp-less
編譯完index.less
,再去編譯component.less
,發現內部有一個未知的 less 變數,因為此時gulp-less
不透過index.less
這個橋樑走,而是直接編譯component.less
,那自然會報錯。
生成 css.js
生成 css.js 讓不安裝 less 外掛的使用者也可以正常使用。
功能實現參考 antd-tools,由於 antd5 現在不使用 less 了,就直接找到之前的 commit 把程式碼貼出來了,如下圖所示
這段程式碼做的就是匹配到 style/index.js
時,生成 style/css.js
,並透過正則將檔案內容中引入的 less 檔案字尾改成 css。
- 安裝
through2
pnpm i through2 -D
對 compileScripts
進行補充
/**
* 編譯指令碼檔案
* @param {("esm"|"cjs")} babelEnv babel環境變數
* @param {String} destDir 目標目錄
*/
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel())
.pipe(
through2.obj(function (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
實現,還是在 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');
}
執行 pnpm build
,即可看到 css.js
檔案
其實這一部分很多可以最佳化的地方,做的更細緻一點,大家在看懂之後可以自行嘗試最佳化,比如
token.css
並不存在(token.less
未被編譯),可以去掉,再比如可以把 less 變數編譯 cssvar,css.js
可以再額外引入 css 變數,就可以做到動態換膚功能。
按需載入
實際上只要結構上寫出來,按需載入的核心就已經完成了。
在 package.json
中增加 sideEffects
屬性,配合 ES module 達到 tree shaking
效果(將樣式依賴檔案標註為side effects,避免被誤刪除)。
cssinjs 的庫都不需要這個,因為 cssinjs 只有 js,天然支援 tree shaking
。
"sideEffects": [
"dist/*",
"es/**/style/*",
"lib/**/style/*",
"**/*.less"
],
好,此時按需載入的步驟就已經完成了,大家如果用 webpack,可以藉助 babel-plugin-import
實現按需匯入樣式,如果是 vite,可以使用 vite-plugin-imp
以上內容作為一次 commit 暫存一下:https://github.com/json-q/rc-library-templete/commit/c9d8a53c26d1189cd05939f57d07b07bfd2c1642
整理 package.json 的入口以及匯出模組的方式和指向
{
"main": "lib/index.js",
"module": "es/index.js",
"types": "es/index.d.ts",
"files": [
"dist",
"es",
"lib"
],
"exports": {
".": {
"types": "./es/index.d.ts",
"require": "./lib/index.js",
"import": "./es/index.js"
},
"./es/*": "./es/*",
"./lib/*": "./lib/*",
"./dist/*": "./dist/*"
},
"sideEffects": [
"dist/*",
"es/**/style/*",
"lib/**/style/*",
"**/*.less"
],
}
除了 sideEffects
之外,其它的簡單介紹一下:
files
就是釋出到 npm 時包含的檔案main
、module
、types
分別指向不同環境下的不同包,types
是在 TS 環境下的地址指向exports
欄位內和main
、module
、types
作用差不多,只不過可以更細粒度的去區分
其實 exports
是用來替代 @babel/plugin-transform-runtime
的 useESModules
的(對匯出這塊我也不是很熟,同樣是查資料和摸索出來的)
而後邊又新增了 ./es/*": "./es/*
等的指向,是因為我在使用按需載入的過程中發現外掛無法從 ./es
中讀取檔案,找不到檔案路徑,而且編輯器無法給出路徑提示(看樣子確實是沒有指定到檔案下),所以才有了這一系列的指向,大家可以去掉之後自行嘗試一下。
新增元件實時編譯能力
本地使用元件庫時,當修改了元件想要看到最新效果,就只能重新 pnpm build
打包,這顯然很不方便,好在 gulp
提供了相關的實時編譯支援。
gulpfile.js
// 監視 src 目錄下的檔案變化
function watchFiles() {
gulp.watch('src/**/*', build);
}
exports.watch = watchFiles;
package.json
新增 dev 命令
"scripts": {
"dev": "gulp watch",
// ...
},
後續執行 pnpm dev
,當 src 下的元件發生變化,就會自動重新編譯
安裝 storybook 驗證打包成果
上一章節我們把 monorepo 的基本檔案結構搭建好了,storybook 是直接處於根目錄下的子包,此時在該目錄下執行初始化命令,暫時不要選擇 Next
,因為 SSR 環境下目前不確定是否會發生意外情況
pnpm dlx storybook@latest init
安裝之後會自動開啟頁面,這個我們暫時先不關心,因為我們是測試元件的打包,所以先安裝元件依賴
pnpm add rclt-components --workspace
storybook 分兩個執行介面,一個是 vite + react 預設的模板頁面,就是 src
下的檔案,這個也可以作為自己的元件庫文件進行開發。一個是根據 *.storeis.(tsx|...)
生成的 storybook 文件。
測試元件可用性
在 App.tsx
中匯入元件
執行 pnpm dev
,開啟即可看到一個很醜的按鈕 button,因為沒有樣式,接下來我們做樣式的按需匯入
測試樣式的按需載入
安裝 vite-plugin-imp
pnpm i vite-plugin-imp -D
在 vite.config.ts
中增加配置
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'node:url';
import vitePluginImp from 'vite-plugin-imp';
export default defineConfig({
resolve: {
// sb 打包時,由於元件庫為本地目錄,sb 找不到路徑會打包錯誤,需使用 alias 指向正確路徑
alias: {
'rclt-components': fileURLToPath(new URL('../packages/components', import.meta.url)),
},
},
plugins: [
react(),
vitePluginImp({
libList: [
{
libName: 'rclt-components',
style: (name) => `rclt-components/es/${name}/style/index.css`,
},
],
}),
],
});
此時一個漂亮的按鈕就成功出來了
這麼看來在專案結構搭建時可以把
storybook
作為根目錄作為專案共享的配置,這樣可以直接在components
資料夾下寫 story 文件,結構上更方便檢視
編寫文件
storybook 的約定檔案格式可以在 main.ts
中看到,我們把它進行稍加修改
const config: StorybookConfig = {
docs: {
autodocs: true,
},
stories: ['../src/**/index.stories.@(js|jsx|mjs|ts|tsx)'],
};
autodocs
就是用來全域性配置 stories.ts 中的tags:["autotag"]
stories
的路徑現在只匹配index.stories
,是為了當文件示例過多時,以index.stories
作為入口,其餘可以匯入進來,條理更清晰一點。
刪除 stories
下的所有檔案,新建 button 資料夾,新建 Button 元件的 story 文件
// storybook/stories/button/index.stories.tsx
import { Button } from 'rclt-components';
import type { Meta, StoryObj } from '@storybook/react';
// export {OtherButton} from "./source/other-button.store"
const meta = {
title: '基礎元件/Button',
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ButtonType: Story = {
name: '按鈕型別',
render: () => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button type="default">Default Button</Button>
<Button type="primary">Primary Button</Button>
<Button type="danger">Danger Button</Button>
</div>
),
};
export const ButtonSize: Story = {
name: '按鈕大小',
render: () => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button type="primary" size="small">
Small Button
</Button>
<Button type="primary">Default Button</Button>
<Button type="primary" size="large">
Large Button
</Button>
</div>
),
};
執行 pnpm storybook
,執行 storybook 文件
樣式的按需載入也被 storybook 文件享受到了,不需要單獨匯入 css。
打包 umd 格式的元件 bundler
umd 可以直接在瀏覽器環境使用,各大元件庫基本都提供有 umd 格式的元件產物。
這裡使用的是 webpack 打包 umd,沒有選擇 rollup,大家根據喜好選擇即可,這個比較簡單。
webpack 打包 bulder
安裝 webpack 相關依賴和打包用到的 loader:
pnpm i webpack webpack-cli webpack-merge terser-webpack-plugin babel-loader ts-loader -D
在 components
下新建 webpack
資料夾,思路如下
- dist 包提供兩個 bundler 產物,一個壓縮過的
min.js
和未壓縮的.js
- dist 包提供兩個 css 產物,一個壓縮過的
min.css
和未壓縮的.css
在 webpack
下新建三個檔案 webpack.common.js
、webpack.dev.js
、webpack.prod.js
在 webpack.common.js
寫入以下內容
const path = require('path');
/** @type {import("webpack").Configuration} */
module.exports = {
bail: true,
// devtool: 'source-map',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
entry: {
index: path.resolve(__dirname, '../src/index.ts'),
},
output: {
// filename: 'jwstwe-ui.min.js',
path: path.join(__dirname, '../dist'),
library: 'rclt',
libraryTarget: 'umd',
},
module: {
rules: [
{
test: /.js(x?)$/,
use: [{ loader: 'babel-loader' }],
exclude: /node_modules/,
},
{
test: /.ts(x?)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
exclude: /node_modules/,
},
],
},
// 元件庫不直接整合 react 和 react-dom
externals: {
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'react-dom': {
root: 'ReactDOM',
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
},
},
};
webpack.dev.js
實際上就是來打包未壓縮版本的產物
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
/** @type {import("webpack").Configuration} */
const devConfig = {
mode: 'development',
output: {
filename: 'rclt.js',
},
};
module.exports = merge(devConfig, commonConfig);
webpack.prod.js
打包壓縮版本的產物
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
const commonConfig = require('./webpack.common');
/** @type {import("webpack").Configuration} */
const prodConfig = {
mode: 'production',
output: {
filename: 'rclt.min.js',
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
module.exports = merge(prodConfig, commonConfig);
在 package.json
新增命令 build:dist
命令
"scripts": {
// ...
"build:dist": "webpack --config ./webpack/webpack.dev.js && webpack --config ./webpack/webpack.prod.js",
// ...
},
執行 pnpm build:dist
,就可以看到生成了 dist
資料夾
細節最佳化
- webpackbar 打包進度條
- case-sensitive-paths-webpack-plugin 檔案大小寫敏感檢測
pnpm i webpackbar case-sensitive-paths-webpack-plugin @types/case-sensitive-paths-webpack-plugin -D
在 webpack.common.js
中新增
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const WebpackBarPlugin = require('webpackbar');
/** @type {import("webpack").Configuration} */
module.exports = {
// ...
plugins: [new CaseSensitivePathsPlugin(), new WebpackBarPlugin()],
};
打包 css
webpack
就算安裝 less-loader
,想打包出 css,也是完全沒用的,因為 less 樣式是完全獨立的,元件內部不引入樣式,webpack
在打包時,完全找不到使用的 less,自然就打包不出來。
其實也是因為有更簡單的方法。
之前是怎麼編譯 css 的?gulp 使用 gulp-less
將 less 編譯成 css 分別輸出到產物目錄下,那也可以順便再生成一下 dist
的 css
先安裝 gulp-concact
合併檔案, gulp-cleaner-css
壓縮 css(是 gulp-clean-css
的一個維護分支)
pnpm i gulp-concact @types/gulp-concact gulp-cleaner-css -D
完善 gulpfile.js
/**
* 生成css檔案
*/
function less2css() {
return gulp
.src(paths.compileStyles)
.pipe(less()) // 編譯 less檔案
.pipe(autoprefixer()) // 根據browserslistrc增加字首
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm))
.pipe(concat('rclt.css'))
.pipe(gulp.dest(paths.dest.dist))
.pipe(cleanCSS()) // 壓縮 CSS
.pipe(concat('rclt.min.css'))
.pipe(gulp.dest(paths.dest.dist)); // 輸出壓縮版到 dist
}
此時 dist 的 css 也可以生成了。回到 package.json
,將 build
命令裡新增 build:dist
"scripts": {
"dev": "gulp watch",
"build": "npm run clean:build && npm run build:types && gulp && npm run build:dist",
"build:types": "tsc -p tsconfig.json --outDir es && cpr es lib",
"build:dist": "webpack --config ./webpack/webpack.dev.js && webpack --config ./webpack/webpack.prod.js",
"clean:build": "rimraf lib es dist"
},
執行 pnpm build
,如圖所示,就大功告成了
測試 dist
新建一個 test-dist.html
,引入 react、react-dom、babel 的 umd 連結
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./packages/components/dist/rclt.min.css" />
</head>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin></script>
<script src="./packages/components/dist/rclt.min.js"></script>
<script type="text/babel">
const { Button } = rclt;
function App() {
return (
<div style={{ display: 'flex', gap: '10px' }}>
<Button type="default">Button</Button>
<Button type="primary">Button</Button>
<Button type="danger">Button</Button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</html>
元件庫的打包到此結束!