從0到1構建基于自身業(yè)務的前端工具庫
作者:京東零售
前言
?在實際項目開發(fā)中無論 M 端、PC 端,或多或少都有一個 utils 文件目錄去管理項目中用到的一些常用的工具方法,比如:時間處理、價格處理、解析url參數(shù)、加載腳本等,其中很多是重復、基礎、或基于某種業(yè)務場景的工具,存在項目間冗余的痛點以及工具方法規(guī)范不統(tǒng)一的問題。
?在實際開發(fā)過程中,經(jīng)常使用一些開源工具庫,如 lodash,以方便、快捷的進行項目開發(fā)。但是當 npm上沒有自己中意或符合自身業(yè)務的工具時,我們不得不自己動手,此時擁有自己的、基于業(yè)務的工具庫就顯得尤為重要。
?我們所熟知的Vue、React等諸多知名前端框架,或公司提供的一些類庫,它們是如何開發(fā)、構建、打包出來的,本文將帶領你了解到如何從0到1構建基于自身業(yè)務的前端工具庫。
構建工具庫主流方案
1. WEBPACK
? webpack 提供了構建和打包不同模塊化規(guī)則的庫,只是需要自己去搭建開發(fā)底層架構。
? vue-cli,基于 webpack , vue-cli 腳手架工具可以快速初始化一個 vue 應用,它也可以初始化一個構建庫。
2. ROLLUP
? rollup 是一個專門針對JavaScript模塊打包器,可以將應用或庫的小塊代碼編譯成更復雜的功能代碼。
? Vue、React 等許多流行前端框架的構建和打包都能看到 rollup 的身影。
為什么采用 ROLLUP 而不是 WEBPACK
?webpack 主要職能是開發(fā)應用,而 rollup 主要針對的就是 js 庫的開發(fā),如果你要開發(fā) js 庫,那 webpack 的繁瑣配置和打包后的文件體積就不太適用了,通過webpack打包構建出來的源代碼增加了很多工具函數(shù)以外的模塊依賴代碼。
?rollup 只是把業(yè)務代碼轉(zhuǎn)碼成目標 js ,小巧且輕便。rollup對于代碼的Tree-shaking和ES6模塊有著算法優(yōu)勢上的支持,如果只想構建一個簡單的庫,并且是基于ES6開發(fā)的,加上其簡潔的API,rollup得到更多開發(fā)者的青睞。
工具庫底層架構設計
構建工具庫底層架構大概需要哪些功能的支持:
架構依賴需知
在對底層架構設計的基礎上,首先需要把用到的依賴庫簡單熟悉一下:
rollup 全家桶
?? rollup(工具庫打包構建核心包)
?? rollup-plugin-livereload(rollup 插件,熱更新,方便本地 debugger 開發(fā))
?? rollup-plugin-serve(rollup 插件,本地服務代理,方便在本地 html 中調(diào)試工具)
?? rollup-plugin-terser(rollup 插件,代碼壓縮混淆)
?? rollup-plugin-visualizer(rollup 插件,可視化并分析 Rollup bundle,以查看模塊占用)
?? @rollup/plugin-babel(rollup 插件,rollup 的 babel 插件,ES6 轉(zhuǎn) ES5)
?? @rollup/plugin-commonjs(rollup 插件,用來將 CommonJS 模塊轉(zhuǎn)換為 ES6,這樣它們就可以包含在 Rollup 包中)
?? @rollup/plugin-json(rollup 插件,它將.json 文件轉(zhuǎn)換為 ES6 模塊)
?? @
rollup/plugin-node-resolve(rollup 插件,它使用節(jié)點解析算法定位模塊,用于在節(jié)點模塊中使用第三方 node_modules 包)
?? @rollup/plugin-typescript(rollup 插件,對 typescript 的支持,將 typescript 進行 tsc 轉(zhuǎn)為 js)
typescript 相關
?? typescript(使用 ts 開發(fā)工具庫)
?? tslib(TypeScript 的運行庫,它包含了 TypeScript 所有的幫助函數(shù))
?? @
typescript-eslint/eslint-plugin(TypeScript 的 eslint 插件,約束 ts 書寫規(guī)范)
?? @typescript-eslint/parser(ESLint 解析器,它利用 TypeScript ESTree 來允許 ESLint 檢測 TypeScript 源代碼)
文檔相關
?? typedoc(TypeScript 項目的文檔生成器)
?? gulp(使用 gulp 構建文檔系統(tǒng))
?? gulp-typedoc(Gulp 插件來執(zhí)行 TypeDoc 工具)
?? browser-sync(文檔系統(tǒng)熱更新)
單元測試相關
?? jest(一款優(yōu)雅、簡潔的 JavaScript 測試框架)
?? @types/jest(Jest 的類型定義)
?? ts-jest(一個支持源映射的 Jest 轉(zhuǎn)換器,允許您使用 Jest 來測試用 TypeScript 編寫的項目)
?? @babel/preset-typescript(TypeScript 的 Babel 預設)
其他依賴
?? eslint(代碼規(guī)范約束)
?? @babel/core(@rollup/plugin-babel 依賴的 babel 解析插件)
?? @
babel/plugin-transform-runtime(babel 轉(zhuǎn)譯依賴)
?? @babel/preset-env(babel 轉(zhuǎn)譯依賴)
?? chalk(控制臺字符樣式)
?? rimraf(UNIX 命令 rm -rf 用于 node)
?? cross-env(跨平臺設置 node 環(huán)境變量)
底層架構搭建
1. 初始化項目
新建一個文件夾 utils-demo,執(zhí)行 npm init,過程會詢問構建項目的基本信息,按需填寫即可:
npm init
2. 組織工具庫業(yè)務開發(fā) SRC 目錄結構
創(chuàng)建工具庫業(yè)務開發(fā) src 文件目錄,明確怎樣規(guī)劃工具庫包,里面放置的是工具庫開發(fā)需要的業(yè)務代碼:
3. 安裝項目依賴
要對 typescript 代碼進行解析支持需要安裝對 ts 支持的依賴,以及對開發(fā)的工具的一些依賴包:
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官網(wǎng)
4. 組織項目打包構建 SCRIPTS 目錄結構
1) 根目錄創(chuàng)建項目打包構建 scripts 腳本文件目錄,里面放置的是有關于項目打包構建需要的文件:
生成rollup配置項函數(shù)核心代碼:
const moduleName = camelCase(name) // 當format為iife和umd時必須提供,將作為全局變量掛在window下:window.moduleName=...
const banner = generateBanner() // 包說明文案
// 生成rollup配置文件函數(shù)
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]
// 生產(chǎn)環(huán)境加入包分析以及代碼壓縮
plugins = [
...plugins,
visualizer({
gzipSize: true,
brotliSize: true
}),
terser()
]
pushPlugins({ format, plugins, ext })
})
return result
}
2) rollup 在打包構建的過程中需要進行 babel 的轉(zhuǎn)譯,需要在根目錄添加.babelrc 文件告知 babel:
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
3) 此時距離打包構建工具庫只差一步之遙,配置打包腳本命令,在 package.json 中配置命令:
"scripts": {
"build": "rimraf lib && rollup -c ./scripts/rollup.config.js" // rollup打包
},
4) 執(zhí)行 yarn build,根目錄會構建出一個 lib 文件夾,里面有打包構建的文件,還多了一個 stats.html,這個是可視化并分析 Rollup bundle,用來查看工具模塊占用空間:
架構搭建優(yōu)化
項目搭建到這里,不知機智的你能否發(fā)現(xiàn)問題:
1) 只要添加了一個工具,就要在入口文件導出需要打包構建的工具,在多人開發(fā)提交代碼的時候?qū)⒁齺頉_突的產(chǎn)生:
2) 使用工具庫的時候,按需引用的顆粒度太細了,不能滿足一些要求顆粒度粗的朋友,比如:
?? 我想使用該包里面 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) 針對第一個代碼沖突的問題,可以根據(jù) 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')
2) 針對顆粒度的問題,可以將 modules 下各種類型工具文件夾下面也自動生成入口文件,除了全部導出,再追加 import * as 模塊類名稱 類型的導出
至此,基本上解決了工具庫打包的問題,但是架構中還缺少本地開發(fā)調(diào)試的環(huán)境,下面為大家介紹如何架構中添加本地開發(fā)調(diào)試的系統(tǒng)。
本地開發(fā)調(diào)試系統(tǒng)
首先要明確要加入本地開發(fā)調(diào)試系統(tǒng)的支持,需要做到以下:
?? 跨平臺(window不支持NODE_ENV=xxx)設置環(huán)境變量,根據(jù)環(huán)境配置不同的 rollup 配置項
?? 引入本地開發(fā)需要的 html 靜態(tài)服務器環(huán)境,并能做到熱更新
1) 跨平臺設置環(huán)境變量很簡單,使用 cross-env 指定 node 的環(huán)境
yarn add cross-env -D
2) 配置 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 表示監(jiān)聽的工具模塊的修改
"build": "rimraf lib && yarn entry && cross-env NODE_ENV=production rollup -c ./scripts/rollup.config.js"
},
3) 根據(jù)最開始架構設計的模塊,在項目根目錄新建 debugger 文件夾,里面存放的是工具調(diào)試的 html 靜態(tài)頁面
4) 接下來就是配置 scripts > rollup.config.js ,將 NODE_ENV=development 環(huán)境加入 rollup 配置,修改生成rollup配置項函數(shù)核心代碼:
(isProd ? buildType : devType).forEach(({ format, ext }) => {
let plugins = [...defaultPlugins]
if (isProd) {
// 生產(chǎn)環(huán)境加入包分析以及代碼壓縮
plugins = [...plugins, visualizer({
gzipSize: true,
brotliSize: true
}), terser()]
} else {
// 非生產(chǎn)環(huán)境加入熱更新和本地服務插件,方便本地debugger
plugins = [...plugins, ...[
// 熱更新
rollUpLiveLoad({
watch: ['debugger', 'lib'],
delay: 300
}),
// 本地服務代理
rollupServe({
open: true,
// resolveFile('')代理根目錄原因是為了在ts代碼里debugger時可以方便看到調(diào)試信息
contentBase: [resolveFile('debugger'), resolveFile('lib'), resolveFile('')]
})
]]
}
pushPlugins({ format, plugins, ext })
})
5) 執(zhí)行 yarn dev 之后瀏覽器會新打開窗口,輸入剛添加的工具鏈接,并且它是熱更新的:
工具庫文檔系統(tǒng)
一個完備的工具庫需要有一個文檔來展示開發(fā)的工具函數(shù),它可能需要具備以下幾點支持:
?? 支持工具庫中方法的可視化預覽
?? 支持修改工具的時候,具備熱更新機制
typedoc(TypeScript 項目的文檔生成器)能完美支持 typescript 開發(fā)工具庫的文檔生成器的支持,它的核心原理就是讀取源代碼,根據(jù)工具的注釋、ts的類型規(guī)范等,自動生成文檔頁面
關于熱更新機制的支持,第一個自然想到 browser-sync(文檔系統(tǒng)熱更新)
由于文檔系統(tǒng)的預覽功能有很多插件組合來實現(xiàn)的,可以借助 gulp (基于流的自動化構建工具),typedoc正好有對應的 gulp-typedocGulp 插件來執(zhí)行 TypeDoc 工具插件
構建完成后打開文檔系統(tǒng),并且它是熱更新的,修改工具方法后自動更新文檔:
單元測試
為確保用戶使用的工具代碼的安全性、正確性以及可靠性,工具庫的單元測試必不可少。單元測試選用的是 Facebook 出品的 Jest 測試框架,它對于 TypeScript 有很好的支持。
1. 環(huán)境搭建
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
執(zhí)行完之后根目錄會自動生成jest.config.ts 文件,里面設置了單元測試的配置規(guī)則,package.json 里面也多了一個 script 指令 test。
2) 關于jest.config.js文件配置項具體含義可以查看官網(wǎng),要想完成 jest 對于 TypeScript 的測試,還需要安裝一些依賴:
yarn add jest ts-jest @babel/preset-typescript @types/jest -D
3) jest 還需要借助 .babelrc 去解析 TypeScript 文件,再進行測試,編輯 .babelrc 文件,添加依賴包 @babel/preset-typescript:
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env"
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
2. 單元測試文件的編寫
1) 通過以上環(huán)節(jié),jest 單元測試環(huán)境基本搭建完畢,接下來在__tests__下編寫測試用例
2) 執(zhí)行 yarn test
可以看到關于 debounce 防抖工具函數(shù)的測試情況顯示在了控制臺:
?? stmts 是語句覆蓋率(statement coverage):是不是每個語句都執(zhí)行了?
?? Branch 分支覆蓋率(branch coverage):是不是每個 if 代碼塊都執(zhí)行了?
?? Funcs 函數(shù)覆蓋率(function coverage):是不是每個函數(shù)都調(diào)用了?
?? Lines 行覆蓋率(line coverage):是不是每一行都執(zhí)行了?
3) 同時還會發(fā)現(xiàn)項目根目錄多了一個 coverage 文件夾,里面就是 jest 生成的測試報告:
3. 單元測試文件的編寫引發(fā)的思考
每次修改單元測試都要執(zhí)行 yarn test 去查看測試結果,怎么解決?
jest提供了 watch 指令,只需要配置 scripts 腳本就可以做到,單元測試的熱更新。
"scripts": {
"test": "jest --watchAll"
},
以后會寫很多工具的測試用例,每次 test 都將所有工具都進行了測試,能否只測試自己寫的工具?
jest 也提供了測試單個文件的方法,這樣 jest 只會對防抖函數(shù)進行測試(前提全局安裝了 jest)。
jest debounce.test.ts --watch
工具庫包的發(fā)布
至此工具庫距離開發(fā)者使用僅一步之遙,就是發(fā)布到npm上,發(fā)包前需要在 package.json 中聲明庫的一些入口,關鍵詞等信息。
"main": "lib/main.js", // 告知引用該包模塊化方式的默認文件路徑
"module": "lib/main.esm.js", // 告知引用該包模塊化方式的文件路徑
"types": "lib/types/index.d.ts", // 告知引用該包的類型聲明文件路徑
"sideEffects": false, // false 為了告訴 webpack 我這個 npm 包里的所有文件代碼都是沒有副作用的
"files": [ // 開發(fā)者引用該包后node_modules包里面的文件
"lib",
"README.md"
],
"keywords": [
"typescript",
"utils-demo",
"utils"
],
"scripts": {
"pub": "npm publish"
},
登陸npm,你會看到自己的 packages 里面有了剛剛發(fā)布的工具庫包:
寫在最后
以上就是作者整理的從0到1構建基于自身業(yè)務的前端工具庫的全過程,希望能給閱讀本文的開發(fā)人員帶來一些新的想法與嘗試。
在此基礎上已經(jīng)成功在京東npm源發(fā)布了應用于京東汽車前端的工具庫@jdcar/car-utils,并在各個業(yè)務線及系統(tǒng)得到落地。
當然,架構優(yōu)化之路也還遠未結束,比如:打包構建的速度、本地開發(fā)按需構建、工具庫腳手架化等,后續(xù)我們也會基于自身業(yè)務以及一些新技術,持續(xù)深入優(yōu)化,在性能上進一步提升,在功能上進一步豐富。本文或存在一些淺顯不足之處,也歡迎大家評論指點。
參考資料
[1] rollup 英文文檔(https://rollupjs.org/guide/en/#quick-start)
[2] rollup 中文文檔(https://rollupjs.org/guide/zh/#introduction)
[3] Rollup.js 實戰(zhàn)學習筆記(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] 如何開發(fā)一個基于 TypeScript 的工具庫并自動生成文檔(https://juejin.cn/post/6844903881030238221)
[8] 一款優(yōu)雅、簡潔的 JavaScript 測試框架(https://jestjs.io/zh-Hans/)