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