网络日志

使用 Webpack 构建 JavaScript 工具库

引言

最近收到需求,需要开发一些针对业务特定公共逻辑部分使用的 JavaScript 函数(类似于开发一个公共 SDK),统一维护,同时供各业务部门的前端开发人员进行复用。为了满足公共库开发调试简单、易用性与健壮性等需求,需要满足以下要求:

  • 支持 TypeScript;
  • 支持输出多种模块化文件(UMD、CommonJS、ESM 等),便于引入使用;
  • 支持按需加载(ESM Tree Shaking);
  • 支持自动化测试;
  • ......

考虑到 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 与 TS 配置

安装 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 相关用法即可,本文不再赘述。

引入 html-webpack-plugin 插件

为便于后续结合 webpack dev server 使用,实现实时调试,引入了 html-webpack-plugin 插件,其在 Webpack 执行打包命令时会创建引入打包结果 JS 的 html 文件。pnpm i -D html-webpack-pluginwebpack.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"
  },
}

执行测试npm run test

打包

区分环境 & 输出 UMD 格式文件

区分生产/开发环境由于在打包时要将编写的工具库文件作为入口文件,因此需要对生产/开发环境进行区分。通过 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

执行打包操作npm run build此时,生成的文件已支持通过 CommonJS、AMD、浏览器全局变量(window)引用等多种引入方式。同时,在 Webpack 环境下,也支持通过 ESM 方式来引入此文件。但当前的打包方式会将所有函数打包在一起,不利于 ESM Tree Shaking,因此,将利用 Webpack5 支持输出 ESM 格式文件的特性,单独输出文件的 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

执行打包操作npm run build其中 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 类型声明文件,便于用户在使用库时进行相关的类型提示。修改 TS 配置文件(tsconfig.json)

{
  // 其他配置已省略
  "compilerOptions": {
    "declaration": true, // 指定生成类型声明文件
    "declarationDir": "./types" // 指定类型声明文件的文件夹
  },
  // 指定需要排除的无需生成类型声明的相关文件
  "exclude": [
    "node_modules",
    "**/*.d.ts",
    "src/index.ts",
    "src/calc/index.test.ts"
  ]
}

执行打包操作,生成类型声明文件。npm run build

指定当前模块的入口文件、类型声明入口文件。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://git@github.com:xxx/xxx.git#tagName来安装使用。

示例代码地址

本文的示例代码地址:https://github.com/hwjfqr/javascript-lib-demo有任何疑问欢迎评论或提 issue,谢谢。