React 組件庫搭建指南-打包輸出

重頭戲來了。css

概覽

宿主環境各不相同,須要將源碼進行相關處理後發佈至 npm。node

明確如下目標:react

  1. 導出類型聲明文件
  2. 導出 umd/Commonjs module/ES module 等 3 種形式供使用者引入
  3. 支持樣式文件 css 引入,而非只有less
  4. 支持按需加載

本節全部代碼可在倉庫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 moduleesm指代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 函數(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-envuseBuiltIns選項值設置爲 usage,同時把node_modulesbabel-loaderexclude掉的同窗可能想要這個特性:"useBuiltIns: usage" for node_modules without transpiling #9419,在未支持該issue提到的內容以前,仍是乖乖地將useBuiltIns設置爲entry,或者不要把node_modulesbabel-loaderexclude

因此組件庫不用多此一舉,引入多餘的polyfill,寫好文檔說明,比什麼都重要(就像zentantd這樣)。

如今@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-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));
}

// 並行任務 後續加入樣式處理 能夠並行處理
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

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函數。

.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
      },
    ],
  ],
};
複製代碼

目標達成,咱們再使用環境變量區分esmcjs(執行任務時設置對應的環境變量便可),最終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

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目錄,能夠發現 less 文件已被拷貝至alert/style目錄下。

lib

├── alert
│   ├── alert.js
│   ├── index.js
│   ├── interface.js
│   └── style
│       ├── index.js
│       └── index.less # less文件
└── index.js
複製代碼

可能有些同窗已經發現問題:若使用者沒有使用less預處理器,使用的是sass方案甚至原生css方案,那現有方案就搞不定了。經分析,有如下 3 種預選方案:

  1. 告知用戶增長less-loader
  2. 打包出一份完整的 css 文件,進行全量引入;
  3. 單獨提供一份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文件,就能引入對應組件及其依賴組件的樣式。

繼續咱們的旅程。

生成 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()) // 根據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文件。

生成 css.js

此處參考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';
複製代碼

生成 umd

真正意義上的「打包」,生成全量 js文件 和 css文件供使用者外鏈引入。此處選擇rollup進行打包。

留坑待填。

To be Continued...

相關文章
相關標籤/搜索