文档元信息
- 适用 Webpack 版本: 5.96.1+
- 最后更新时间: 2026-01-22
- 核心主题: ESM-first 库构建、output.module、package.json exports、Tree-shaking 友好
- 前置知识: Webpack 基础配置、模块化规范(CJS/ESM/UMD)
- 相关章节: 第3章(Webpack 核心概念)、第5章(Loader 与 Plugin)、第6章(性能优化)
| 维度 | v1 原版 | v2 更新版 |
|---|---|---|
| Webpack 版本 | 5.x 通用 | 5.96.1+(支持 output.module) |
| output.library | 仅介绍 UMD | 全面对比 commonjs/umd/module/library 类型 |
| ESM 支持 | 未涉及 | 深入讲解 output.module + experiments.outputModule |
| package.json | main + module 简单配置 | 完整 exports 条件导出 + 最佳实践 |
| externals | 基础用法 | 新增 Node.js built-ins 处理策略 |
| Tree-shaking | 未涉及 | sideEffects、#PURE 完整指南 |
| 构建模板 | 单一 UMD 模板 | UMD / CJS / ESM / Universal 四种完整模板 |
| 可视化 | 无 | Mermaid 图表(矩阵图 + 对应关系图) |
| 目标读者 | 入门级 | 中高级 + 库作者实战指南 |
虽然 Webpack 多数情况下被用于构建 Web 应用,但与 Rollup、Snowpack 等工具类似,Webpack 同样具有完备的构建 NPM 库的能力。与一般场景相比,构建 NPM 库时需要遵循以下核心设计原则:
output.library 配置项,以适当方式导出模块内容package.json 的 exports 字段精确控制入口.d.ts TypeScript 类型文件lib/utils)为方便讲解,假定我们正在开发一个全新的 NPM 库,暂且叫它 test-lib 吧,首先需要创建并初始化项目:
mkdir test-lib && cd test-lib
npm init -y虽然有很多构建工具能够满足 NPM 库的开发需求,但现在暂且选择 Webpack,所以需要先装好基础依赖:
yarn add -D webpack webpack-cli接下来,可以开始写一些代码了,首先创建代码文件:
mkdir src
touch src/index.js之后,在 test-lib/src/index.js 文件中随便实现一些功能,比如:
// test-lib/src/index.js
export const add = (a, b) => a + b
export const multiply = (a, b) => a * b至此,项目搭建完毕,目录如下:
├─ test-lib
│ ├─ package.json
│ ├─ src
│ │ └─ index.js提示:本文代码均已上传到 本系列仓库。
接下来,我们需要将上例 test-lib 构建为适合分发的产物形态。虽然 NPM 库与普通 Web 应用在形态上有些区别,但大体的编译需求趋同,因此可以复用前面章节介绍过的大多数知识点。
test-lib 所需要的基础编译配置如下:
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
}
};提示:我们还可以在上例基础上叠加任意 Loader、Plugin,例如:
babel-loader、eslint-loader、ts-loader等。
上述配置会将代码编译成一个 IIFE 函数,但这并不适用于 NPM 库,我们需要修改 output.library 配置,以适当方式导出模块内容:
module.exports = {
// ...
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
+ library: {
+ name: "TestLib",
+ type: "umd",
+ },
},
// ...
};output.library.type 是最核心的配置项,用于指定编译产物的模块化方案。Webpack 5.96.1+ 支持以下类型:
| 类型 | 输出格式 | 适用场景 | Tree-shaking 兼容 | 推荐指数 |
|---|---|---|---|---|
commonjs | CommonJS(exports["..."] =) | Node.js 环境、老项目兼容 | ❌ 不支持 | ⭐⭐⭐ |
commonjs2 | CommonJS2 (module.exports =) | Node.js 环境(更常用) | ❌ 不支持 | ⭐⭐⭐⭐ |
umd | UMD (Universal Module Definition) | 浏览器 <script> 标签、CDN 引入 | ❌ 不支持 | ⭐⭐⭐⭐⭐ |
module | ES Module (export ...) | 现代浏览器、ESM-first 工具链 | ✅ 完全支持 | ⭐⭐⭐⭐⭐ |
var | 全局变量挂载 | 简单脚本、非模块化环境 | ❌ 不支持 | ⭐⭐ |
jsonp | JSONP 异步加载 | 跨域动态加载 | ❌ 不支持 | ⭐ |
assign | 属性赋值 | 特殊场景 | ❌ 不支持 | ⭐ |
// webpack.config.js
output: {
library: {
type: 'commonjs2', // 或 'commonjs'
}
}产物示例:
module.exports = (() => {
// ... 模块代码
return { add, multiply };
})();特点:
commonjs 使用 exports["TestLib"] = ...commonjs2 使用 module.exports = ...(更常用)适用场景:
// webpack.config.js
output: {
library: {
name: 'TestLib',
type: 'umd',
}
}产物示例:
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["TestLib"] = factory();
else
root["TestLib"] = factory();
})(self, function() {
return { add: (a, b) => a + b, multiply: (a, b) => a * b };
});特点:
<script> 标签直接引入适用场景:
使用示例:
<!-- HTML 中直接使用 -->
<script src="https://unpkg.com/test-lib/dist/main.js"></script>
<script>
TestLib.add(1, 2); // 通过全局变量访问
</script>// ES Module
import { add } from 'test-lib';
// CommonJS
const { add } = require('test-lib');// webpack.config.js
module.exports = {
experiments: {
outputModule: true, // 启用实验性特性
},
output: {
library: {
type: 'module',
},
filename: '[name].mjs', // 建议使用 .mjs 扩展名
},
};产物示例:
// 输出纯 ES Module 格式
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;特点:
export 关键字<script type="module"> 的原生懒加载experiments.outputModule: true适用场景:
生产就绪状态(截至 2026-01):
experiments.outputModule// webpack.config.js
output: {
library: {
name: 'TestLib',
type: 'var', // 默认值,挂载为全局变量
}
}产物示例:
var TestLib = (function() {
return { add: (a, b) => a + b };
})();特点:
适用场景:
library.name 用于定义模块名称,主要影响以下场景:
window[library.name]output: {
library: {
name: ['MyCompany', 'Utils', 'Math'],
type: 'umd',
}
}效果:生成层级命名空间 window.MyCompany.Utils.Math
// 访问方式
MyCompany.Utils.Math.add(1, 2);对于 module 类型,通常不需要 name:
output: {
library: {
type: 'module',
// 不设置 name,因为 ESM 使用命名导出
}
}默认情况下,Webpack 会导出整个 entry point。但你可以使用 library.export 控制导出的内容:
// src/index.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export default { add, multiply };
// webpack.config.js
output: {
library: {
name: 'TestLib',
type: 'umd',
export: 'default', // 只导出 default
}
}可选值:
'default':只导出默认导出['add', 'multiply']:只导出指定的命名导出(数组形式)⚠️ 重要提示(2026 年重点方向)
随着 JavaScript 生态系统的演进,ESM-first 已成为构建 NPM 库的主流趋势。主流框架和工具(React 19、Vue 3.6、Vite 6、Rollup 4)都已采用或推荐 ESM-first 策略。
Webpack 从 5.96.1 开始正式支持
output.module,标志着 ESM 库构建进入成熟阶段。
从 Webpack 5.96.1 开始,可以直接使用 output.module: true 来启用 ES Module 输出:
// webpack.config.js(ESM-first 配置)
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'index.mjs', // 使用 .mjs 扩展名标识 ESM
path: path.resolve(__dirname, 'dist'),
library: {
type: 'module', // ES Module 输出
},
module: true, // 启用 ESM 输出(Webpack 5.96.1+)
},
experiments: {
outputModule: true, // 兼容旧版本的实验性特性开关
},
};关键配置说明:
| 配置项 | 值 | 说明 |
|---|---|---|
output.filename | index.mjs | 使用 .mjs 扩展名明确标识 ESM 文件 |
output.library.type | 'module' | 指定输出格式为 ES Module |
output.module | true | (5.96.1+)正式启用 ESM 输出 |
experiments.outputModule | true | (5.0-5.95)实验性特性开关 |
outputModule 经历了从实验性特性到稳定特性的演进过程:
| 版本 | 状态 | 说明 |
|---|---|---|
| Webpack 5.0 - 5.50 | 🧪 实验性 | 需要显式开启 experiments.outputModule: true |
| Webpack 5.51 - 5.95 | 🔄 改进期 | API 稳定化,修复边界情况,增加错误提示 |
| Webpack 5.96+ | ✅ 正式稳定 | 可直接使用 output.module: true,无需 experiments |
| Webpack 5.107(最新) | 🚀 生产就绪 | 完整支持,官方推荐用于新项目 |
// 方式一:检查 Webpack 版本(推荐)
const webpackVersion = require('webpack/package.json').version;
module.exports = {
// ...
...(webpackVersion.startsWith('5.') && parseInt(webpackVersion.split('.')[1]) < 96
? { experiments: { outputModule: true } }
: { output: { module: true } }),
};
// 方式二:统一使用(向后兼容)
module.exports = {
output: {
module: true, // 新版本识别此配置
},
experiments: {
outputModule: true, // 旧版本需要此配置
},
};package.json 的 exports 字段是 Node.js 官方支持的条件导出机制,也是现代 NPM 库的标准配置方式。
{
"name": "test-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}注意:条件按声明顺序匹配,
types条件应放在最前,确保 TypeScript 优先命中类型声明。
条件优先级(按顺序匹配):
| 条件 | 触发场景 | 示例 |
|---|---|---|
"import" | ESM import 语句 | import lib from 'test-lib' |
"require" | CommonJS require() | const lib = require('test-lib') |
"types" | TypeScript 类型导入 | import type { Lib } from 'test-lib' |
"default" | 兜底条件 | 以上都不匹配时使用 |
{
"name": "modern-esm-lib",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"files": ["dist"]
}优点:
缺点:
require() 加载{
"name": "dual-format-lib",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}优点:
缺点:
{
"name": "awesome-components",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./Button": {
"import": "./dist/Button.mjs",
"require": "./dist/Button.cjs",
"types": "./dist/Button.d.ts"
},
"./Input": {
"import": "./dist/Input.mjs",
"require": "./dist/Input.cjs",
"types": "./dist/Input.d.ts"
},
"./styles": "./dist/styles.css",
"./package.json": "./package.json"
},
"sideEffects": [
"**/*.css",
"**/*.scss"
]
}使用示例:
// 按需导入单个组件(Tree-shaking 友好)
import { Button } from 'awesome-components/Button';
import { Input } from 'awesome-components/Input';
// 导入样式
import 'awesome-components/styles';当 exports 字段存在时,它会覆盖其他传统字段:
exports > main/module/browser/types具体表现:
| 字段 | 是否被 exports 覆盖 | 说明 |
|---|---|---|
exports | - | 最高优先级,存在时其他字段失效 |
main | ✅ 被 exports 覆盖 | 传统 CJS 入口 |
module | ✅ 被 exports 覆盖 | 非标准 ESM 入口(Rollup 提出) |
browser | ✅ 被 exports 覆盖 | 浏览器特定入口 |
types | ✅ 被 exports 覆盖 | TypeScript 类型入口 |
{
"name": "my-awesome-lib",
"version": "2.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"sideEffects": false,
"engines": {
"node": ">=14.18"
}
}配置说明:
main/module:兼容不支持 exports 的旧工具exports 为主:现代工具优先读取此字段type: "module":声明项目为 ESM 项目sideEffects: false:标记库无副作用,允许 Tree-shakingengines:声明最低 Node.js 版本要求假设我们需要在 test-lib 中使用其它 NPM 包,例如 lodash:
// src/index.js
import _ from "lodash";
export const add = (a, b) => a + b;
export const max = _.max;此时执行编译命令 npx webpack,我们会发现产物文件的体积非常大:
这是因为 Webpack 默认会将所有第三方依赖都打包进产物中,这种逻辑能满足 Web 应用资源合并需求,但在开发 NPM 库时则很可能导致代码冗余。以 test-lib 为例,若使用者在业务项目中已经安装并使用了 lodash,那么最终产物必然会包含两份 lodash 代码!
为解决这一问题,我们需要使用 externals 配置项,将第三方依赖排除在打包系统之外:
// webpack.config.js
module.exports = {
// ...
+ externals: {
+ lodash: {
+ commonjs: "lodash",
+ commonjs2: "lodash",
+ amd: "lodash",
+ root: "_",
+ },
+ },
// ...
};提示: Webpack 编译过程会跳过 externals 所声明的库,并假定消费场景已经安装了相关依赖,常用于 NPM 库开发场景;在 Web 应用场景下则常被用于优化性能。
例如,我们可以将 React 声明为外部依赖,并在页面中通过
<script>标签方式引入 React 库,之后 Webpack 就可以跳过 React 代码,提升编译性能。
改造后,再次执行 npx webpack,编译结果如下:
改造后,主要发生了两个变化:
test-lib 库代码,体积相比修改前大幅降低;require、define 函数中引入 lodash 依赖并传递到 factory。至此,Webpack 不再打包 lodash 代码,我们可以顺手将 lodash 声明为 peerDependencies:
{
"name": "6-1_test-lib",
// ...
+ "peerDependencies": {
+ "lodash": "^4.17.21"
+ }
}实践中,多数第三方框架都可以沿用上例方式处理,包括 React、Vue、Angular、Axios、Lodash 等,方便起见,可以直接使用 webpack-node-externals 排除所有 node_modules 模块,使用方法:
// webpack.config.js
const nodeExternals = require('webpack-node-externals');
module.exports = {
// ...
+ externals: [nodeExternals()]
// ...
};⚠️ Webpack 5 重要变更
Webpack 5 不再自动 polyfill Node.js 核心模块(如
crypto、fs、path等)。如果你的库使用了这些模块,需要显式配置。
// src/index.js
import crypto from 'crypto';
export function hash(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}Webpack 5 错误信息:
Module not found: Error: Can't resolve 'crypto' in '...'方案一:标记为 external(推荐用于库开发)
// webpack.config.js
module.exports = {
target: 'node', // 或 'node-current'、'async-node'
externals: [
nodeExternals({
allowlist: [], // 允许打包的模块(一般留空)
}),
],
};方案二:使用 polyfill(需要兼容浏览器时)
// webpack.config.js
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
module.exports = {
plugins: [
new NodePolyfillPlugin(),
],
};方案三:手动 fallback
// webpack.config.js
module.exports = {
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer/'),
},
},
};// webpack.config.js
const isNodeTarget = process.env.TARGET === 'node';
module.exports = {
target: isNodeTarget ? ['node', 'es2020'] : ['web', 'es2020'],
externals: isNodeTarget ? [nodeExternals()] : {},
resolve: {
fallback: isNodeTarget ? {} : {
crypto: false,
stream: false,
},
},
};webpack-node-externals 是专门用于排除 node_modules 的工具:
yarn add -D webpack-node-externalsconst nodeExternals = require('webpack-node-externals');
module.exports = {
externals: [nodeExternals()],
};externals: [nodeExternals({
allowlist: [/^lodash/, /^@babel/], // 白名单:这些模块仍然被打包
importType: 'commonjs2', // 外部模块的类型
modulesDir: path.resolve(__dirname, 'node_modules'), // node_modules 路径
modulesFromFile: true, // 从 package.json 读取依赖
})],常用场景:
| 选项 | 值 | 说明 |
|---|---|---|
allowlist | [] | 需要打包进产物的模块白名单 |
importType | 'commonjs2' | 外部模块的导入类型 |
modulesFromFile | true | 自动从 dependencies/peerDependencies 读取 |
假设我们开发的 NPM 库中包含了 CSS 代码 —— 这在组件库中特别常见,我们通常需要使用 mini-css-extract-plugin 插件将样式抽离成单独文件,由用户自行引入。
这是因为 Webpack 处理 CSS 的方式有很多,例如使用 style-loader 将样式注入页面的 <head> 标签;使用 mini-css-extract-plugin 抽离样式文件。作为 NPM 库开发者,如果我们粗暴地将 CSS 代码打包进产物中,有可能与用户设定的方式冲突。
为此,需要在前文基础上添加如下配置:
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
+ },
+ ],
+ },
+ plugins: [new MiniCssExtractPlugin({
+ filename: '[name].css',
+ })],
};提示:关于 CSS 构建的更多规则,可参考《如何借助预处理器、PostCSS 等构建现代 CSS 工程环境?》章节。
完整 CSS 处理配置(推荐):
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: {
localIdentName: '[local]__[hash:base64:5]',
},
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer', 'postcss-preset-env'],
},
},
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
};Sourcemap 是一种代码映射协议,它能够将经过压缩、混淆、合并的代码还原回未打包状态,帮助开发者在生产环境中精确定位问题发生的行列位置,所以一个成熟的 NPM 库除了提供兼容性足够好的编译包外,通常还需要提供 Sourcemap 文件。
接入方法很简单,只需要添加适当的 devtool 配置:
// webpack.config.js
module.exports = {
// ...
+ devtool: 'source-map'
};再次执行 npx webpack 就可以看到 .map 后缀的映射文件:
├─ test-lib
│ ├─ package.json
│ ├─ webpack.config.js
│ ├─ src
│ │ ├─ index.css
│ │ └─ index.js
│ └─ dist
│ ├─ main.js
│ ├─ main.js.map
│ ├─ main.css
│ └─ main.css.map此后,业务方只需使用 source-map-loader 就可以将这段 Sourcemap 信息加载到自己的业务系统中,实现框架级别的源码调试能力。关于 Sourcemap 的更多信息,可查阅:
提示:示例代码已上传到 本系列仓库。
| devtool 值 | 构建速度 | 质量 | 推荐场景 |
|---|---|---|---|
'eval' | 🚀 快 | ⭐ 低 | 开发环境 |
'eval-source-map' | 🚀 快 | ⭐⭐⭐ | 开发环境(初始加载慢) |
'cheap-source-map' | ⚡ 中等 | ⭐⭐ | 生产环境(不含列信息) |
'source-map' | 🐢 慢 | ⭐⭐⭐⭐⭐ | NPM 库发布(推荐) |
'hidden-source-map' | 🐢 慢 | ⭐⭐⭐⭐⭐ | 生产环境(不引用 .map) |
NPM 库推荐配置:
module.exports = {
devtool: 'source-map', // 生成独立的 .map 文件
// 其他配置...
};🎯 为什么重要?
Tree-shaking(摇树优化)是现代打包工具的核心优化技术,能够消除未使用的代码,显著减小产物体积。作为 NPM 库作者,编写 Tree-shaking 友好的代码是对用户的负责。
package.json 的 sideEffects 字段告诉打包工具你的代码是否有副作用。
副作用是指模块在被导入时会执行的、影响全局状态的代码,例如:
// ❌ 有副作用的代码
import './polyfill.js'; // 导入即执行
window.globalVar = 'value'; // 修改全局对象
Array.prototype.myMethod = function() {}; // 扩展原型链
// ✅ 无副作用的代码
export const add = (a, b) => a + b; // 纯函数定义
export class Utils {} // 类定义方式一:标记整个包无副作用(推荐)
{
"sideEffects": false
}方式二:指定有副作用的文件
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/register-components.js"
]
}{
"name": "my-utils",
"sideEffects": [
"**/*.css",
"**/*.scss",
"./register.ts"
]
}其中 **/*.css、**/*.scss 为有副作用的样式文件,./register.ts 为注册全局组件的文件。
// src/index.js
// 这些导出都是无副作用的,可以被 Tree-shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// 如果用户只 import { add },subtract 会被移除当 Webpack 无法确定某个调用是否有副作用时,可以使用 #__PURE__ 注解辅助判断。
// src/utils.js
// ❌ Webpack 不确定是否有副作用,可能不会被 Tree-shaking
const result = someFunction();
// ✅ 明确告诉 Webpack 这是纯调用,可以安全移除
const result = /*#__PURE__*/someFunction();// 创建对象
const config = /*#__PURE__*/({
key: 'value',
foo: 'bar',
});
// 函数调用
const result = /*#__PURE__*/calculateSomething(a, b);
// 条件表达式
const value = /*#__PURE__*/condition ? x : y;#__PURE__ 注解也可以由编译/压缩工具自动生成或保留:Babel 在转换已知纯调用(如 helper 调用)时会补充注解,压缩阶段(如 Terser)会基于自身分析生成注解。源码中无法被工具识别的纯调用仍需手动书写。Babel 配置中保持 ESM 输出即可:
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
modules: false, // 保持 ESM 以便 Tree-shaking
}],
],
};不同的导出方式对 Tree-shaking 的影响不同:
// src/math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
// 使用方
import { add } from './math'; // 只会打包 add优点:
// src/math.js
export default {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
// 使用方
import math from './math';
math.add(1, 2); // 整个对象都会被打包问题:
babel-plugin-transform-default-import)// src/math.js(内部实现)
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
// 同时支持默认导出和命名导出
export { add, multiply };
export default { add, multiply };
// 使用方可以选择任一方式
import { add } from './math'; // ✅ Tree-shaking 友好
import math from './math'; // ⚠️ 会打包全部// src/index.js(主入口)
// 所有子模块都使用命名导出
export { add, multiply, divide } from './math';
export { format, parse } from './string';
export { fetchJSON, postData } from './http';
// 默认导出聚合所有功能(可选)
export default {
math: { add, multiply, divide },
string: { format, parse },
http: { fetchJSON, postData },
};下面的图表展示了不同构建目标与输出格式的对应关系:
理解 package.json 字段与 Webpack 配置的对应关系,有助于构建规范的 NPM 库:
| package.json 字段 | 作用 | 对应 Webpack 配置 | 优先级 |
|---|---|---|---|
"main" | CJS 入口 | output.library.type: 'commonjs2' | 低(被 exports 覆盖) |
"module" | ESM 入口(非标准) | output.library.type: 'module' | 低(被 exports 覆盖) |
"exports" | 条件导出 | 多个 output 配置组合 | 最高 |
"browser" | 浏览器替代 | target: 'web' | 中 |
"types" | TS 类型入口 | 输出 .d.ts 文件 | 独立 |
"sideEffects" | 副作用标记 | 影响 optimization.usedExports | 独立 |
"peerDependencies" | Peer 依赖 | externals 配置 | 独立 |
"files" | 发布白名单 | 影响 output.path 内容 | 独立 |
"engines" | 版本要求 | 影响目标平台选择 | 独立 |
适用场景:组件库、需要 CDN 引入的库、最大兼容性需求
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'index.umd.js',
path: path.resolve(__dirname, 'dist'),
library: {
name: 'TestLib',
type: 'umd',
export: 'default',
},
globalObject: 'this',
umdNamedDefine: true,
},
externals: {
react: {
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
root: 'React',
},
'react-dom': {
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM',
},
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }],
'@babel/preset-react',
],
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'index.css',
}),
],
devtool: 'source-map',
optimization: {
minimize: true,
},
};{
"name": "test-lib-umd",
"version": "1.0.0",
"main": "dist/index.umd.js",
"module": "dist/index.esm.js",
"browser": "dist/index.umd.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "webpack --config webpack.config.umd.js",
"build:prod": "NODE_ENV=production webpack --config webpack.config.umd.js"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"devDependencies": {
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"babel-loader": "^9.1.3",
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-react": "^7.24.0",
"mini-css-extract-plugin": "^2.8.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.0"
}
}<!-- CDN 引入 -->
<script src="https://unpkg.com/test-lib-umd/dist/index.umd.js"></script>
<script>
TestLib.add(1, 2);
</script>// CommonJS
const TestLib = require('test-lib-umd');
// ES Module
import TestLib from 'test-lib-umd';适用场景:Node.js CLI 工具、SSR 库、服务端中间件
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
mode: 'production',
target: ['node', 'es2020'],
entry: './src/index.js',
output: {
filename: 'index.cjs',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'commonjs2',
},
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: { node: 'current' },
modules: false,
}],
],
},
},
},
],
},
devtool: 'source-map',
optimization: {
minimize: false, // Node.js 通常不需要压缩
},
};{
"name": "test-lib-cjs",
"version": "1.0.0",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "webpack --config webpack.config.cjs.js"
},
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0",
"babel-loader": "^9.1.3"
}
}// CommonJS(Node.js)
const { add, multiply } = require('test-lib-cjs');
console.log(add(1, 2)); // 3适用场景:2026 年新项目、前端库、工具函数库、追求 Tree-shaking
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
target: ['web', 'es2020'],
entry: './src/index.js',
output: {
filename: 'index.mjs',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'module',
},
module: true,
environment: {
module: true,
dynamicImport: true,
},
},
experiments: {
outputModule: true, // Webpack < 5.96 需要
},
externalsType: 'module', // 外部依赖也使用 ESM
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead',
modules: false, // 保持 ESM!重要!
bugfixes: true,
}],
],
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'index.css',
}),
],
devtool: 'source-map',
optimization: {
usedExports: true,
innerGraph: true,
sideEffects: true,
},
};{
"name": "test-lib-esm",
"version": "2.0.0",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
},
"./styles": "./dist/index.css"
},
"types": "dist/index.d.ts",
"files": ["dist"],
"sideEffects": [
"**/*.css"
],
"scripts": {
"build:esm": "webpack --config webpack.config.esm.js",
"build:cjs": "webpack --config webpack.config.cjs.js",
"build": "npm run build:esm && npm run build:cjs"
},
"engines": {
"node": ">=14.18"
},
"devDependencies": {
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"mini-css-extract-plugin": "^2.8.0",
"babel-loader": "^9.1.3"
}
}// ES Module(完美 Tree-shaking)
import { add } from 'test-lib-esm'; // 只导入需要的函数
import 'test-lib-esm/styles'; // 单独导入样式
console.log(add(1, 2));适用场景:大型开源库、需要最大兼容性的企业级库
const path = require('path');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const nodeExternals = require('webpack-node-externals');
const baseConfig = {
mode: 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
modules: false, // 保持 ESM
}],
],
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
devtool: 'source-map',
};
const esmConfig = merge(baseConfig, {
target: ['web', 'es2020'],
output: {
filename: 'index.mjs',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'module',
},
module: true,
},
experiments: {
outputModule: true,
},
externalsType: 'module',
optimization: {
usedExports: true,
},
});
const cjsConfig = merge(baseConfig, {
target: ['node', 'es2020'],
output: {
filename: 'index.cjs',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'commonjs2',
},
},
externals: [nodeExternals()],
});
const umdConfig = merge(baseConfig, {
target: ['web', 'es2020'],
output: {
filename: 'index.umd.js',
path: path.resolve(__dirname, 'dist'),
library: {
name: 'TestLib',
type: 'umd',
},
globalObject: 'this',
},
externals: {
react: {
commonjs2: 'react',
amd: 'react',
root: 'React',
},
},
});
module.exports = [esmConfig, cjsConfig, umdConfig];{
"name": "test-lib-universal",
"version": "3.0.0",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"browser": "dist/index.umd.js",
"unpkg": "dist/index.umd.js",
"jsdelivr": "dist/index.umd.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
},
"./styles": "./dist/index.css",
"./package.json": "./package.json"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css",
"**/*.scss"
],
"scripts": {
"build": "webpack --config webpack.config.universal.js",
"build:dev": "webpack --mode=development --config webpack.config.universal.js",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"devDependencies": {
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.10.0",
"webpack-node-externals": "^3.0.0",
"mini-css-extract-plugin": "^2.8.0",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0"
}
}dist/
├── index.mjs # ES Module 格式
├── index.mjs.map # ESM Sourcemap
├── index.cjs # CommonJS 格式
├── index.cjs.map # CJS Sourcemap
├── index.umd.js # UMD 格式(浏览器/CDN)
├── index.umd.js.map # UMD Sourcemap
├── index.css # 样式文件
├── index.css.map # CSS Sourcemap
└── index.d.ts # TypeScript 类型声明// 方式 1:ES Module(推荐,支持 Tree-shaking)
import { add } from 'test-lib-universal';
// 方式 2:CommonJS
const { add } = require('test-lib-universal');
// 方式 4:单独导入样式
import 'test-lib-universal/styles';<!-- 方式 3:HTML Script 标签 -->
<script src="https://unpkg.com/test-lib-universal/dist/index.umd.js"></script>
<script>TestLib.add(1, 2)</script>至此,开发 NPM 库所需的 Webpack 配置就算是介绍完毕了,接下来我们还可以用一些小技巧优化 test-lib 的项目配置,提升开发效率,包括:
使用 .npmignore 文件忽略不需要发布到 NPM 的文件:
src/
*.ts
*.map
webpack.config.*
.babelrc
.vscode/
.idea/
.github/
tests/
__tests__/
coverage/
*.log
.DS_Store注意:如果存在
.npmignore,它会覆盖.gitignore的规则。如果没有.npmignore,则使用.gitignore。
在 package.json 文件中,使用 prepublishOnly 指令,在发布前自动执行编译命令:
{
"name": "test-lib",
"scripts": {
"build": "webpack --mode=production",
"prepublishOnly": "npm run build",
"prepublishOnly:cjs": "webpack --config webpack.config.cjs.js --mode=production",
"prepublishOnly:esm": "webpack --config webpack.config.esm.js --mode=production"
}
}显式指定发布的文件列表(比 .npmignore 更精确):
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}{
"name": "@scope/my-awesome-library",
"version": "1.0.0",
"description": "An awesome library built with Webpack 5",
"license": "MIT",
"author": "Your Name <email@example.com>",
"repository": {
"type": "git",
"url": "https://github.com/username/my-awesome-library.git"
},
"keywords": ["utility", "library", "esm", "tree-shakable"],
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
},
"./styles": "./dist/style.css",
"./package.json": "./package.json"
},
"files": ["dist", "README.md", "LICENSE"],
"sideEffects": ["**/*.css", "**/*.scss"],
"scripts": {
"build": "webpack --mode=production",
"build:dev": "webpack --mode=development",
"watch": "webpack --watch",
"clean": "rm -rf dist",
"prepublishOnly": "npm run clean && npm run build",
"test": "jest",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=17.0.0"
},
"devDependencies": {
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"typescript": "^5.4.0",
"@types/react": "^18.2.0",
"babel-loader": "^9.1.3",
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-react": "^7.24.0",
"@babel/preset-typescript": "^7.24.0",
"mini-css-extract-plugin": "^2.8.0",
"css-loader": "^7.1.0",
"postcss-loader": "^8.1.0",
"sass-loader": "^14.2.0",
"webpack-merge": "^5.10.0",
"clean-webpack-plugin": "^4.0.0",
"eslint": "^8.57.0",
"jest": "^29.7.0"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}站在 Webpack 角度,构建 Web 应用于构建 NPM 库的差异并不大,开发时注意:
output.library 配置项,正确选择 type(module/umd/commonjs2)externals 配置项,忽略第三方库(配合 peerDependencies)mini-css-extract-plugin 单独打包 CSS 样式代码devtool: 'source-map' 配置项生成 Sourcemap 文件package.json 的 exports 字段实现条件导出sideEffects 标记,支持 Tree-shaking.d.ts)| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 全新库项目 | ESM-first + CJS fallback | 符合行业趋势,兼顾兼容性 |
| React/Vue 组件库 | UMD + ESM 双格式 | 需要 CDN 支持 + Tree-shaking |
| Node.js 工具库 | Pure ESM 或 Pure CJS | 运行环境单一 |
| 企业内部库 | Universal(三格式) | 最大兼容性,降低接入门槛 |
| 小型工具函数 | Pure ESM | 体积最小,Tree-shaking 效果最好 |
如果你正在维护一个基于 v1 配置的老库,建议按以下步骤升级:
exports 字段(不影响现有用户)output.module: true)module 字段指向 ESM 产物type: "module",逐步引导用户迁移遵循上述规则,基本上就能满足开发一个 NPM 库所需的大部分需求。另外,文章代码均已上传到 本系列仓库,建议大家 Clone 阅读。
模块化规范对比:请结合实际场景,详细比较 CommonJS、UMD、ES Module 三种模块化规范的优劣势,以及它们在不同运行环境下的表现。
ESM-first 迁移挑战:如果要将一个现有的 UMD-only 库迁移为 ESM-first,可能会遇到哪些挑战?如何解决 Dual Package Hazard(双重包危害)问题?
构建工具选型:有许多工具能被构建 NPM 库,例如 Webpack、Snowpack、Vite、Rollup、tsup、unbuild 等,这些工具各有什么特点?在 2026 年,你更倾向于使用哪种工具?为什么?
Tree-shaking 深度实践:如何编写一个完全 Tree-shaking 友好的库?请列举至少 5 个常见的反模式(Anti-patterns),并提供解决方案。
exports 字段设计:为一个具有以下结构的复杂组件库设计 package.json 的 exports 字段:
@scope/lib/Button)// UMD(通用)
output: { library: { name: 'Lib', type: 'umd' } }
// CommonJS(Node.js)
output: { library: { type: 'commonjs2' } }
// ES Module(现代化)
output: { library: { type: 'module' }, module: true }
experiments: { outputModule: true }
// Global Variable(简单场景)
output: { library: { name: 'Lib', type: 'var' } }{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"sideEffects": false,
"files": ["dist"]
}npm run build
npm publish # 自动触发 prepublishOnly
npm link
cd ../test-project
npm link my-lib
ls -la dist/文档版本:v2.0.0 最后更新:2026-01-22 适用 Webpack:5.96.1+(推荐 5.107) 下一步学习:第8章(Webpack 性能优化深度实践)