隨着公司產品線的增多,開發維護的項目也愈來愈多,在業務開發過程當中,就會發現常常用到的cookie處理,數組處理,節流防抖函數等工具函數,這些工具函數在不少的項目中會使用到,爲了不一份代碼屢次複製粘貼使用的low操做,筆者嘗試從零搭建JavaScript工具庫typescript+rollup+karma+mocha+coverage , 寫這篇文章主要是分享給有一樣需求的朋友提供參考,但願對你有所幫助。css
項目源碼在文章結尾處,記得查收哦~html
├── scripts ------------------------------- 構建相關的文件 │ ├── config.js ------------------------- 生成rollup配置的文件 │ ├── build.js -------------------------- 對 config.js 中全部的rollup配置進行構建 ├── coverage ---------------------------------- 測試覆蓋率報告 ├── dist ---------------------------------- ts編譯後文件的輸出目錄 ├── lib ---------------------------------- 構建後後文件的輸出目錄 ├── test ---------------------------------- 包含全部測試文件 │ ├── index.ts --------------------------自動化單元測試入口文件 │ ├── xx.spec.ts ------------------------------ 單元測試文件 ├── src ----------------------------------- 工具函數源碼 │ ├── entry-compiler.ts -------------------------- 函數入口文件 │ ├── arrayUtils ------------------------------ 存放與數組處理相關的工具函數 │ │ ├── arrayFlat.ts ---------------------- 數組平鋪 │ ├── xx ------------------------------ xx │ │ ├── xxx.ts ----------------------xxx ├── package.json ----------------------------- 配置文件 ├── package-lock.json ----------------------------- 鎖定安裝包的版本號 ├── index.d.ts ------------------------- 類型聲明文件 ├── karma.conf.js ------------------------- karma配置文件 ├── .babelrc ------------------------------ babel 配置文件 ├── tsconfig.json ----------------------------- ts 配置文件 ├── tslint.json ----------------------------- tslint 配置文件 ├── .npmignore ------------------------- npm發包忽略配置 ├── .gitignore ---------------------------- git 忽略配置
目錄結構會隨着時間迭代,建議查看庫上最新的目錄結構前端
目前社區有不少的構建工具,不一樣的構建工具適用場景不一樣,Rollup是一個js模塊打包器,能夠將小塊代碼編譯成複雜的代碼塊,偏向應用於js庫,像vue,vuex,dayjs等優秀的開源項目就是使用rollup,而webpack是一個js應用程序的靜態模塊打包器,適用於場景中涉及css、html,複雜的代碼拆分合並的前端工程,如element-ui。vue
簡單來講就是,在開發應用時使用webpack,開發庫時使用Rollup
若是對Rollup還不熟悉,建議查看Rollup官網文檔node
主要說明下項目中config.js和script/build.js的構建過程webpack
第一步,構建全量包,在cofig.js配置後,有兩種方式打包:git
- package.json的script字段自定義指令打包指定格式的包並導出到lib下
- 在build.js獲取config.js導出rollup配置,經過rollup一次性打包不一樣格式的包並保存到lib文件夾下
在config.js配置umd,es,cjs格式,及壓縮版min的全量包,對於包umd/esm/cjs不一樣格式之間的區別請移步 JS 模塊化規範github
...... ...... const builds = { 'm-utils': { entry: resolve('dist/src/entry-compiler.js'), // 入口文件路徑 dest: resolve('lib/m-utils.js'), // 導出的文件路徑 format: 'umd', // 格式 moduleName: 'mUtils', banner, // 打包後默認的文檔註釋 plugins: defaultPlugins // 插件 }, 'm-utils-min': { entry: resolve('dist/src/entry-compiler.js'), dest: resolve('lib/m-utils-min.js'), format: 'umd', moduleName: 'mUtils', banner, plugins: [...defaultPlugins, terser()] }, 'm-utils-cjs': { entry: resolve('dist/src/entry-compiler.js'), dest: resolve('lib/m-utils-cjs.js'), format: 'cjs', banner, plugins: defaultPlugins }, 'm-utils-esm': { entry: resolve('dist/src/entry-compiler.js'), dest: resolve('lib/m-utils-esm.js'), format: 'es', banner, plugins: defaultPlugins }, } /** * 獲取對應name的打包配置 * @param {*} name */ function getConfig(name) { const opts = builds[name]; const config = { input: opts.entry, external: opts.external || [], plugins: opts.plugins || [], output: { file: opts.dest, format: opts.format, banner: opts.banner, name: opts.moduleName || 'mUtils', globals: opts.globals, exports: 'named', /** Disable warning for default imports */ }, onwarn: (msg, warn) => { warn(msg); } } Object.defineProperty(config, '_name', { enumerable: false, value: name }); return config; } if(process.env.TARGET) { module.exports = getConfig(process.env.TARGET); }else { exports.defaultPlugins = defaultPlugins; exports.getBuild = getConfig; exports.getAllBuilds = () => Object.keys(builds).map(getConfig); } ...... ......
爲了打包文件兼容node端,以及瀏覽器端的引用,getConfig該方法默認返回umd格式的配置,根據環境變量process.env.TARGET返回指定格式的rollup配置並導出rollup的options配置web
在package.json ,`--environment TARGET:m-utils`-cjs
指定了 process.env.TARGET
的值, 執行npm run dev:cjs
m-utils-cjs.js保存到lib下vuex
"scripts": { ...... "dev:umd": "rollup -w -c scripts/config.js --environment TARGET:m-utils", "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:m-utils-cjs.js", "dev:esm": "rollup -c scripts/config.js --environment TARGET:m-utils-esm", ...... },
...... let building = ora('building...'); if (!fs.existsSync('lib')) { fs.mkdirSync('lib') } // 獲取rollup配置 let builds = require('./config').getAllBuilds() // 打包全部配置的文件 function buildConfig(builds) { building.start(); let built = 0; const total = builds.length; const next = () => { buildEntry(builds[built]).then(() => { built++; if (built < total) { next() } }).then(() => { building.stop() }).catch(logError) } next() } function buildEntry(config) { const output = config.output; const { file } = output; return rollup(config).then(bundle => bundle.generate(output)).then(({ output: [{ code }] }) => { return write(file, code); }) } ...... ......
從config.js暴露的getAllBuilds()方法獲取全部配置,傳入buildConfig方法,打包全部配置文件,即m-utils-cjs.js、m-utils-esm.js等文件。
看過lodash.js的源碼就知道,它每一個方法都是一個獨立的文件,因此須要什麼就 import lodash + '/' + 對應的方法名就能夠的,這樣有利於後續按需加載的實現。參考該思路, 此項目每一個方法是一個獨立的文件,並打包保存到lib路徑下,實現以下:
...... ...... // 導出單個函數 function buildSingleFn() { const targetPath1 = path.resolve(__dirname, '../', 'dist/src/') const dir1 = fs.readdirSync(targetPath1) dir1.map(type => { if (/entry-compiler.js/.test(type)) return; const targetPath2 = path.resolve(__dirname, '../', `dist/src/${type}`) const dir2 = fs.readdirSync(targetPath2) dir2.map(fn => { if (/.map/.test(fn)) return; try { const targetPath3 = path.resolve(__dirname, '../', `dist/src/${type}/${fn}`) fs.readFile(targetPath3, async (err, data) => { if(err) return; const handleContent = data.toString().replace(/require\(".{1,2}\/[\w\/]+"\)/g, (match) => { // match 爲 require("../collection/each") => require("./each") const splitArr = match.split('/') const lastStr = splitArr[splitArr.length - 1].slice(0, -2) const handleStr = `require('./${lastStr}')` return handleStr }) const libPath = path.resolve(__dirname, '../', 'lib') await fs.writeFileSync(`${libPath}/${fn}`, handleContent) //單個函數rollup打包到lib文件根目錄下 let moduleName = firstUpperCase(fn.replace(/.js/,'')); let config = { input: path.resolve(__dirname, '../', `lib/${fn}`), plugins: defaultPlugins, external: ['tslib', 'dayjs'], // 因爲函數用ts編寫,使用external外部引用tslib,減小打包體積 output: { file: `lib/${fn}`, format: 'umd', name: `${moduleName}`, globals: { tslib:'tslib', dayjs: 'dayjs', }, banner: '/*!\n' + ` * @author mzn\n` + ` * @desc ${moduleName}\n` + ' */', } } await buildEntry(config); }) } catch (e) { logError(e); } }) }) } // 構建打包(全量和單個) async function build() { if (!fs.existsSync(path.resolve(__dirname, '../', 'lib'))) { fs.mkdirSync(path.resolve(__dirname, '../', 'lib')) } building.start() Promise.all([ await buildConfig(builds), await buildSingleFn(), ]).then(([result1, result2]) => { building.stop() }).catch(logError) } build(); ...... ......
執行
npm run build
,調用build方法,打包全量包和單個函數的文件。
打包全部單個文件的方法待優化
單元測試使用karma + mocha + coverage + chai
,karma
爲咱們自動創建一個測試用的瀏覽器環境,可以測試涉及到Dom等語法的操做。
引入karma
,執行karma init
,在項目根路徑生成karma.config.js
配置文件,核心部分以下:
module.exports = function(config) { config.set({ // 識別ts mime: { 'text/x-typescript': ['ts', 'tsx'] }, // 使用webpack處理,則不須要karma匹配文件,只留一個入口給karma webpackMiddleware: { noInfo: true, stats: 'errors-only' }, webpack: { mode: 'development', entry: './src/entry-compiler.ts', output: { filename: '[name].js' }, devtool: 'inline-source-map', module: { rules: [{ test: /\.tsx?$/, use: { loader: 'ts-loader', options: { configFile: path.join(__dirname, 'tsconfig.json') } }, exclude: [path.join(__dirname, 'node_modules')] }, { test: /\.tsx?$/, include: [path.join(__dirname, 'src')], enforce: 'post', use: { //webpack打包前記錄編譯前文件 loader: 'istanbul-instrumenter-loader', options: { esModules: true } } } ] }, resolve: { extensions: ['.tsx', '.ts', '.js', '.json'] } }, // 生成coverage覆蓋率報告 coverageIstanbulReporter: { reports: ['html', 'lcovonly', 'text-summary'], dir: path.join(__dirname, 'coverage/%browser%/'), fixWebpackSourcePaths: true, 'report-config': { html: { outdir: 'html' } } }, // 配置使用的測試框架列表,默認爲[] frameworks: ['mocha', 'chai'], // list of files / patterns to load in the browser files: [ 'test/index.ts' ], //預處理 preprocessors: { 'test/index.ts': ['webpack', 'coverage'] }, //使用的報告者(reporter)列表 reporters: ['mocha', 'nyan', 'coverage-istanbul'], // reporter options mochaReporter: { colors: { success: 'blue', info: 'bgGreen', warning: 'cyan', error: 'bgRed' }, symbols: { success: '+', info: '#', warning: '!', error: 'x' } }, // 配置覆蓋率報告的查看方式,type查看類型,可取值html、text等等,dir輸出目錄 coverageReporter: { type: 'lcovonly', dir: 'coverage/' }, ... }) }
配置中webpack關鍵在與打包前使用istanbul-instrumenter-loader
,記錄編譯前文件,由於webpack會幫咱們加入不少它的代碼,得出的代碼覆蓋率失去了意義。
查看測試覆蓋率,打開coverage文件夾下的html瀏覽,
當前項目源碼使用typescript編寫,若還不熟悉的同窗,請先查看ts官方文檔
在src
目錄下, 新建分類目錄或者選擇一個分類,在子文件夾下添加子文件,每一個文件爲單獨的一個函數功能模塊。(以下:src/array/arrayFlat.ts)
/** * @author mznorz * @desc 數組平鋪 * @param {Array} arr * @return {Array} */ function arrayFlat(arr: any[]) { let temp: any[] = []; for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (Object.prototype.toString.call(item).slice(8, -1) === "Array") { temp = temp.concat(arrayFlat(item)); } else { temp.push(item); } } return temp; } export = arrayFlat;
而後在 src/entry-compiler.ts中暴露arrayFlat
爲了在使用該庫時,可以得到對應的代碼補全、接口提示等功能,在項目根路徑下添加index.d.ts
聲明文件,並在package.json
中的type
字段指定聲明文件的路徑。
...... declare namespace mUtils { /** * @desc 數組平鋪 * @param {Array} arr * @return {Array} */ export function arrayFlat(arr: any[]): any[]; ...... } export = mUtils;
在test文件下新建測試用例
import { expect } from "chai"; import _ from "../src/entry-compiler"; describe("測試 數組操做 方法", () => { it("測試數組平鋪", () => { const arr1 = [1,[2,3,[4,5]],[4],0]; const arr2 = [1,2,3,4,5,4,0]; expect(_.arrayFlat(arr1)).to.deep.equal(arr2); }); }); ...... ......
執行npm run test
,查看全部測試用例是否經過,查看/coverage文件下代碼測試覆蓋率報告,如若沒什麼問題,執行npm run compile
編譯ts代碼,再執行npm run build
打包
[1] 公司內部使用,通常都是發佈到內部的npm私服,對於npm私服的搭建,在此不作過多的講解
[2] 在此發佈npm做用域包,修改package.json
中的name
爲@mutils/m-utils
[3] 項目的入口文件,修改 mian
和module
分別爲`
lib/m-utils-min.js 和
lib/m-utils-esm.js`
[4] 設置發佈的私服地址,修改publishConfig
字段
"publishConfig": { "registry": "https://npm-registry.xxx.cn/" },
[5] 執行npm publish
,登陸帳號密碼發佈
lib
目錄下的 m.min.js,經過 <script>
標籤引入<script src="m-utils-min.js"></script> <script> var arrayFlat = mUtils.arrayFlat() </script>
npm i @mutils/m-utils -S
直接安裝會報找不到該包的錯誤信息,需在項目根路徑建立 .npmrc
文件,併爲做用域包設置registry
registry=https://registry.npmjs.org # Set a new registry for a scoped package # https://npm-registry.xxx.cn 私服地址 @mutils:registry=https://npm-registry.xxx.cn
import mUtils from '@mutils/m-utils'; import { arrayFlat } from '@mutils/m-utils';
今天的分享就到這裏,後續會繼續完善,但願對你有幫助~~
~~未完待續