從零搭建自己的js工具庫 typescript+rollup+karma+mocha+coverage

前言

隨着公司產品線的增多,開發維護的項目也越來越多,在業務開發過程中,就會發現經常用到的cookie處理,數組處理,節流防抖函數等工具函數,這些工具函數在很多的項目中會使用到,爲了避免一份代碼多次複製粘貼使用的low操作,筆者嘗試從零搭建JavaScript工具庫typescript+rollup+karma+mocha+coverage , 寫這篇文章主要是分享給有同樣需求的朋友提供參考,希望對你有所幫助。

項目源碼在文章結尾處,記得查收哦~

目錄結構說明

├── 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。

簡單來說就是,在開發應用時使用webpack,開發庫時使用Rollup

如果對Rollup還不熟悉,建議查看Rollup官網文檔

如何構建?

主要說明下項目中config.js和script/build.js的構建過程

第一步,構建全量包,在cofig.js配置後,有兩種方式打包:

  1. package.json的script字段自定義指令打包指定格式的包並導出到lib下
  2. 在build.js獲取config.js導出rollup配置,通過rollup一次性打包不同格式的包並保存到lib文件夾下
自定義打包

在config.js配置umd,es,cjs格式,及壓縮版min的全量包,*對於包umd/esm/cjs不同格式之間的區別請移步 [
JS 模塊化規範](
https://qwqaq.com/b8fd304a.html)*

......
......
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配置

在package.json ,`--environment TARGET:m-utils`-cjs
指定了 process.env.TARGET 的值, 執行npm run dev:cjs m-utils-cjs.js保存到lib下

"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",
     ......
  },
build.js構建腳本
  ......
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 + chaikarma 爲我們自動建立一個測試用的瀏覽器環境,能夠測試涉及到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瀏覽,
  • 行覆蓋率(line coverage)
  • 函數覆蓋率(function coverage)
  • 分支覆蓋率(branch coverage)
  • 語句覆蓋率(statement coverage)

發佈

添加函數

當前項目源碼使用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打包

發佈到npm私服

[1] 公司內部使用,一般都是發佈到內部的npm私服,對於npm私服的搭建,在此不做過多的講解
[2] 在此發佈npm作用域包,修改package.json中的name@mutils/m-utils
[3] 項目的入口文件,修改 mianmodule 分別爲`
lib/m-utils-min.jslib/m-utils-esm.js`

  • main : 定義了 npm 包的入口文件,browser 環境和 node 環境均可使用
  • module : 定義 npm 包的 ESM 規範的入口文件,browser 環境和 node 環境均可使用

[4] 設置發佈的私服地址,修改publishConfig字段

"publishConfig": {
    "registry": "https://npm-registry.xxx.cn/"
  },

[5] 執行npm publish,登錄賬號密碼發佈

使用

  1. 直接下載lib 目錄下的 m.min.js,通過 <script> 標籤引入
 <script src="m-utils-min.js"></script> 
 <script> 
  var arrayFlat = mUtils.arrayFlat() 
 </script>
  1. 使用npm安裝
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';

相關鏈接

今天的分享就到這裏,後續會繼續完善,希望對你有幫助~~

~~未完待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章