关于前端:使用-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-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"
  },
}

执行测试
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,谢谢。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理