共计 11902 个字符,预计需要花费 30 分钟才能阅读完成。
前言
随着公司产品线的增多,开发维护的项目也越来越多,在业务开发过程中,就会发现经常用到的 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 配置后,有两种方式打包:
- package.json 的 script 字段自定义指令打包指定格式的包并导出到 lib 下
- 在 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 + 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 浏览,
- 行覆盖率(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] 项目的入口文件,修改 mian
和module
分别为 `
lib/m-utils-min.js 和
lib/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
,登录账号密码发布
使用
- 直接下载
lib
目录下的 m.min.js,通过<script>
标签引入
<script src="m-utils-min.js"></script> | |
<script> | |
var arrayFlat = mUtils.arrayFlat() | |
</script> |
- 使用 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'; |
相关链接
- 源码地址
今天的分享就到这里,后续会继续完善,希望对你有帮助~~
~~ 未完待续