随着公司产品线的增多,开发维护的项目也愈来愈多,在业务开发过程当中,就会发现常常用到的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';
今天的分享就到这里,后续会继续完善,但愿对你有帮助~~
~~未完待续