最近收到需求,需要开发一些针对业务特定公共逻辑部分使用的 JavaScript 函数(类似于开发一个公共 SDK),统一维护,同时供各业务部门的前端开发人员进行复用。
为了满足公共库开发调试简单、易用性与健壮性等需求,需要满足以下要求:
考虑到 Webpack5 已支持输出 ESM 文件结果,并且开发与调试简单、文档齐全等因素,决定采用 Webpack 作为模块构建与打包工具,同时配合 babel-loader(ES6+ 转 ES5)、ts-loader(支持 TypeScript)、Jest(单元测试)的技术方案。
本文将基于 Webpack 一步一步完成一个 JavaScript 工具库的搭建、开发、调试、打包与发布的基本流程,同时提供相关示例代码:https://github.com/hwjfqr/javascript-lib-demo 。
创建项目文件夹 & 安装 Webpack 相关包
mkdir javascript-lib-demo
cd javascript-lib-demo
pnpm init
pnpm i -D webpack webpack-cli webpack-dev-server
采用任意包管理工具皆可(npm、yarn),本文主要采用 pnpm 作为包管理工具。
创建 Webpack 配置文件,并指定打包入口与出口以及 mode :
webpack.config.js
const path = require('path')
/** @type {import('webpack').Configuration} */
const config = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
clean: true
}
}
module.exports = config
安装 babel-loader 与 ts-loader 相关依赖,支持 ES6+ 转 ES5 以及支持利用 TypeScript 语言开发工具库pnpm i -D @babel/core @babel/preset-env babel-loader typescript ts-loader
其中, @babel/core 为 babel 的核心依赖模块,@babel/preset-env 为 babel 提供的预设插件。
babel 本身只是一个平台,需要使用具体的插件才能实现转换,@babel/preset-env 主要用于将 ES6+ 语法转换为 ES5 。
初始化 TS 配置文件npx tsc --init
修改 Webpack 配置
webpack.config.js
const path = require('path')
/** @type {import('webpack').Configuration} */
const config = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
clean: true
},
// 使路径查找时,支持省略文件名的 ts 后缀。
resolve: {
extensions: ['.js', '.json', '.ts']
},
// Babel 与 TS 配置
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env']]
}
},
{ loader: 'ts-loader' }
]
}
]
}
}
module.exports = config
@babel/preset-env 仅支持对 ES6+ 语法进行转换,但对于一些 ES6+ API 是无法转换的(例如 Promise、Async/Await 等),如果对新 API 的兼容性有需求,请参考 core-js、@babel/preset-typescript 相关用法即可,本文不再赘述。
为便于后续结合 webpack dev server 使用,实现实时调试,引入了 html-webpack-plugin 插件,其在 Webpack 执行打包命令时会创建引入打包结果 JS 的 html 文件。pnpm i -D html-webpack-plugin
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/** @type {import('webpack').Configuration} */
const config = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
clean: true
},
resolve: {
extensions: ['.js', '.json', '.ts']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env']]
}
},
{ loader: 'ts-loader' }
]
}
]
},
plugins: [new HtmlWebpackPlugin()] // 引入 html-webpack-plugin
}
module.exports = config
本文以实现一个 calc 模块为例子,其包含 add、subtract、multiply、divide 等函数。
add.ts
function add(...args: number[]) {
return args.reduce((ac, cur) => ac + cur)
}
export default add
subtract.ts
function subtract(...args: number[]) {
return args.reduce((ac, cur) => ac - cur)
}
export default subtract
multiply.ts
function multiply(...args: number[]) {
return args.reduce((ac, cur) => ac * cur)
}
export default multiply
divide.ts
function divide(...args: number[]) {
return args.reduce((ac, cur) => ac / cur)
}
export default divide
src/calc/index.ts
import add from './add'
import subtract from './subtract'
import multiply from './multiply'
import divide from './divide'
const calc = {
add,
subtract,
multiply,
divide
}
export default calc
export { add, subtract, multiply, divide }
配置 package.json script 字段
package.json
{
// 其他配置已省略
"scripts": {
"dev": "npx webpack serve",
"build": "npx webpack"
},
}
在入口文件中调用函数,进行测试。
src/index.ts
import calc from './calc'
console.log(calc.add(1, 2))
console.log(calc.subtract(1, 2))
console.log(calc.multiply(1, 2))
console.log(calc.divide(1, 2))
启用 webpack dev server 服务,查看运行结果。npm run dev
单元测试是保障质量的有效手段,通过书写测试用例,使用测试框架即可自动化完成测试工作,从而使得每次改动都能通过之前所有的测试用例,防止因为改动破坏了某些功能。
本文采用 Jest 来实现自动化测试
安装 Jest 相关依赖pnpm i -D jest ts-jest @types/jest
初始化 Jest 配置文件npx ts-jest config:init
编写测试用例
./calc/index.test.ts
import { add, subtract, multiply, divide } from './index'
test('add test', () => {
expect(add(1, 2)).toBe(3)
})
test('subtract test', () => {
expect(subtract(1, 2)).toBe(-1)
})
test('multiply test', () => {
expect(multiply(1, 2)).toBe(2)
})
test('divide test', () => {
expect(divide(1, 2)).toBe(0.5)
})
**在 package.json 中添加测试 script **
package.json
{
// 其他配置已省略
"scripts": {
"dev": "npx webpack serve",
"build": "npx webpack",
"test": "jest ./src/calc/index.test.ts"
},
}
区分生产/开发环境
由于在打包时要将编写的工具库文件作为入口文件,因此需要对生产/开发环境进行区分。
通过 cross-env 修改环境变量来实现区分生产/开发环境。pnpm i -D cross-env
修改 package.json script 字段配置
package.json
"scripts": {
"dev": "npx webpack serve",
"build": "cross-env NODE_ENV=production webpack",
"test": "jest ./src/calc/index.test.ts"
},
打包输出 UMD 格式文件
修改 webpack 配置
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const isProduction = process.env.NODE_ENV === 'production' // 根据环境变量,判断当前是否为生产模式。
/** @type {import('webpack').Configuration} */
const config = {
// 根据环境变量决定 mode 的值
mode: isProduction ? 'production' : 'development',
entry: isProduction ? './src/calc/index.ts' : './src/index.ts',
// 输出 JavaScript 库
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
library: {
name: 'calc', // 指定库名称
type: 'umd', // 输出的模块化格式, umd 表示允许模块通过 CommonJS、AMD 或作为全局变量使用。
export: 'default' // 指定将入口文件的默认导出作为库暴露。
},
globalObject: 'globalThis', // 设置全局对象为 globalThis,使库同时兼容 Node.js 与浏览器环境。
clean: true
},
resolve: {
extensions: ['.js', '.json', '.ts']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env']]
}
},
{ loader: 'ts-loader' }
]
}
]
},
// html-webpack-plugin 只需在开发环境时使用。
plugins: [...(!isProduction ? [new HtmlWebpackPlugin()] : [])]
}
module.exports = config
此时,生成的文件已支持通过 CommonJS、AMD、浏览器全局变量(window)引用等多种引入方式。同时,在 Webpack 环境下,也支持通过 ESM 方式来引入此文件。
但当前的打包方式会将所有函数打包在一起,不利于 ESM Tree Shaking,因此,将利用 Webpack5 支持输出 ESM 格式文件的特性,单独输出文件的 ESM 格式版本。
通过输出 ESM 格式文件,实现 ESM 方式引入下的模块函数按需加载。
Webpack5 通过指定 output.library.type 值为 module,来实现输出 ESM 格式文件。
通过设置自定义环境变量(OUTPUT_TYPE),将输出 ESM 格式文件作为一个单独的任务。
修改 package.json script 配置
package.json
"scripts": {
"dev": "npx webpack serve",
"build": "npm run test & npm run generate:esm & npm run generate:umd",
"generate:umd": "cross-env NODE_ENV=production OUTPUT_TYPE=umd webpack --config ./webpack.config.js",
"generate:esm": "cross-env NODE_ENV=production OUTPUT_TYPE=esm webpack --config ./webpack.config.js",
"test": "jest ./src/calc/index.test.ts"
},
修改 Webpack 配置
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const isProduction = process.env.NODE_ENV === 'production'
const outputType = process.env.OUTPUT_TYPE // 读取当前的输出格式(UMD/ESM)
/** @type {import('webpack').Configuration} */
const config = {
mode: isProduction ? 'production' : 'development',
entry:
// 打包输出 ESM 格式文件,最终要输出多个文件,便于实现按需加载,因此设置为多入口。
outputType === 'esm'
? {
add: './src/calc/add.ts',
subtract: './src/calc/subtract.ts',
multiply: './src/calc/multiply.ts',
divide: './src/calc/divide.ts'
}
: isProduction
? './src/calc/index.ts'
: './src/index.ts',
// 由于输出 ESM 格式文件为 Webpack 实验特性,因此需要加上此配置。
experiments: {
outputModule: outputType === 'esm'
},
// 针对不同的环境变量,执行不同的打包动作。
output:
outputType === 'esm'
? // ESM
{
path: path.resolve(__dirname, 'es'),
filename: '[name].esm.js',
library: {
type: 'module'
},
chunkFormat: 'module',
clean: true
}
: // UMD
{
path: path.resolve(__dirname, 'lib'),
filename: 'index.js',
library: {
name: 'calc',
type: 'umd',
export: 'default'
},
globalObject: 'globalThis',
clean: true
},
resolve: {
extensions: ['.js', '.json', '.ts']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env']]
}
},
{ loader: 'ts-loader' }
]
}
]
},
plugins: [...(!isProduction ? [new HtmlWebpackPlugin()] : [])]
}
module.exports = config
其中 es 文件夹下的产物支持 ESM 格式引入,支持按需加载。lib 文件夹下的产物支持 CommonJS、AMD 以及全局变量引入。
引入方式示例如下:
CommonJS 引入
const calc = require('javascript-demo-lib')
浏览器 Script 标签引入
<script src="javascript-lib-demo/lib/index.js">script>
<script>
window.calc.add(1,2); // 结果为 3
script>
ESM 引入
// 整体引入
import calc from 'javascript-lib-demo'
// 按需加载引入
import add from 'javascript-lib-demo/es/add.esm'
配置生成 TS 类型声明文件,便于用户在使用库时进行相关的类型提示。
修改 TS 配置文件(tsconfig.json)
{
// 其他配置已省略
"compilerOptions": {
"declaration": true, // 指定生成类型声明文件
"declarationDir": "./types" // 指定类型声明文件的文件夹
},
// 指定需要排除的无需生成类型声明的相关文件
"exclude": [
"node_modules",
"**/*.d.ts",
"src/index.ts",
"src/calc/index.test.ts"
]
}
指定当前模块的入口文件、类型声明入口文件。
package.json
{
"main": "lib/index.js",
"typings": "types/index.d.ts",
}
如果需要将工具库开源,则可直接在 NPM 上发布使用,具体发布方式可参考:https://www.yuque.com/u109677/ncfyh7/phighc#m7LHO
下面主要针对私有工具库的发布方式进行说明:npm 支持通过 git 地址来实现包的安装,因此可以在私有 git (例如公司的 gitlab)中提交代码,然后通过 git tag
命令打上版本号标签,后续则可通过 pnpm i git+ssh://[email protected]:xxx/xxx.git#tagName
来安装使用。
本文的示例代码地址:https://github.com/hwjfqr/javascript-lib-demo
有任何疑问欢迎评论或提 issue,谢谢。