JavaScript庫架構實戰
本專案 jslib-base 是一個能讓開發者輕鬆開發屬於自己的JavaScript庫的基礎框架。
靈感來源於顏海鏡的 8102年如何寫一個現代的JavaScript庫 , 專案連結在此 。
需求簡介
最近在專案中需要對內部一款歷史悠久的js庫進行改造升級,此庫使用iife+ES5的方式編寫,日後維護及開發存在諸多不便,隨即萌生了搭建一個編寫js庫的基礎框架的想法,正好又看到了顏大的文章,說幹就幹,最終達到的效果如下:
- 編寫原始碼支援ES6+和TypeScript
- 打包後代碼支援多環境(支援瀏覽器原生,支援AMD,CMD,支援Webpack,Rollup,fis等,支援Node)
- 收斂專案相關配置,目錄清晰,上手簡單
- Tree Shaking: 自動剔除第三方依賴無用程式碼
- 一鍵初始化框架
- 自動生成API文件
- 整合程式碼風格校驗
- 整合commit資訊校驗及增量程式碼風格校驗
- 整合單元測試及測試覆蓋率
- 整合可持續構建工具與測試結果上報
使用說明
首先克隆倉庫至本地並安裝依賴:
$ git clone https://github.com/logan70/jslib-base.git $ cd jslib-base $ npm install 複製程式碼
初始化框架,按照提示填寫專案名、變數名及專案地址
$ npm run init 複製程式碼

然後就可以在 src/
資料夾愉快地開發了(可監聽變化構建,實時檢視效果),開發完成後打包
# 監聽構建 $ npm run dev # 打包構建 $ npm run build 複製程式碼
最後就是打包釋出:
# 自動修改CHANGLOG及版本資訊 $ npm run release # 登入npm $ npm login # 釋出npm包 $ npm publish 複製程式碼
釋出後就可以在各種環境內使用你自己的JavaScript庫了:
// 首先npm安裝你的js庫 $ npm install yourLibName --save-dev // 瀏覽器內使用 // 引入檔案:<script src="path/to/index.aio.min.js"><script> yourLibName.xxx(xxx) // es6模組規範內使用 import yourLibName from 'yourLibName' yourLibName.xxx(xxx) // Node內使用 const yourLibName = require('yourLibName') yourLibName.xxx(xxx) 複製程式碼
是不是很簡單,更多資訊可往下閱讀技術實現,也可前往Github專案檢視(主要是歡迎Star,哈哈)。
技術實現
首先要明確的一點是,要做到原始碼支援ES6+和TypeScript,我們一開始就要做好規劃,理想情況是使用者切換隻需要修改一處即可,故為專案建立配置檔案 jslib.config.js
:
// jslib.config.js module.exports = { srcType: 'js' // 原始碼型別,js或ts } 複製程式碼
編譯打包工具
使用工具:Rollup + Babel + TypeScript
相關文件:
- Rollup : 下一代打包工具 -Rollup中文文件
- TypeScript : JavaScript的超集 -TypeScript中文網
- Babel : JavaScript編譯工具 -Babel中文網
打包工具我選擇Rollup,主要因為其強大的Tree Shaking能力以及構建體積優勢。
- Tree Shaking : Rollup僅支援ES6模組,在構建程式碼時,在使用ES6模組化的程式碼中,會對你的程式碼進行靜態分析,只打包使用到的程式碼。
- 構建體積 : Webpack構建後除了業務邏輯程式碼,還包括程式碼執行引導及模組關係記錄的程式碼,Rollup構建後則只有業務邏輯程式碼,構建體積佔優,總結就是開發庫或框架使用Rollup,應用開發時選擇Webpack。
下面我們看看Rollup的使用:
首先安裝Rollup及相關外掛
$ npm install rollup -D 複製程式碼
然後新建一個配置檔案 build/rollupConfig/rollup.config.aio.js
:
// build/rollupConfig/rollup.config.aio.js const{ srcType } = require('../../jslib.config') export default { input: `src/index.${srcType}`, // 入口檔案,區分js|ts output: { file: 'dist/index.aio.js', // 構建檔案 format: 'umd', // 輸出格式,umd格式支援瀏覽器直接引入、AMD、CMD、Node name: 'myLib', // umd模組名,在瀏覽器環境用作全域性變數名 banner: '/* https://github.com/logan70/jslib-base */' // 插入打包後文件的頭部內容 } } 複製程式碼
然後在 src/index.js
下編寫原始碼:
// src/index.js export function foo() { console.log('Hello world!') } 複製程式碼
然後執行命令進行打包構建:
$ npx rollup --config build/rollupConfig/rollup.config.aio.js 複製程式碼
我們來看看打包後的檔案 dist/index.aio.js
:
/* https://github.com/logan70/jslib-base */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.myLib = {})); }(this, function (exports) { 'use strict'; function foo() { console.log('Hello world!'); } exports.foo = foo; Object.defineProperty(exports, '__esModule', { value: true }); })); 複製程式碼
非常完美有木有,我們繼續編寫:
// src/index.js // ... export const add = (num1, num2) => num1 + num2 複製程式碼
打包後檢視:
// dist/index.aio.js // ... function foo() { console.log('Hello world!'); } const add = (num1, num2) => num1 + num2; exports.foo = foo; exports.add = add; // ... 複製程式碼
???幾個意思小老弟,const和箭頭函式什麼鬼?
原來是忘記了編譯,說到編譯我就想到了今年下半年...
直接開花,說到編譯當然是大名鼎鼎的Babel,Rollup有Babel的外掛,直接安裝Babel相關及外掛使用:
$ npm install @babel/core @babel/preset-env @babel/plugin-transform-runtime-D $ npm install @babel/polyfill @babel/runtime -S $ npm install rollup-plugin-babel rollup-plugin-node-resolve rollup-plugin-commonjs -D 複製程式碼
名稱 | 作用 |
---|---|
@babel/core | Babel核心 |
@babel/preset-env | JS新語法轉換 |
@babel/polyfill | 為所有 API 增加相容方法 |
@babel/plugin-transform-runtime & @babel/runtime | 把幫助類方法從每次使用前定義改為統一 require,精簡程式碼 |
rollup-plugin-babel | Rollup的Babel外掛 |
rollup-plugin-node-resolve | Rollup解析外部依賴模組外掛 |
rollup-plugin-commonjs | Rollup僅支援ES6模組,此外掛是將外部依賴CommonJS模組轉換為ES6模組的外掛 |
然後修改Rollup配置:
// build/rollupConfig/rollup.config.aio.js const babel = require('rollup-plugin-babel') const nodeResolve = require('rollup-plugin-node-resolve') const commonjs = require('rollup-plugin-commonjs') const { srcType } =require('../../jslib.config') export default { input: `src/index.${srcType}`, // 入口檔案 output: { // ... }, plugins: [ // Rollup解析外部依賴模組外掛 nodeResolve(), // Rollup僅支援ES6模組,此外掛是將外部依賴CommonJS模組轉換為ES6模組的外掛 commonjs({ include: 'node_modules/**', }), babel({ presets: [ [ '@babel/preset-env', { targets: { browsers: 'last 2 versions, > 1%, ie >= 6, Android >= 4, iOS >= 6, and_uc > 9', node: '0.10' }, // 是否將ES6模組轉為CommonJS模組,必須為false // 否則 Babel 會在 Rollup 有機會做處理之前,將我們的模組轉成 CommonJS,導致 Rollup 的一些處理失敗 // 例如rollup-plugin-commonjs外掛,將 CommonJS 轉換成 ES6 模組 modules: false, // 鬆散模式,原始碼不同時使用export和export default時可開啟,更好相容ie8以下 loose: false, // 按需進行polyfill useBuiltIns: 'usage' } ] ], plugins: ['@babel/plugin-transform-runtime'], runtimeHelpers: true, exclude: 'node_modules/**' }) ] } 複製程式碼
再次打包後檢視:
// dist/index.aio.js // ... function foo() { console.log('Hello world!'); } var add = function add(num1, num2) { return num1 + num2; }; exports.foo = foo; exports.add = add; // ... 複製程式碼
完事兒收工!接下來就是解決TypeScript的支援了,首先安裝依賴:
$ npm install typescript rollup-plugin-typescript2 -D 複製程式碼
名稱 | 作用 |
---|---|
typescript | typescript核心 |
rollup-plugin-typescript2 | rollup編譯typeScript的外掛 |
然後建立TypeScript編譯配置檔案 tsconfig.json
:
// tsconfig.json { "compilerOptions": { "target": "ES5", "module": "ES6", "lib": ["esnext", "dom"], "esModuleInterop": true }, "include": [ "src/**/*.ts" ], "exclude": [ "node_modules", "**.d.ts" ] } 複製程式碼
由於Rollup編譯外掛會根據原始碼型別動態切換,所以我們建立檔案 build/rollupConfig/getCompiler.js
用來動態匯出Rollup編譯外掛:
// build/rollupConfig/getCompiler.js const babel = require('rollup-plugin-babel') const typeScript = require('rollup-plugin-typescript2') const { srcType } = require('../../jslib.config') const jsCompiler = babel({ // ... }) const tsCompiler = typeScript({ // 覆蓋tsconfig.json的配置,rollup僅支援ES6模組 tsconfigOverride: { compilerOptions : { module: 'ES6', target: 'ES5' } } }) module.exports = () => srcType === 'js' ? jsCompiler : tsCompiler 複製程式碼
然後修改Rollup配置檔案:
const nodeResolve = require('rollup-plugin-node-resolve') const commonjs = require('rollup-plugin-commonjs') const getCompiler = require('./getCompiler') const { srcType } =require('./jslib.config') export default { input: `src/index.${srcType}`, // 入口檔案 output: { // ... }, plugins: [ nodeResolve(), commonjs({ include: 'node_modules/**', }), getCompiler() ] } 複製程式碼
然後建立 src/index.ts
編寫原始碼:
export function foo(): void { console.log('Hello world!') } export const add: (num1: number, num2: number) => number = (num1: number, num2: number): number => num1 + num2 複製程式碼
記得修改 jslib.config.js
中的原始碼型別為 ts
:
module.exports = { srcType: 'ts' // 原始碼型別,js|ts } 複製程式碼
然後執行打包命令檢視輸出檔案 dist/index.aio.js
,發現打包結果完全一樣,大功告成!
多環境支援
使用工具:
考慮到要支援多環境,所以要打包多種格式檔案,但是如果使用 npx rollup --config 1.js && npx rollup --config 2.js
這種構建方式,其實是序列構建,效率低,所以使用Rollup提供的Node API結合 Promise.all
來充分利用js的非同步特性,提升構建效率。
工欲善其事,必先利其器。考慮到之後別的命令可能也會使用Node來完成,所以我們先實現一個自己的CLI。
由於我們接下來編寫時會用到JS新特性,所以要求版本大於8,我們使用 semver 工具:
$ npm install semver -D 複製程式碼
然後新建 build/index.js
檔案作為我們的Node執行入口:
// build/index.js const semver = require('semver') const requiredVersion = '>=8' // check node version if (!semver.satisfies(process.version, requiredVersion)) { console.error( `You are using Node ${process.version}, but @logan/jslib-base ` + `requires Node ${requiredVersion}.\nPlease upgrade your Node version.` ) process.exit(1) } 複製程式碼
然後我們將Node版本切換到7進行測試:

測試OK,接下來需要實現根據命令列引數不同執行不同任務的功能,我們解析命令列用到 minimist :
$ npm install minimist -D 複製程式碼
minimist的使用很簡單:
const args = require('minimist')(process.argv.slice(2)) console.log(args) 複製程式碼
我們來看看效果:

我想大家已經明白怎麼使用了,下面繼續編寫Node入口:
// build/index.js // ... // 解析命令列引數 const args = require('minimist')(process.argv.slice(2)) // 取出第一個作為命令 const command = args._[0] // 從args._中刪除命令 args._.shift() function run(command, args) { // 動態載入命令執行檔案 const runner = require(`./command-${command}/index`) // 將args作為引數傳入並執行對應任務函式 runner(args) } run(command, args) 複製程式碼
之後如果我們想新增任務,只需要建立 command-${任務名稱}
資料夾,在資料夾下的 index.js
中編寫程式碼,然後在 package.json
新增對應的script命令即可。
我們先在 package.json
中新增構建命令:
// package.json { "scripts": { ... "build": "node build/index.js build", ... } } 複製程式碼
然後建立 build/command-build/index.js
編寫執行構建任務的程式碼:
其他輸出格式的Rollup配置檔案以及Rollup提供的Node API的使用不做詳解,感興趣的朋友自行了解
const path = require('path') const rollup = require('rollup') // 不同環境配置檔案對映 const rollupConfigMap = { // UMD格式 aio: 'rollup.config.aio.js', // UMD格式壓縮版 aioMin: 'rollup.config.aio.min.js', // ES6模組格式 esm: 'rollup.config.esm.js', // CommonJS格式 cjs: 'rollup.config.js' } // 單個rollup構建任務 function runRollup(configFile) { return new Promise(async (resolve) => { // 根據配置檔名引入rollup配置 const options = require(path.resolve(__dirname, '../rollupConfig', configFile)) // 建立rollup任務 const bundle = await rollup.rollup(options.inputOption) // 構建檔案 await bundle.write(options.outputOption) console.log(`${options.outputOption.file} 構建成功`) resolve() }) } module.exports = async (args = {}) => { // 要構建的格式陣列 const moduleTypes = args._ // 目的在於支援選擇要構建的型別 // 例如 node build/index.js build esm cjs 則只構建es6模組格式和commonjs格式檔案 // 不傳則全部構建 const configFiles = moduleTypes && moduleTypes.length ? moduleTypes.map(moduleKey => rollupConfigMap[moduleKey]) : Object.values(rollupConfigMap) try { // 並行構建(偽,JS單執行緒) await Promise.all(configFiles.map(file => runRollup(file))) } catch (e) { throw new Error(e) } } 複製程式碼
然後我們執行構建命令 npm run build
檢視效果:

程式碼風格檢查
使用工具:
- ESLint : JavaScript程式碼風格校驗工具 -ESLint中文文件
- TSLint : TypeScript程式碼風格校驗工具-TSLint官網
首先安裝依賴:
$ npm install eslint eslint-config-airbnb eslint-plugin-import -D 複製程式碼
配置ESLint校驗規則檔案 .eslintrc.js
,詳細過程略,詳情請前往上方官網瞭解。
JavaScript程式碼使用 Airbnb JavaScript 風格 作為基礎,配合 無分號 規則(可視個人/團隊偏好修改)來校驗程式碼風格。
配置TSLint校驗規則檔案 tslint.json
,詳細過程略,詳情請前往上方官網瞭解。
TypeScript程式碼使用預設規則,配合 單引號、無分號 規則(可視個人/團隊偏好修改)來校驗程式碼風格。
然後在 package.json
中新增校驗命令:
// package.json { "scripts": { ... "lint": "node build/index.js lint", "lint:fix": "node build/index.js lint --fix", ... } } 複製程式碼
然後建立 build/command-jslint/index.js
編寫執行構建任務的程式碼:
// build/command-jslint/index.js // Node自帶子程序方法 const { spawn } = require('child_process') const { srcType } = require('../../jslib.config') module.exports = async (args = {}) => { const options = [ // 要校驗的檔案,glob匹配 `src/**/*.${srcType}`, // 錯誤輸出格式,個人喜歡codeframe風格,資訊比較詳細 '--format', 'codeframe' ] // 是否需要自動修復,npm run lint:fix 啟用 if (args.fix) { options.push('--fix') } // 要使用的lint工具 const linter = srcType === 'js' ? 'eslint' : 'tslint' // 開啟子程序 spawn( linter, options, // 資訊輸出至主程序 { stdio: 'inherit' } ) } 複製程式碼
然後我們來測試一下:
JavaScript程式碼風格檢查及修復:

TypeScript程式碼風格檢查及修復:

自動生成API文件
使用工具
- JSDoc : 根據JS註釋自動生成API文件工具 -JSDoc官網
- docdash : JSDoc主題,支援搜尋等功能 - docdash
- TypeDoc : 根據TS註釋自動生成API文件工具 -TypeDoc官網
- typedoc-plugin-external-module-name : 優化TypeScript文件模組分類外掛 - typedoc-plugin-external-module-name
首先安裝依賴:
$ npm install jsdoc typedoc typedoc-plugin-external-module-name -D 複製程式碼
配置JSDoc檔案 build/command-doc/jsdocConf.js
,詳細過程略,詳情請前往上方官網瞭解。
配置TypeDoc檔案 build/command-doc/tsdocConf.js
,詳細過程略,詳情請前往上方官網瞭解。
然後在 package.json
中新增生成API文件的命令:
// package.json { "scripts": { ... "doc": "node build/index.js doc", ... } } 複製程式碼
然後建立 build/command-doc/index.js
編寫執行生成API文件任務的程式碼:
// build/command-jslint/index.js // Node自帶子程序方法 const { spawn } = require('child_process') const path = require('path') const TypeDoc = require('typedoc') const { srcType } = require('../../jslib.config') module.exports = async (args = {}) => { if (srcType === 'js') { spawn('jsdoc', ['-c', path.resolve(__dirname, './jsdocConf.js')], { stdio: 'inherit' }) resolve() } else { // 引入tsdoc配置 const tsdocConf = require(path.resolve(__dirname, './tsdocConf')) // 初始化任務,詳見typedoc官網 const app = new TypeDoc.Application(tsdocConf) const project = app.convert(app.expandInputFiles(['src'])) if (project) { const outputDir = tsdocConf.outputDir // 輸出文件 app.generateDocs(project, outputDir) } } } 複製程式碼
然後我們在原始碼內新增JavaScript規範化註釋,相關注釋標準也可前往JSDoc官網檢視:
// src/index.js /** * @module umdName * @description JavaScript庫 - umdName * @see https://github.com/logan70/jslib-base * @example * // 瀏覽器內使用 * // 引入檔案:<script src="path/to/index.aio.min.js"><script> * window.umdName.add(1, 2) * * // es6模組規範內使用 * import umdName from '@logan/jslib-base' * umdName.add(1, 2) * * // Node內使用 * const umdName = require('@logan/jslib-base') * umdName.add(1, 2) */ /** * @description 加法函式 * @method add * @memberof module:umdName * @param {Number} num1 - 加數 * @param {Number} num2 - 被加數 * @return {Number} - 兩數相加結果 * @example * umdName.add('Hello World!') */ export const add = (num1, num2) => num1 + num2 複製程式碼
然後將 jslib.config.js
中的原始碼型別修改為 js
,執行命令 npm run doc
,開啟 docs/index.html
檢視效果:

效果如上圖所示,然後我們在原始碼內新增TypeScript規範化註釋,相關注釋標準也可前往TypeDoc官網檢視:
注意:TypeDoc不支援@example標籤,但是支援MarkDown語法,所以我們可以將程式碼例項寫在md標籤內
/** * @module umdName * @description JavaScript庫 - umdName * @see https://github.com/logan70/jslib-base * @example * ```js * * // 瀏覽器內使用 * // 引入檔案:<script src="path/to/index.aio.min.js"><script> * window.umdName.add(1, 2) * * // es6模組規範內使用 * import umdName from '@logan/jslib-base' * umdName.add(1, 2) * * // Node內使用 * const umdName = require('@logan/jslib-base') * umdName.add(1, 2) * ``` */ /** * @description 加法函式 * @param num1 - 加數 * @param num2 - 被加數 * @returns 兩數相加結果 * @example * ```js * * umdName.add(1, 2) * ``` */ export const add: (num1: number, num2: number) => number = (num1: number, num2: number): number => num1 + num2 複製程式碼
然後將 jslib.config.js
中的原始碼型別修改為 ts
,執行命令 npm run doc
,開啟 docs/index.html
檢視效果:


單元測試及測試覆蓋率
使用工具
- Jest : 單元測試框架 -Jest中文文件
- babel-jest : JS新語法特性支援外掛 - babel-jest
- ts-jest : TypeScript支援外掛 -ts-jest
- @type/jest : TypeScript的Jest宣告外掛 -@type/jest
首先安裝依賴:
$ npm install jest babel-jest ts-jest @type/jest -D 複製程式碼
編寫jest檔案 build/command-test/jest.config.js
。
// build/command-test/jest.config.js const path = require('path') module.exports = { // 根路徑,指向專案根路徑 rootDir: path.resolve(__dirname, '../../'), // jest尋找的路徑陣列,新增專案根路徑 "roots": [ path.resolve(__dirname, '../../') ], // ts-jest用於支援typescript, babel-jest用於支援ES6模組化語法 "transform": { "^.+\\.tsx?$": "ts-jest", "^.+\\.jsx?$": "babel-jest" }, // 測試檔案匹配正則 "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$", // 測試檔案內可省略的檔案字尾 "moduleFileExtensions": ["ts", "js"], // 顯示測試內容 "verbose": true } 複製程式碼
然後在 package.json
中新增生成API文件的命令, npm run test:coverage
命令為單元測試並收集測試覆蓋資訊的命令,測試覆蓋資訊後面會講到:
// package.json { "scripts": { ... "test": "node build/index.js test", "test:coverage": "node build/index.js test --coverage", ... } } 複製程式碼
由於JS新語法特性支援需要Babel編譯,我們建立並編寫Babel配置檔案 .babelrc
:
// .babelrc { "presets": ["@babel/preset-env"] } 複製程式碼
然後建立 build/command-test/index.js
編寫執行單元測試任務的程式碼:
// build/command-test/index.js const { spawnSync } = require('child_process') module.exports = (args = {}) => { return new Promise(resolve => { // 指定jest配置檔案 const cliOptions = ['--config', 'build/command-test/jest.config.js'] // 是否收集測試覆蓋率資訊 if (args.coverage) { cliOptions.push('--collectCoverage') } spawnSync('jest', cliOptions, { stdio: 'inherit' }) resolve() }) } 複製程式碼
然後我們在專案根目錄下新建 __tests__
資料夾編寫單元測試用例,更多單元測試知識請前往Jest中文文件學習:
// __tests__/index.test.js import { add } from '../src/index.js' describe('單元測試(js)', () => { it('1加2等於3', () => { expect(add(1, 2)).toEqual(3) }) }) // __tests__/index.test.ts import { add } from '../src/index' describe('單元測試(ts)', () => { it('1加2等於3', () => { expect(add(1, 2)).toEqual(3) }) }) 複製程式碼
然後執行命令 npm run test
檢視效果:

然後我們來執行命令 npm run test:coverage
檢視測試覆蓋率資訊:

在瀏覽器中開啟 coverage/lcov-report/index.html
也可檢視測試覆蓋率資訊:

幫助資訊
使用工具
首先安裝依賴:
$ npm install chalk ascii-art -D 複製程式碼
然後在 package.json
中新增顯示幫助資訊的命令:
// package.json { "scripts": { ... "help": "node build/index.js help", ... } } 複製程式碼
然後建立 build/command-help/index.js
編寫執行輸出幫助資訊任務的程式碼:
const art = require('ascii-art') const chalk = require('chalk') module.exports = () => { return new Promise(resolve => { // 生成字元畫 art.font('@logan\/jslib\-base', 'Doom', data => { console.log(chalk.cyan(('-').repeat(104))) console.log(chalk.cyan(data)) console.log(chalk.cyan(('-').repeat(104))) console.log() console.log('Usage: npm run <command>') console.log() console.log('A good JavaScript library scaffold.') console.log() console.log('Commands:') console.log('npm run init, initialize this scaffold.') console.log('npm run build, output bundle files of three different types(UMD, ES6, CommonJs).') console.log('npm run dev, select a type of output to watch and rebuild on change.') console.log('npm run lint, lint your code with ESLint/TSLint.') console.log('npm run lint:fix, lint your code and fix errors and warnings that can be auto-fixed.') console.log('npm run doc, generate API documents based on good documentation comments in source code.') console.log('npm run test, test your code with Jest.') console.log('npm run test:coverage, test your code and collect coverage information with Jest.') console.log('npm run help, output usage information.') console.log() console.log(`See more details at ${chalk.cyan('https://github.com/logan70/jslib-base')}`) resolve() }) }) } 複製程式碼
然後我們執行命令 npm run help
檢視效果:

一鍵重新命名
實現一鍵重新命名的思路是:獲取使用者輸入的資訊,然後把相關檔案內佔位符替換為使用者輸入資訊即可。
使用工具
- inquirer : 互動式命令列工具 - inquirer
首先安裝依賴:
$ npm install inquirer -D 複製程式碼
然後在 package.json
中新增初始化腳手架的命令:
// package.json { "scripts": { ... "init": "node build/index.js init", ... } } 複製程式碼
inquirer
使用方法前往 inquirer文件 學習,這裡不做贅述,直接上程式碼。
然後建立 build/command-init/index.js
編寫執行初始化腳手架任務的程式碼:
// build/command-init/index.js const fs = require('fs') const path = require('path') const inquirer = require('inquirer') // 顯示幫助資訊 const runHelp = require('../command-help/index') // inquirer要執行的任務佇列 const promptArr = [] // 獲取UMD格式輸出名 promptArr.push({ type: 'input', name: 'umdName', // 提示資訊 message: 'Enter the name for umd export (used as global varible name in browsers):', // 校驗使用者輸入 validate(name) { if (/^[a-zA-Z][\w\.]*$/.test(name)) { return true } else { // 校驗失敗提示資訊 return `Invalid varible name: ${name}!` } } }) // 獲取專案名 promptArr.push({ type: 'input', name: 'libName', message: 'Enter the name of your project (used as npm package name):', validate(name) { if (/^[a-zA-Z@][\w-]*\/?[\w-]*$/.test(name)) { return true } else { return `Invalid project name: ${name}!` } } }) // 獲取專案地址 promptArr.push({ type: 'input', name: 'repoUrl', default: 'https://github.com/logan70/jslib-base', message: 'Enter the url of your repository:', validate(url) { if (/^https?\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$/.test(url)) { return true } else { return `Invalid repository url: ${url}!` } } }) module.exports = (args = {}) => { return new Promise(async (resolve, reject) => { // 獲取使用者輸入 const { umdName, libName, repoUrl } = await inquirer.prompt(promptArr) // 需要修改的檔案 let files = [ 'jslib.config.js', 'package.json', 'package-lock.json', 'README.md' ] try { await Promise.all(files.map((file) => new Promise((resolve, reject) => { const filePath = path.resolve(__dirname, '../../', file) // 讀取檔案 fs.readFile(filePath, 'utf8', function (err, data) { if (err) { reject(err) return } // 替換佔位符 const result = data .replace(/umdName/g, umdName) .replace(/@logan\/jslib\-base/g, libName) .replace(/https:\/\/github\.com\/logan70\/jslib/g, repoUrl) // 重寫檔案 fs.writeFile(filePath, result, 'utf8', (err) => { if (err) { reject(err) return } resolve() }) }) }))) // 顯示幫助資訊 await runHelp() } catch (e) { throw new Error(e) } }) } 複製程式碼
然後我們執行命令 npm run init
檢視效果:

watch監聽構建模式
實現監聽構建的思路是:使用者選擇一種輸出格式,使用Rollup提供的Node API開啟watch模式。
首先在 package.json
中新增初始化腳手架的命令:
// package.json { "scripts": { ... "dev": "node build/index.js watch", ... } } 複製程式碼
然後建立 build/command-watch/index.js
編寫執行監聽構建任務的程式碼:
const path = require('path') const rollup = require('rollup') const inquirer = require('inquirer') const { srcType } = require('../../jslib.config') // rollup 監聽配置 const watchOption = { // 使用chokidar替換原生檔案變化監聽的工具 chokidar: true, include: 'src/**', exclude: 'node_modules/**' } // 使用者選擇一種輸出格式 const promptArr = [{ type: 'list', name: 'configFile', message: 'Select an output type to watch and rebuild on change:', default: 'rollup.config.aio.js', choices: [{ value: 'rollup.config.aio.js', name: 'UMD - dist/index.aio.js (Used in browsers, AMD, CMD.)' }, { value: 'rollup.config.esm.js', name: 'ES6 - dist/index.esm.js (Used in ES6 Modules)' }, { value: 'rollup.config.js', name: 'CommonJS - dist/index.js (Used in Node)' }] }] module.exports = (args = {}) => { return new Promise((resolve, reject) => { // 獲取使用者選擇的輸出格式 inquirer.prompt(promptArr).then(({ configFile }) => { // 對應輸出格式的rollup配置 const customOptions = require(path.resolve(__dirname, '../rollConfig/', configFile)) const options = { ...customOptions.inputOption, output: customOptions.outputOption, watch: watchOption } // 開始監聽 const watcher = rollup.watch(options) // 監聽階段時間處理 watcher.on('event', async (event) => { if (event.code === 'START') { console.log('正在構建...') } else if (event.code === 'END') { console.log('構建完成。') } }) }) }) } 複製程式碼
然後我們執行命令 npm run dev
檢視效果:

規範Git提交資訊
使用工具
- husky : Git鉤子工具 - husky
- @commitlint/config-conventional 和 @commitlint/cli : Git commit資訊校驗工具 - commitlint
- commitizen : 撰寫合格 Commit message 的工具。 - commitizen
- lint-staged : 增量校驗程式碼風格工具 - lint-staged
具體使用方法前往上方文件自行學習。
首先安裝依賴:
$ npm install husky @commitlint/config-conventional @commitlint/cli commitizen lint-staged -D 複製程式碼
然後在 package.json
中新增以下資訊:
// package.json { "scripts": { ... "husky": { "hooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "lint-staged": { "src/**/*.js": [ "eslint --fix", "git add" ], "src/**/*.ts": [ "tslint --fix", "git add" ] }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] }, ... } } 複製程式碼
配置完成後我們來看看效果:

與預期相符,關於Git commit資訊規範推薦閱讀阮一峰老師的文章
這麼多資訊規範,記不住或者不想記怎麼辦,沒關係, commitizen 是一個撰寫合格 Commit message 的工具。
之前我們已經安裝過依賴,我們執行下面的命令使其支援 Angular 的 Commit message 格式。
$ commitizen init cz-conventional-changelog --save --save-exact 複製程式碼
然後在 package.json
中新增Git提交的命令:
// package.json { "scripts": { ... "commit": "npx git-cz", ... } } 複製程式碼
之後本專案凡是用到 git commit
命令,一律替換為 npm run commit
命令即可,我們看下效果:

這邊還要介紹一個根據commit資訊自動生成CHANGELOG並更新版本資訊的工具 - standard-version
我們先安裝依賴:
$ npm install standard-version 複製程式碼
然後在 package.json
中新增release命令:
// package.json { "scripts": { ... "release": "standard-version", ... } } 複製程式碼
之後要釋出新版本時,可以執行命令 npm run release
來根據Git Commit歷史自動更新 CHANGELOG.md
和版本資訊。
持續整合
使用工具
- travis-ci : 持續整合工具 -travis-ci
- codecov : 測試結果分析工具 -codecov
使用方法非常簡單,首先使用Github賬號分別登入Travis-CI和Codecov,新增你的Github專案。
然後安裝依賴:
$ npm install codecov -D 複製程式碼
然後在 package.json
中新增codecov命令:
// package.json { "scripts": { ... "codecov": "codecov", ... } } 複製程式碼
然後在專案跟目下下建立travis-ci配置檔案 .travis.yml
:
language: node_js# 指定執行環境為node node_js:# 指定nodejs版本為8 - "8" cache:# 快取 node_js 依賴,提升第二次構建的效率 directories: - node_modules script:# 執行的指令碼命令 - npm run test:coverage# 單元測試並收集測試覆蓋資訊 - npm run codecov# 將單元測試結果上傳到codecov 複製程式碼
我們編寫好原始碼及單元測試,推送到遠端倉庫,然後去檢視結果:
Travis-CI:

Codecov:
