作者:京东批发 吴迪
前言
在理论我的项目开发中无论 M 端、PC 端,或多或少都有一个 utils 文件目录去治理我的项目中用到的一些罕用的工具办法,比方:工夫解决、价格解决、解析 url 参数、加载脚本等,其中很多是反复、根底、或基于某种业务场景的工具,存在我的项目间冗余的痛点以及工具办法标准不对立的问题。
- 在理论开发过程中,常常应用一些开源工具库,如 lodash,以不便、快捷的进行我的项目开发。然而当 npm 上没有本人中意或合乎本身业务的工具时,咱们不得不本人入手,此时领有本人的、基于业务的工具库就显得尤为重要。
- 咱们所熟知的 Vue、React 等诸多出名前端框架,或公司提供的一些类库,它们是如何开发、构建、打包进去的,本文将率领你理解到如何从 0 到 1 构建基于本身业务的前端工具库。
构建工具库支流计划
1. WEBPACK
- webpack 提供了构建和打包不同模块化规定的库,只是须要本人去搭建开发底层架构。
- vue-cli,基于 webpack,vue-cli 脚手架工具能够疾速初始化一个 vue 利用,它也能够初始化一个构建库。
2. ROLLUP
- rollup 是一个专门针对 JavaScript 模块打包器,能够将利用或库的小块代码编译成更简单的性能代码。
- Vue、React 等许多风行前端框架的构建和打包都能看到 rollup 的身影。
为什么采纳 ROLLUP 而不是 WEBPACK
- webpack 次要职能是开发利用,而 rollup 次要针对的就是 js 库的开发,如果你要开发 js 库,那 webpack 的繁琐配置和打包后的文件体积就不太实用了,通过 webpack 打包构建进去的源代码减少了很多工具函数以外的模块依赖代码。
- rollup 只是把业务代码转码成指标 js,玲珑且轻便。rollup 对于代码的 Tree-shaking 和 ES6 模块有着算法劣势上的反对,如果只想构建一个简略的库,并且是基于 ES6 开发的,加上其简洁的 API,rollup 失去更多开发者的青眼。
工具库底层架构设计
构建工具库底层架构大略须要哪些性能的反对:
架构依赖需知
在对底层架构设计的根底上,首先须要把用到的依赖库简略相熟一下:
rollup 全家桶
• rollup(工具库打包构建外围包)
• rollup-plugin-livereload(rollup 插件,热更新,不便本地 debugger 开发)
• rollup-plugin-serve(rollup 插件,本地服务代理,不便在本地 html 中调试工具)
• rollup-plugin-terser(rollup 插件,代码压缩混同)
• rollup-plugin-visualizer(rollup 插件,可视化并剖析 Rollup bundle,以查看模块占用)
• @rollup/plugin-babel(rollup 插件,rollup 的 babel 插件,ES6 转 ES5)
• @rollup/plugin-commonjs(rollup 插件,用来将 CommonJS 模块转换为 ES6,这样它们就能够蕴含在 Rollup 包中)
• @rollup/plugin-json(rollup 插件,它将.json 文件转换为 ES6 模块)
• @rollup/plugin-node-resolve(rollup 插件,它应用节点解析算法定位模块,用于在节点模块中应用第三方 node_modules 包)
• @rollup/plugin-typescript(rollup 插件,对 typescript 的反对,将 typescript 进行 tsc 转为 js)
typescript 相干
• typescript(应用 ts 开发工具库)
• tslib(TypeScript 的运行库,它蕴含了 TypeScript 所有的帮忙函数)
• @typescript-eslint/eslint-plugin(TypeScript 的 eslint 插件,束缚 ts 书写标准)
• @typescript-eslint/parser(ESLint 解析器,它利用 TypeScript ESTree 来容许 ESLint 检测 TypeScript 源代码)
文档相干
• typedoc(TypeScript 我的项目的文档生成器)
• gulp(应用 gulp 构建文档零碎)
• gulp-typedoc(Gulp 插件来执行 TypeDoc 工具)
• browser-sync(文档零碎热更新)
单元测试相干
• jest(一款优雅、简洁的 JavaScript 测试框架)
• @types/jest(Jest 的类型定义)
• ts-jest(一个反对源映射的 Jest 转换器,容许您应用 Jest 来测试用 TypeScript 编写的我的项目)
• @babel/preset-typescript(TypeScript 的 Babel 预设)
其余依赖
• eslint(代码标准束缚)
• @babel/core(@rollup/plugin-babel 依赖的 babel 解析插件)
• @babel/plugin-transform-runtime(babel 转译依赖)
• @babel/preset-env(babel 转译依赖)
• chalk(控制台字符款式)
• rimraf(UNIX 命令 rm -rf 用于 node)
• cross-env(跨平台设置 node 环境变量)
底层架构搭建
1. 初始化我的项目
新建一个文件夹 utils-demo,执行 npm init,过程会询问构建我的项目的根本信息,按需填写即可:
npm init
2. 组织工具库业务开发 SRC 目录构造
创立工具库业务开发 src 文件目录,明确怎么布局工具库包,外面搁置的是工具库开发须要的业务代码:
3. 装置我的项目依赖
要对 typescript 代码进行解析反对须要装置对 ts 反对的依赖,以及对开发的工具的一些依赖包:
yarn add typescript tslib rollup rollup-plugin-livereload rollup-plugin-serve rollup-plugin-terser rollup-plugin-visualizer
@rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-typescript
@babel/core @babel/plugin-transform-runtime @babel/preset-env rimraf lodash chalk@^4.1.2 -D
这里遇到一个坑,对于最新 chalk5.0.0 不反对在 nodejs 中 require() 导入,所以锁定包版本 chalk@^4.1.2
要对 typescript 进行解析和编译还须要配置 tsconfig.json,该文件中指定了用来编译这个我的项目的根文件和编译选项,在我的项目根目录,应用 tsc –init 命令疾速生成 tsconfig.json 文件(前提全局装置 typescript)
npm i typescript -g
tsc --init
初始化 tsconfig 实现之后,根目录主动生成 tsconfig.json 文件,须要对其进行简略的配置,以实用于 ts 我的项目,其中具体含意能够参考 tsconfig.json 官网
4. 组织我的项目打包构建 SCRIPTS 目录构造
1) 根目录创立我的项目打包构建 scripts 脚本文件目录,外面搁置的是有对于我的项目打包构建须要的文件:
生成 rollup 配置项函数外围代码:
const moduleName = camelCase(name) // 当 format 为 iife 和 umd 时必须提供,将作为全局变量挂在 window 下:window.moduleName=...
const banner = generateBanner() // 包阐明文案
// 生成 rollup 配置文件函数
const generateConfigs = (options) => {const { input, outputFile} = options
console.log(chalk.greenBright(` 获取打包入口:${input}`))
const result = []
const pushPlugins = ({format, plugins, ext}) => {
result.push({
input, // 打包入口文件
external: [], // 如果打包进去的文件有我的项目依赖,能够在这里配置是否将我的项目依赖一起打到包外面还是作为内部依赖
// 打包进口文件
output: {file: `${outputFile}${ext}`, // 进口文件名称
sourcemap: true, // // 是否生成 sourcemap
format, // 打包的模块化格局
name: moduleName, // 当 format 为 iife 和 umd 时必须提供,将作为全局变量挂在 window 下:window.moduleName=...
exports: 'named' /** Disable warning for default imports */,
banner, // 打包进去的文件在最顶部的阐明文案
globals: {} // 如果 external 设置了打包疏忽的我的项目依赖,在此配置,我的项目依赖的全局变量},
plugins // rollup 插件
})
}
buildType.forEach(({format, ext}) => {let plugins = [...defaultPlugins]
// 生产环境退出包剖析以及代码压缩
plugins = [
...plugins,
visualizer({
gzipSize: true,
brotliSize: true
}),
terser()]
pushPlugins({format, plugins, ext})
})
return result
}
2) rollup 在打包构建的过程中须要进行 babel 的转译,须要在根目录增加.babelrc 文件告知 babel:
{
"presets": [
["@babel/preset-env"]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
3) 此时间隔打包构建工具库只差一步之遥,配置打包脚本命令,在 package.json 中配置命令:
"scripts": {"build": "rimraf lib && rollup -c ./scripts/rollup.config.js" // rollup 打包},
4) 执行 yarn build,根目录会构建出一个 lib 文件夹,外面有打包构建的文件,还多了一个 stats.html,这个是可视化并剖析 Rollup bundle,用来查看工具模块占用空间:
架构搭建优化
我的项目搭建到这里,不知机智的你是否发现问题:
1) 只有增加了一个工具,就要在入口文件导出须要打包构建的工具,在多人开发提交代码的时候将引来抵触的产生:
2) 应用工具库的时候,按需援用的颗粒度太细了,不能满足一些要求颗粒度粗的敌人,比方:
• 我想应用该包外面 date 相干工具,要这样吗?
import {dateA, dateB, dateC} from "utils-demo"
能不能这样?
import {date} from "utils-demo"
date.dateA()
date.dateB()
date.dateC()
• 在一些应用 script 脚本引入的场景下,就仅仅须要 date 相干的工具,要这样吗?
<script src="https://xxx/main.min.js">
能不能这样?
<script src="https://xxx/date.min.js">
这样仅仅应用 date 外面的工具,就没有必要将所有的工具都引入了
解决方案:
1) 针对第一个代码抵触的问题,能够依据 src > modules 下目录构造主动生成入口文件 index.ts
主动构建入口文件外围代码:
const fs = require('fs') // node fs 模块
const chalk = require('chalk') // 自定义输入款式
const {resolveFile, getEntries} = require('./utils')
let srcIndexContent = `
// tips:此文件是主动生成的,无需手动增加
`
getEntries(resolveFile('src/modules/*')).forEach(({baseName, entry}) => {
let moduleIndexContent = `
// tips:此文件是主动生成的,无需手动增加
`
try {
// 判断是否文件夹
const stats = fs.statSync(entry)
if (stats.isDirectory()) {getEntries(`${entry}/*.ts`).forEach(({baseName}) => {baseName = baseName.split('.')[0]
if (baseName.indexOf('index') === -1) {
moduleIndexContent += `
export * from './${baseName}'
`
}
})
fs.writeFileSync(`${entry}/index.ts`, moduleIndexContent, 'utf-8')
srcIndexContent += `
export * from './modules/${baseName}'
export * as ${baseName} from './modules/${baseName}'
`
} else {
srcIndexContent += `
export * from './modules/${baseName.split('.')[0]}'
`
}
} catch (e) {console.error(e)
}
})
fs.writeFileSync(resolveFile('src/index.ts'), srcIndexContent, 'utf-8')
2) 针对颗粒度的问题,能够将 modules 下各种类型工具文件夹上面也主动生成入口文件,除了全副导出,再追加 import * as 模块类名称 类型的导出
至此,基本上解决了工具库打包的问题,然而架构中还短少本地开发调试的环境,上面为大家介绍如何架构中增加本地开发调试的零碎。
本地开发调试零碎
首先要明确要退出本地开发调试零碎的反对,须要做到以下:
• 跨平台(window 不反对 NODE_ENV=xxx)设置环境变量,依据环境配置不同的 rollup 配置项
• 引入本地开发须要的 html 动态服务器环境,并能做到热更新
1) 跨平台设置环境变量很简略,应用 cross-env 指定 node 的环境
yarn add cross-env -D
2) 配置 package.json 命令
"scripts": {
"entry": "node ./scripts/build-entry.js",
"dev": "rimraf lib && yarn entry && cross-env NODE_ENV=development rollup -w -c ./scripts/rollup.config.js", // -w 示意监听的工具模块的批改
"build": "rimraf lib && yarn entry && cross-env NODE_ENV=production rollup -c ./scripts/rollup.config.js"
},
3) 依据最开始架构设计的模块,在我的项目根目录新建 debugger 文件夹,外面寄存的是工具调试的 html 动态页面
4) 接下来就是配置 scripts > rollup.config.js,将 NODE_ENV=development 环境退出 rollup 配置,批改生成 rollup 配置项函数外围代码:
(isProd ? buildType : devType).forEach(({format, ext}) => {let plugins = [...defaultPlugins]
if (isProd) {
// 生产环境退出包剖析以及代码压缩
plugins = [...plugins, visualizer({
gzipSize: true,
brotliSize: true
}), terser()]
} else {
// 非生产环境退出热更新和本地服务插件,不便本地 debugger
plugins = [...plugins, ...[
// 热更新
rollUpLiveLoad({watch: ['debugger', 'lib'],
delay: 300
}),
// 本地服务代理
rollupServe({
open: true,
// resolveFile('') 代理根目录起因是为了在 ts 代码里 debugger 时能够不便看到调试信息
contentBase: [resolveFile('debugger'), resolveFile('lib'), resolveFile('')]
})
]]
}
pushPlugins({format, plugins, ext})
})
5) 执行 yarn dev 之后浏览器会新关上窗口,输出刚增加的工具链接,并且它是热更新的:
工具库文档零碎
一个齐备的工具库须要有一个文档来展现开发的工具函数,它可能须要具备以下几点反对:
• 反对工具库中办法的可视化预览
• 反对批改工具的时候,具备热更新机制
typedoc(TypeScript 我的项目的文档生成器)能完满反对 typescript 开发工具库的文档生成器的反对,它的外围原理就是读取源代码,依据工具的正文、ts 的类型标准等,主动生成文档页面
对于热更新机制的反对,第一个天然想到 browser-sync(文档零碎热更新)
因为文档零碎的预览性能有很多插件组合来实现的,能够借助 gulp(基于流的自动化构建工具),typedoc 正好有对应的 gulp-typedocGulp 插件来执行 TypeDoc 工具插件
构建实现后打开文档零碎,并且它是热更新的,批改工具办法后自动更新文档:
单元测试
为确保用户应用的工具代码的安全性、正确性以及可靠性,工具库的单元测试必不可少。单元测试选用的是 Facebook 出品的 Jest 测试框架,它对于 TypeScript 有很好的反对。
1. 环境搭建
1) 首先全局装置 jest 应用 init 来初始化 jest 配置项
npm jest -g
jest --init
上面是自己设置的 jest 的配置项
✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls, instances and results before every test? … yes
执行完之后根目录会主动生成 jest.config.ts 文件,外面设置了单元测试的配置规定,package.json 外面也多了一个 script 指令 test。
2) 对于 jest.config.js 文件配置项具体含意能够查看官网,要想实现 jest 对于 TypeScript 的测试,还须要装置一些依赖:
yarn add jest ts-jest @babel/preset-typescript @types/jest -D
3) jest 还须要借助 .babelrc 去解析 TypeScript 文件,再进行测试,编辑 .babelrc 文件,增加依赖包 @babel/preset-typescript:
{
"presets": [
"@babel/preset-typescript",
["@babel/preset-env"]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
2. 单元测试文件的编写
1) 通过以上环节,jest 单元测试环境根本搭建结束,接下来在 \_\_tests\_\_下编写测试用例
2) 执行 yarn test
能够看到对于 debounce 防抖工具函数的测试状况显示在了控制台:
• stmts 是语句覆盖率(statement coverage):是不是每个语句都执行了?
• Branch 分支覆盖率(branch coverage):是不是每个 if 代码块都执行了?
• Funcs 函数覆盖率(function coverage):是不是每个函数都调用了?
• Lines 行覆盖率(line coverage):是不是每一行都执行了?
3) 同时还会发现我的项目根目录多了一个 coverage 文件夹,外面就是 jest 生成的测试报告:
3. 单元测试文件的编写引发的思考
每次批改单元测试都要执行 yarn test 去查看测试后果,怎么解决?
jest 提供了 watch 指令,只须要配置 scripts 脚本就能够做到,单元测试的热更新。
"scripts": {"test": "jest --watchAll"},
当前会写很多工具的测试用例,每次 test 都将所有工具都进行了测试,是否只测试本人写的工具?
jest 也提供了测试单个文件的办法,这样 jest 只会对防抖函数进行测试(前提全局装置了 jest)。
jest debounce.test.ts --watch
工具库包的公布
至此工具库间隔开发者应用仅一步之遥,就是公布到 npm 上,发包前须要在 package.json 中申明库的一些入口,关键词等信息。
"main": "lib/main.js", // 告知援用该包模块化形式的默认文件门路
"module": "lib/main.esm.js", // 告知援用该包模块化形式的文件门路
"types": "lib/types/index.d.ts", // 告知援用该包的类型申明文件门路
"sideEffects": false, // false 为了通知 webpack 我这个 npm 包里的所有文件代码都是没有副作用的
"files": [ // 开发者援用该包后 node_modules 包外面的文件
"lib",
"README.md"
],
"keywords": [
"typescript",
"utils-demo",
"utils"
],
"scripts": {"pub": "npm publish"},
登陆 npm,你会看到本人的 packages 外面有了刚刚公布的工具库包:
写在最初
以上就是作者整顿的从 0 到 1 构建基于本身业务的前端工具库的全过程,心愿能给浏览本文的开发人员带来一些新的想法与尝试。
在此基础上曾经胜利在京东 npm 源公布了利用于京东汽车前端的工具库 @jdcar/car-utils,并在各个业务线及零碎失去落地。
当然,架构优化之路也还远未完结,比方:打包构建的速度、本地开发按需构建、工具库脚手架化等,后续咱们也会基于本身业务以及一些新技术,继续深刻优化,在性能上进一步晋升,在性能上进一步丰盛。本文或存在一些通俗不足之处,也欢送大家评论指导。
参考资料
[1] rollup 英文文档(https://rollupjs.org/guide/en/#quick-start)
[2] rollup 中文文档(https://rollupjs.org/guide/zh/#introduction)
[3] Rollup.js 实战学习笔记(https://chenshenhai.github.io/rollupjs-note/)
[4] Rollup 打包工具的应用(https://juejin.cn/post/6844904058394771470)
[5] TypeScript、Rollup 搭建工具库(https://juejin.cn/post/6844904035309322254)
[6] 应用 rollup.js 封装各我的项目共用的工具包(https://juejin.cn/post/6993720790046736420)
[7] 如何开发一个基于 TypeScript 的工具库并主动生成文档(https://juejin.cn/post/6844903881030238221)
[8] 一款优雅、简洁的 JavaScript 测试框架(https://jestjs.io/zh-Hans/)