從0到1構建基於自身業務的前端工具庫

2023-04-10 18:00:54

作者:京東零售  吳迪

前言

在實際專案開發中無論 M 端、PC 端,或多或少都有一個 utils 檔案目錄去管理專案中用到的一些常用的工具方法,比如:時間處理、價格處理、解析url引數、載入指令碼等,其中很多是重複、基礎、或基於某種業務場景的工具,存在專案間冗餘的痛點以及工具方法規範不統一的問題。

  • 在實際開發過程中,經常使用一些開源工具庫,如 lodash,以方便、快捷的進行專案開發。但是當 npm上沒有自己中意或符合自身業務的工具時,我們不得不自己動手,此時擁有自己的、基於業務的工具庫就顯得尤為重要。
  • 我們所熟知的Vue、React等諸多知名前端框架,或公司提供的一些類庫,它們是如何開發、構建、打包出來的,本文將帶領你瞭解到如何從0到1構建基於自身業務的前端工具庫。

構建工具庫主流方案

1. WEBPACK

  • webpack 提供了構建和打包不同模組化規則的庫,只是需要自己去搭建開發底層架構。
  • vue-cli,基於 webpack , vue-cli 腳手架工具可以快速初始化一個 vue 應用,它也可以初始化一個構建庫。

2. ROLLUP

  • rollup 是一個專門針對JavaScript模組打包器,可以將應用或庫的小塊程式碼編譯成更復雜的功能程式碼。
  • Vue、React 等許多流行前端框架的構建和打包都能看到 rollup 的身影。

為什麼採用 ROLLUP 而不是 WEBPACK

  • webpack 主要職能是開發應用,而 rollup 主要針對的就是 js 庫的開發,如果你要開發 js 庫,那 webpack 的繁瑣設定和打包後的檔案體積就不太適用了,通過webpack打包構建出來的原始碼增加了很多工具函數以外的模組依賴程式碼。
  • rollup 只是把業務程式碼轉碼成目標 js ,小巧且輕便。rollup對於程式碼的Tree-shaking和ES6模組有著演演算法優勢上的支援,如果只想構建一個簡單的庫,並且是基於ES6開發的,加上其簡潔的API,rollup得到更多開發者的青睞。

工具庫底層架構設計

構建工具庫底層架構大概需要哪些功能的支援:

架構依賴需知

在對底層架構設計的基礎上,首先需要把用到的依賴庫簡單熟悉一下:

rollup 全家桶

•  rollup(工具庫打包構建核心包)

•  rollup-plugin-livereload(rollup 外掛,熱更新,方便本地 debugger 開發)

•  rollup-plugin-serve(rollup 外掛,本地服務代理,方便在本地 html 中偵錯工具)

•  rollup-plugin-terser(rollup 外掛,程式碼壓縮混淆)

•  rollup-plugin-visualizer(rollup 外掛,視覺化並分析 Rollup bundle,以檢視模組佔用)

•  @rollup/plugin-babel(rollup 外掛,rollup 的 babel 外掛,ES6 轉 ES5)

•  @rollup/plugin-commonjs(rollup 外掛,用來將 CommonJS 模組轉換為 ES6,這樣它們就可以包含在 Rollup 包中)

•  @rollup/plugin-json(rollup 外掛,它將.json 檔案轉換為 ES6 模組)

•  @rollup/plugin-node-resolve(rollup 外掛,它使用節點解析演演算法定位模組,用於在節點模組中使用第三方 node_modules 包)

•  @rollup/plugin-typescript(rollup 外掛,對 typescript 的支援,將 typescript 進行 tsc 轉為 js)

typescript 相關

•  typescript(使用 ts 開發工具庫)

•  tslib(TypeScript 的執行庫,它包含了 TypeScript 所有的幫助函數)

•  @typescript-eslint/eslint-plugin(TypeScript 的 eslint 外掛,約束 ts 書寫規範)

•  @typescript-eslint/parser(ESLint 解析器,它利用 TypeScript ESTree 來允許 ESLint 檢測 TypeScript 原始碼)

檔案相關

•  typedoc(TypeScript 專案的檔案生成器)

•  gulp(使用 gulp 構建檔案系統)

•  gulp-typedoc(Gulp 外掛來執行 TypeDoc 工具)

•  browser-sync(檔案系統熱更新)

單元測試相關

•  jest(一款優雅、簡潔的 JavaScript 測試框架)

•  @types/jest(Jest 的型別定義)

•  ts-jest(一個支援源對映的 Jest 轉換器,允許您使用 Jest 來測試用 TypeScript 編寫的專案)

•  @babel/preset-typescript(TypeScript 的 Babel 預設)

其他依賴

•  eslint(程式碼規範約束)

•  @babel/core(@rollup/plugin-babel 依賴的 babel 解析外掛)

•  @babel/plugin-transform-runtime(babel 轉譯依賴)

•  @babel/preset-env(babel 轉譯依賴)

•  chalk(控制檯字元樣式)

•  rimraf(UNIX 命令 rm -rf 用於 node)

•  cross-env(跨平臺設定 node 環境變數)


底層架構搭建

1. 初始化專案

新建一個資料夾 utils-demo,執行 npm init,過程會詢問構建專案的基本資訊,按需填寫即可:

npm init

2. 組織工具庫業務開發 SRC 目錄結構

建立工具庫業務開發 src 檔案目錄,明確怎樣規劃工具庫包,裡面放置的是工具庫開發需要的業務程式碼:

3. 安裝專案依賴

要對 typescript 程式碼進行解析支援需要安裝對 ts 支援的依賴,以及對開發的工具的一些依賴包:

yarn add typescript tslib rollup rollup-plugin-livereload rollup-plugin-serve rollup-plugin-terser rollup-plugin-visualizer 
@rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-typescript 
@babel/core @babel/plugin-transform-runtime @babel/preset-env rimraf lodash chalk@^4.1.2 -D

這裡遇到一個坑,關於最新 chalk5.0.0 不支援在 nodejs 中 require()匯入,所以鎖定包版本 chalk@^4.1.2

要對 typescript 進行解析和編譯還需要設定 tsconfig.json,該檔案中指定了用來編譯這個專案的根檔案和編譯選項,在專案根目錄,使用 tsc --init 命令快速生成 tsconfig.json 檔案(前提全域性安裝 typescript)

npm i typescript -g
tsc --init

初始化 tsconfig 完成之後,根目錄自動生成 tsconfig.json 檔案,需要對其進行簡單的設定,以適用於 ts 專案,其中具體含義可以參考tsconfig.json官網

4. 組織專案打包構建 SCRIPTS 目錄結構

  1. 根目錄建立專案打包構建 scripts 指令碼檔案目錄,裡面放置的是有關於專案打包構建需要的檔案:

生成rollup設定項函數核心程式碼:

const moduleName = camelCase(name) // 當format為iife和umd時必須提供,將作為全域性變數掛在window下:window.moduleName=...
const banner = generateBanner() // 包說明文案
// 生成rollup組態檔函數
const generateConfigs = (options) => {
  const { input, outputFile } = options
  console.log(chalk.greenBright(`獲取打包入口:${input}`))
  const result = []
  const pushPlugins = ({ format, plugins, ext }) => {
    result.push({
      input, // 打包入口檔案
      external: [], // 如果打包出來的檔案有專案依賴,可以在這裡設定是否將專案依賴一起打到包裡面還是作為外部依賴
      // 打包出口檔案
      output: {
        file: `${outputFile}${ext}`, // 出口檔名稱
        sourcemap: true, // // 是否生成sourcemap
        format, // 打包的模組化格式
        name: moduleName, // 當format為iife和umd時必須提供,將作為全域性變數掛在window下:window.moduleName=...
        exports: 'named' /** Disable warning for default imports */,
        banner, // 打包出來的檔案在最頂部的說明文案
        globals: {} // 如果external設定了打包忽略的專案依賴,在此設定,專案依賴的全域性變數
      },
      plugins // rollup外掛
    })
  }
  buildType.forEach(({ format, ext }) => {
    let plugins = [...defaultPlugins]
    // 生產環境加入包分析以及程式碼壓縮
    plugins = [
      ...plugins,
      visualizer({
        gzipSize: true,
        brotliSize: true
      }),
      terser()
    ]

    pushPlugins({ format, plugins, ext })
  })
return result
}



  1. rollup 在打包構建的過程中需要進行 babel 的轉譯,需要在根目錄新增.babelrc 檔案告知 babel:
{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}


  1. 此時距離打包構建工具庫只差一步之遙,設定打包指令碼命令,在 package.json 中設定命令:
"scripts": {
    "build": "rimraf lib && rollup -c ./scripts/rollup.config.js" // rollup打包
 },


  1. 執行 yarn build,根目錄會構建出一個 lib 資料夾,裡面有打包構建的檔案,還多了一個 stats.html,這個是視覺化並分析 Rollup bundle,用來檢視工具模組佔用空間:

架構搭建優化

專案搭建到這裡,不知機智的你能否發現問題:

  1. 只要新增了一個工具,就要在入口檔案匯出需要打包構建的工具,在多人開發提交程式碼的時候將引來衝突的產生:

  1. 使用工具庫的時候,按需參照的顆粒度太細了,不能滿足一些要求顆粒度粗的朋友,比如:

• 我想使用該包裡面 date 相關工具,要這樣嗎?

import { dateA, dateB, dateC } from "utils-demo"

能不能這樣?

import { date } from "utils-demo"
date.dateA()
date.dateB()
date.dateC()


• 在一些使用 script 指令碼引入的場景下,就僅僅需要 date 相關的工具,要這樣嗎?

<script src="https://xxx/main.min.js">

能不能這樣?

<script src="https://xxx/date.min.js">

這樣僅僅使用 date 裡面的工具,就沒有必要將所有的工具都引入了

解決方案:

  1. 針對第一個程式碼衝突的問題,可以根據 src > modules 下目錄結構自動生成入口檔案 index.ts

自動構建入口檔案核心程式碼:

const fs = require('fs') // node fs模組
const chalk = require('chalk') // 自定義輸出樣式
const { resolveFile, getEntries } = require('./utils')
let srcIndexContent = `
// tips:此檔案是自動生成的,無需手動新增
`
getEntries(resolveFile('src/modules/*')).forEach(({ baseName, entry }) => {
  let moduleIndexContent = `
// tips:此檔案是自動生成的,無需手動新增
`
  try {
    // 判斷是否資料夾
    const stats = fs.statSync(entry)
    if (stats.isDirectory()) {
      getEntries(`${entry}/*.ts`).forEach(({ baseName }) => {
        baseName = baseName.split('.')[0]
        if (baseName.indexOf('index') === -1) {
          moduleIndexContent += `
export * from './${baseName}'
`
        }
      })
      fs.writeFileSync(`${entry}/index.ts`, moduleIndexContent, 'utf-8')
      srcIndexContent += `
export * from './modules/${baseName}'
export * as ${baseName} from './modules/${baseName}'
`
    } else {
      srcIndexContent += `
export * from './modules/${baseName.split('.')[0]}'
`
    }
  } catch (e) {
    console.error(e)
  }
})
fs.writeFileSync(resolveFile('src/index.ts'), srcIndexContent, 'utf-8')


  1. 針對顆粒度的問題,可以將 modules 下各種型別工具資料夾下面也自動生成入口檔案,除了全部匯出,再追加 import * as 模組類名稱 型別的匯出

至此,基本上解決了工具庫打包的問題,但是架構中還缺少本地開發偵錯的環境,下面為大家介紹如何架構中新增本地開發偵錯的系統。

本地開發偵錯系統

首先要明確要加入本地開發偵錯系統的支援,需要做到以下:

•  跨平臺(window不支援NODE_ENV=xxx)設定環境變數,根據環境設定不同的 rollup 設定項

•  引入本地開發需要的 html 靜態伺服器環境,並能做到熱更新

  1. 跨平臺設定環境變數很簡單,使用 cross-env 指定 node 的環境
yarn add cross-env -D
  1. 設定 package.json 命令
 "scripts": {
    "entry": "node ./scripts/build-entry.js",
    "dev": "rimraf lib && yarn entry && cross-env NODE_ENV=development rollup -w -c ./scripts/rollup.config.js", // -w 表示監聽的工具模組的修改
    "build": "rimraf lib && yarn entry && cross-env NODE_ENV=production rollup -c ./scripts/rollup.config.js"
  },


  1. 根據最開始架構設計的模組,在專案根目錄新建 debugger 資料夾,裡面存放的是工具偵錯的 html 靜態頁面

  1. 接下來就是設定 scripts > rollup.config.js ,將 NODE_ENV=development 環境加入 rollup 設定,修改生成rollup設定項函數核心程式碼:
(isProd ? buildType : devType).forEach(({ format, ext }) => {
    let plugins = [...defaultPlugins]
    if (isProd) {
      // 生產環境加入包分析以及程式碼壓縮
      plugins = [...plugins, visualizer({
        gzipSize: true,
        brotliSize: true
      }), terser()]
    } else {
      // 非生產環境加入熱更新和本地服務外掛,方便本地debugger
      plugins = [...plugins, ...[
        // 熱更新
        rollUpLiveLoad({
          watch: ['debugger', 'lib'],
          delay: 300
        }),
        // 本地服務代理
        rollupServe({
          open: true,
          // resolveFile('')代理根目錄原因是為了在ts程式碼裡debugger時可以方便看到偵錯資訊
          contentBase: [resolveFile('debugger'), resolveFile('lib'), resolveFile('')]
        })
      ]]
    }
    pushPlugins({ format, plugins, ext })
  })


  1. 執行 yarn dev 之後瀏覽器會新開啟視窗,輸入剛新增的工具連結,並且它是熱更新的:

工具庫檔案系統

一個完備的工具庫需要有一個檔案來展示開發的工具函數,它可能需要具備以下幾點支援:

•  支援工具庫中方法的視覺化預覽

•  支援修改工具的時候,具備熱更新機制

typedoc(TypeScript 專案的檔案生成器)能完美支援 typescript 開發工具庫的檔案生成器的支援,它的核心原理就是讀取原始碼,根據工具的註釋、ts的型別規範等,自動生成檔案頁面

關於熱更新機制的支援,第一個自然想到 browser-sync(檔案系統熱更新)

由於檔案系統的預覽功能有很多外掛組合來實現的,可以藉助 gulp (基於流的自動化構建工具),typedoc正好有對應的 gulp-typedocGulp 外掛來執行 TypeDoc 工具外掛

構建完成後開啟檔案系統,並且它是熱更新的,修改工具方法後自動更新檔案:

單元測試

為確保使用者使用的工具程式碼的安全性、正確性以及可靠性,工具庫的單元測試必不可少。單元測試選用的是 Facebook 出品的 Jest 測試框架,它對於 TypeScript 有很好的支援。

1. 環境搭建

  1. 首先全域性安裝 jest 使用 init 來初始化 jest 設定項
npm jest -g
jest --init
下面是本人設定的jest的設定項
✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls, instances and results before every test? … yes


執行完之後根目錄會自動生成jest.config.ts 檔案,裡面設定了單元測試的設定規則,package.json 裡面也多了一個 script 指令 test。

  1. 關於jest.config.js檔案設定項具體含義可以檢視官網,要想完成 jest 對於 TypeScript 的測試,還需要安裝一些依賴:
yarn add jest ts-jest @babel/preset-typescript @types/jest -D
  1. jest 還需要藉助 .babelrc 去解析 TypeScript 檔案,再進行測試,編輯 .babelrc 檔案,新增依賴包 @babel/preset-typescript:
{
  "presets": [
    "@babel/preset-typescript",
    [
      "@babel/preset-env"
    ]
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}


2. 單元測試檔案的編寫

  1. 通過以上環節,jest 單元測試環境基本搭建完畢,接下來在__tests__下編寫測試用例

  1. 執行 yarn test

可以看到關於 debounce 防抖工具函數的測試情況顯示在了控制檯:

•  stmts 是語句覆蓋率(statement coverage):是不是每個語句都執行了?

•  Branch 分支覆蓋率(branch coverage):是不是每個 if 程式碼塊都執行了?

•  Funcs 函數覆蓋率(function coverage):是不是每個函數都呼叫了?

•  Lines 行覆蓋率(line coverage):是不是每一行都執行了?

  1. 同時還會發現專案根目錄多了一個 coverage 資料夾,裡面就是 jest 生成的測試報告:

3. 單元測試檔案的編寫引發的思考

每次修改單元測試都要執行 yarn test 去檢視測試結果,怎麼解決?

jest提供了 watch 指令,只需要設定 scripts 指令碼就可以做到,單元測試的熱更新。

"scripts": {
  "test": "jest --watchAll"
},


以後會寫很多工具的測試用例,每次 test 都將所有工具都進行了測試,能否只測試自己寫的工具?

jest 也提供了測試單個檔案的方法,這樣 jest 只會對防抖函數進行測試(前提全域性安裝了 jest)。

jest debounce.test.ts --watch

工具庫包的釋出

至此工具庫距離開發者使用僅一步之遙,就是釋出到npm上,發包前需要在 package.json 中宣告庫的一些入口,關鍵詞等資訊。

  "main": "lib/main.js", // 告知參照該包模組化方式的預設檔案路徑
  "module": "lib/main.esm.js", // 告知參照該包模組化方式的檔案路徑
  "types": "lib/types/index.d.ts", // 告知參照該包的型別宣告檔案路徑
  "sideEffects": false, // false 為了告訴 webpack 我這個 npm 包裡的所有檔案程式碼都是沒有副作用的
  "files": [ // 開發者參照該包後node_modules包裡面的檔案
    "lib",
    "README.md"
  ],
  "keywords": [
    "typescript",
    "utils-demo",
    "utils"
  ],
  "scripts": {
    "pub": "npm publish"
  },


登陸npm,你會看到自己的 packages 裡面有了剛剛釋出的工具庫包:

寫在最後

以上就是作者整理的從0到1構建基於自身業務的前端工具庫的全過程,希望能給閱讀本文的開發人員帶來一些新的想法與嘗試。

在此基礎上已經成功在京東npm源釋出了應用於京東汽車前端的工具庫@jdcar/car-utils,並在各個業務線及系統得到落地。

當然,架構優化之路也還遠未結束,比如:打包構建的速度、本地開發按需構建、工具庫腳手架化等,後續我們也會基於自身業務以及一些新技術,持續深入優化,在效能上進一步提升,在功能上進一步豐富。本文或存在一些淺顯不足之處,也歡迎大家評論指點。

參考資料

[1] rollup 英文檔案(https://rollupjs.org/guide/en/

[2] rollup 中文檔案(https://rollupjs.org/guide/zh/

[3] Rollup.js 實戰學習筆記(https://chenshenhai.github.io/rollupjs-note/

[4] Rollup 打包工具的使用(https://juejin.cn/post/6844904058394771470

[5] TypeScript、Rollup 搭建工具庫(https://juejin.cn/post/6844904035309322254

[6] 使用 rollup.js 封裝各專案共用的工具包(https://juejin.cn/post/6993720790046736420

[7] 如何開發一個基於 TypeScript 的工具庫並自動生成檔案(https://juejin.cn/post/6844903881030238221

[8] 一款優雅、簡潔的 JavaScript 測試框架(https://jestjs.io/zh-Hans/