重頭戲來了。css
宿主環境各不相同,須要將源碼進行相關處理後發佈至 npm。node
明確如下目標:react
umd
/Commonjs module
/ES module
等 3 種形式供使用者引入css
引入,而非只有less
本節全部代碼可在倉庫chapter-3分支中獲取。webpack
既然是使用typescript
編寫的組件庫,那麼使用者應當享受到類型系統的好處。git
咱們能夠生成類型聲明文件,並在package.json
中定義入口,以下:github
package.jsonweb
{
"typings": "types/index.d.ts", // 定義類型入口文件
"scripts": {
"build:types": "tsc --emitDeclarationOnly" // 執行tsc命令 只生成聲明文件
}
}
複製代碼
執行yarn build:types
,能夠發現根目錄下已經生成了types
文件夾(tsconfig.json
中定義的outDir
字段),目錄結構與components
文件夾保持一致,以下:typescript
typesnpm
├── alert
│ ├── alert.d.ts
│ ├── index.d.ts
│ ├── interface.d.ts
│ └── style
│ └── index.d.ts
└── index.d.ts
複製代碼
這樣使用者引入npm
包時,便能獲得自動提示,也可以複用相關組件的類型定義。json
接下來將ts(x)
等文件處理成js
文件。
須要注意的是,咱們須要輸出
Commonjs module
以及ES module
兩種模塊類型的文件(暫不考慮umd
),如下使用cjs
指代Commonjs module
,esm
指代ES module
。
對此有疑問的同窗推薦閱讀:import、require、export、module.exports 混合詳解
其實徹底可使用babel
或tsc
命令行工具進行代碼編譯處理(實際上不少工具庫就是這樣作的),但考慮到還要處理樣式及其按需加載,咱們藉助 gulp
來串起這個流程。
首先安裝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
函數(classCallCheck
,extends
等),減少生成的代碼體積;corejs
設置爲3
,可引入不污染全局的按需polyfill
,經常使用於類庫編寫(我更推薦:不引入polyfill
,轉而告知使用者須要引入何種polyfill
,避免重複引入或產生衝突,後面會詳細提到)。更多參見官方文檔-@babel/plugin-transform-runtime
配置目標環境
爲了不轉譯瀏覽器原生支持的語法,新建.browserslistrc
文件,根據適配需求,寫入支持瀏覽器範圍,做用於@babel/preset-env
。
.browserslistrc
>0.2%
not dead
not op_mini all
複製代碼
很遺憾的是,@babel/runtime-corejs3
沒法在按需引入的基礎上根據目標瀏覽器支持程度再次減小polyfill
的引入,參見@babel/runtime for target environment 。
這意味着@babel/runtime-corejs3
甚至會在針對現代引擎的狀況下注入全部可能的 polyfill
:沒必要要地增長了最終捆綁包的大小。
對於組件庫(代碼量可能很大),我的建議將polyfill
的選擇權交還給使用者,在宿主環境進行polyfill
。若使用者具備兼容性要求,天然會使用@babel/preset-env + core-js + .browserslistrc
進行全局polyfill
,這套組合拳引入了最低目標瀏覽器不支持API
的所有 polyfill
。
將
@babel/preset-env
的useBuiltIns
選項值設置爲usage
,同時把node_modules
從babel-loader
中exclude
掉的同窗可能想要這個特性:"useBuiltIns: usage" for node_modules without transpiling #9419,在未支持該issue
提到的內容以前,仍是乖乖地將useBuiltIns
設置爲entry
,或者不要把node_modules
從babel-loader
中exclude
。
因此組件庫不用多此一舉,引入多餘的polyfill
,寫好文檔說明,比什麼都重要(就像zent和antd這樣)。
如今@babel/runtime-corejs3
更換爲@babel/runtime
,只進行helper
函數抽離。
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
相關依賴
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));
}
// 並行任務 後續加入樣式處理 能夠並行處理
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 types lib esm dist",
+ "build": "npm run clean && npm run build:types && gulp",
...
},
}
複製代碼
執行yarn build
,獲得以下內容:
lib
├── alert
│ ├── alert.js
│ ├── index.js
│ ├── interface.js
│ └── style
│ └── index.js
└── index.js
複製代碼
觀察編譯後的源碼,能夠發現:諸多helper
方法已被抽離至@babel/runtime
中,模塊導入導出形式也是commonjs
規範。
lib/alert/alert.js
生成ES module
能夠更好地進行tree shaking,基於上一步的babel
配置,更新如下內容:
@babel/preset-env
的modules
選項爲false
,關閉模塊轉換;@babel/plugin-transform-runtime
的useESModules
選項爲true
,使用ES module
形式引入helper
函數。.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
},
],
],
};
複製代碼
目標達成,咱們再使用環境變量區分esm
和cjs
(執行任務時設置對應的環境變量便可),最終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環境變量 * @param {string} destDir 目標目錄 */
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
// 設置環境變量
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);
}
// 串行執行編譯腳本任務(cjs,esm) 避免環境變量影響
const buildScripts = gulp.series(compileCJS, compileESM);
// 總體並行執行任務
const build = gulp.parallel(buildScripts);
// ...
複製代碼
執行yarn build
,能夠發現生成了types
/lib
/esm
三個文件夾,觀察esm
目錄,結構同lib
/types
一致,js 文件都是以ES module
模塊形式導入導出。
esm/alert/alert.js
別忘了給package.json
增長相關入口。
package.json
{
+ "module": "esm/index.js"
}
複製代碼
咱們會將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
目錄,能夠發現 less
文件已被拷貝至alert/style
目錄下。
lib
├── alert
│ ├── alert.js
│ ├── index.js
│ ├── interface.js
│ └── style
│ ├── index.js
│ └── index.less # less文件
└── index.js
複製代碼
可能有些同窗已經發現問題:若使用者沒有使用less
預處理器,使用的是sass
方案甚至原生css
方案,那現有方案就搞不定了。經分析,有如下 3 種預選方案:
less-loader
;css
文件,進行全量引入;style/css.js
文件,引入的是組件 css
文件依賴,而非 less
依賴,組件庫底層抹平差別。方案 1 會致使使用成本增長。
方案 2 沒法對樣式文件進行按需引入(後續在 umd
打包時咱們也會提供該樣式文件)。
以上兩種方案實爲下策(畫外音:若是使用css in js
就沒有這麼多屁事了)。
方案 3 比較符合此時的的場景,antd
使用的也是這種方案。
在搭建組件庫的過程當中,有一個問題困擾了我好久:爲何須要alert/style/index.js
引入less
文件或alert/style/css.js
引入css
文件?
答案是管理樣式依賴。
假設存在如下場景:引入<Button />
,<Button />
依賴了<Icon />
,使用者須要手動去引入調用的組件的樣式(<Button />
)及其依賴的組件樣式(<Icon />
),遇到複雜組件極其麻煩,因此組件庫開發者能夠提供一份這樣的js
文件,使用者手動引入這個js
文件,就能引入對應組件及其依賴組件的樣式。
繼續咱們的旅程。
安裝相關依賴。
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()) // 根據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);
// ...
複製代碼
執行yarn build
,組件style
目錄下已經存在css
文件了。
接下來咱們須要一個alert/style/css.js
來幫用戶引入css
文件。
此處參考antd-tools的實現方式:在處理scripts
任務中,截住style/index.js
,生成style/css.js
,並經過正則將引入的less
文件後綴改爲css
。
安裝相關依賴。
yarn add through2 --dev
複製代碼
gulpfile.js
// ...
/** * 編譯腳本文件 * @param {*} babelEnv babel環境變量 * @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
的實現:
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';
複製代碼
以上引入樣式文件的方式不太優雅,直接引入全量樣式文件又和按需加載的本意相去甚遠。
使用者能夠藉助babel-plugin-import來進行輔助,減小代碼編寫量。
import { Alert } from 'happy-ui';
複製代碼
⬇️
import Alert from 'happy-ui/lib/alert';
import 'happy-ui/lib/alert/style';
複製代碼
真正意義上的「打包」,生成全量 js
文件 和 css
文件供使用者外鏈引入。此處選擇rollup
進行打包。
留坑待填。
To be Continued...