Skip to content

配置结构

Webpack 是一种 「配置」 驱动的构建工具

配置相关的各项知识点:

  1. 剖析配置结构规则,解释对象、数组、函数三种形态的写法,以及各自应对的场景
  2. 环境治理的意义,如何借助多文件实现环境治理
  3. 核心配置项:entry/output/target/mode,更深入理解配置规则

配置结构详解

Webpack 除了对象的方式,还支持以数组、函数方式配置运行参数,以适配不同场景应用需求,它们之间大致上区别:

  • 单个配置对象:比较常用的一种方式,逻辑简单,适合大多数业务项目;
  • 配置对象数组:每个数组项都是一个完整的配置对象,每个对象都会触发一次单独的构建,通常用于需要为同一份代码构建多种产物的场景,如 Library;
  • 函数:Webpack 启动时会执行该函数获取配置,我们可以在函数中根据环境参数(如 NODE_ENV)动态调整配置对象。

使用对象数组配置

导出数组的方式很简单:

js
// webpack.array.js
module.exports = [{
  entry: './src/index.js',
}, {
  entry: './src/index.js',
}]

使用数组方式时,Webpack 会在启动后创建多个 Compilation 实例,并行执行构建工作,但需要注意,Compilation 实例间基本上不作通讯,这意味着这种并行构建对运行性能并没有任何正向收益,例如某个 Module 在 Compilation 实例 A 中完成解析、构建后,在其它 Compilation 中依然需要完整经历构建流程,无法直接复用结果

数组方式主要用于应对 “同一份代码打包出多种产物” 的场景,例如在构建 Library 时,我们通常需要同时构建出 ESM/AMD/CMD/UMD 等模块方案的产物,如:

js
// webpack.array.js
const { resolve } = require('path')
module.exports = [{
  mode: 'production',
  output: {
    filename: 'dist-array-amd.js',
    path: resolve(__dirname, 'dist'),
    libraryTarget: 'amd',
  },
  name: 'amd',
  entry: './src/index.js',
}, {
  mode: 'production',
  output: {
    filename: 'dist-array-umd.js',
    path: resolve(__dirname, 'dist'),
    libraryTarget: 'umd',
  },
  name: 'umd',
  entry: './src/index.js',
}]

提示

使用配置数组时,还可以通过 --config-name 参数指定需要构建的配置对象,例如上例配置中若执行 npx webpack --config-name='amd',则仅使用数组中 name='amd' 的项做构建。

若是“多份代码打包多份产物”的场景,则建议使用 entry 配置多个应用入口。

使用数组方式时,我们还可以借助 webpack-merge 工具简化配置逻辑,webpack-mergeWebpack 生态内专门用于合并配置对象的工具。如:

js
// webpack.array.js
const { merge } = require('webpack-merge')

const baseConfig = {
  mode: 'production',
  output: {
    path: resolve(__dirname, 'dist'),
  },
  entry: './src/index.js',
}

module.exports = [
  merge(baseConfig, {
    output: {
      filename: 'merge-array-umd.js',
      libraryTarget: 'umd',
    },
    name: 'umd',
  }),
  merge(baseConfig, {
    output: {
      filename: 'merge-array-amd.js',
      libraryTarget: 'amd',
    },
    name: 'amd',
  })
]

将公共配置抽取为 baseConfig 对象,之后配合 webpack-merge 创建不同目标数组项,这种方式可有效减少重复的配置代码。

使用函数的配置方式

配置函数方式要求在配置文件中导出一个函数,并在函数中返回 Webpack 配置对象,或配置数组,或 Promise 对象

js
// webpack.function.js
const { resolve } = require('path')

module.exports = function (env, argv) {
  console.log('==========================')
  console.log(env, argv)
  console.log('==========================')
  return {
    mode: env.production ? 'production' : 'development',
    devtool: env.production ? 'source-map' : 'eval-source-map',
    entry: './src/index.js',
    output: {
      filename: 'dist-function.js',
      path: resolve(__dirname, 'dist')
    }
  }
}

运行时,Webpack 会传入两个环境参数对象:

  • env:通过 --env 传递的命令行参数,适用于自定义参数,例如:
命令:env 参数值:
npx webpack --env prod{ prod: true }
npx webpack --env prod --env min{ prod: true, min: true }
npx webpack --env platform=app --env production{ platform: "app", production: true }
npx webpack --env foo=bar=app{ foo: "bar=app"}
npx webpack --env app.platform="staging" --env app.name="test"{ app: { platform: "staging", name: "test" }
  • argv:命令行 Flags 参数,支持 entry/output-path/mode/merge 等。

配置函数”这种方式的意义在于,允许用户根据命令行参数动态创建配置对象,可用于实现简单的多环境治理策略,比如:

js
// npx webpack --env app.type=miniapp --mode=production
module.exports = function (env, argv) {
  return {
    mode: argv.mode ? "production" : "development",
    entry: './src/index.js',
    devtool: argv.mode ? "source-map" : "eval",
    output: {
      filename: "[name].js",
      path: path.join(__dirname, `./dist/${env.app.type}`)
    }
  }
}

这种方式并不常用,一是因为需要在配置函数内做许多逻辑判断,复杂场景下可能可读性会很低,维护成本高;二是强依赖于命令行参数,可能最终需要写出一串很长的运行命令,应用体验较差。

目前社区比较流行通过不同配置文件区分不同环境的运行配置,配合 --config 参数实现环境治理。

环境治理策略

在现代前端工程化实践中,通常需要将同一个应用项目部署在不同环境(如生产环境、开发环境、测试环境)中,以满足项目参与各方的不同需求。这就要求我们能根据部署环境需求,对同一份代码执行各有侧重的打包策略,例如:

  • 开发环境需要使用 webpack-dev-server 实现 Hot Module Replacement;
  • 测试环境需要带上完整的 Soucemap 内容,以帮助更好地定位问题;
  • 生产环境需要尽可能打包出更快、更小、更好的应用代码,确保用户体验。

在配置结构详解中,创建了 webpack.array.jswebpack.function.js两个配置文件,用于分辨案例代码

bash
├── webpack.array.js
├── webpack.function.js

可以配合 --config 来具体执行某一个配置文件

bash
webpack --config webpack.array.js
webpack --config webpack.function.js

在实际的开发中,我们有开发、测试、生产环境,我们可以创建各自的配置文件

bash
.
└── config
  ├── webpack.common.js
  ├── webpack.dev.js
  ├── webpack.production.js
  └── webpack.test.js

common 中存放公共的配置,其他配置中可以使用 webpack-merge 来合并相同的配置

  • 支持数组属性合并
js
merge({ arr: [1] }, { arr: [2] }) === { arr: [1, 2] })
  • 支持函数属性合并
js
const res = merge(
  { func: () => console.log(1) },
  { func: () => console.log(2) }
);
res.func();
  • 支持设定对象合并策略,支持 match/append/prepend/replace/merge 规则;
  • 支持传入自定义对象合并函数;
  • ......

具体示例:

js
// ./src/index.js
const divEl = document.createElement('div')
divEl.innerHTML = 'hello webpack config'
document.body.appendChild(divEl)
js
// webpack.dev.js
const baseConfig = require("./webpack.common")
const { merge } = require("webpack-merge")

module.exports = merge(baseConfig, {
  mode: "development",
  devtool: "source-map",
  devServer: {
    port: 3002,
    hot: true,
  }
})
bash
npx webpack serve --config ./config/webpack.dev.js

image-20240827161933845

production 和 test 也是差不多的,根据环境需求进行配置就行

核心配置项汇总

webpack 的配置项有上百种,大致可以分类为:

  • 流程类:
    • 输入:entry、context
    • 模块处理:module、resolve、externals
    • 后处理:optimization、target、mode
    • 输出:output
  • 工具类:
    • 开发效率类:watch、devtool、devServer
    • 性能优化类:cache、performance
    • 日志类:status、infrastructureLogging
  • 其他
    • amd
    • bail

需要着重了解的有:

  • entry:声明项目入口文件,Webpack 会从这个文件开始递归找出所有文件依赖;
  • output:声明构建结果的存放位置;
  • target:用于配置编译产物的目标运行环境,支持 webnodeelectron 等值,不同值最终产物会有所差异;
  • mode:编译模式短语,支持 developmentproduction 等值,Webpack 会根据该属性推断默认配置;
  • optimization:用于控制如何优化产物包体积,内置 Dead Code Elimination、Scope Hoisting、代码混淆、代码压缩等功能;
  • module:用于声明模块加载规则,例如针对什么类型的资源需要使用哪些 Loader 进行处理;
  • plugin:Webpack 插件列表。

entry 配置详解

Webpack 的基本运行逻辑是从 「入口文件」 开始,递归加载、构建所有项目资源,所以几乎所有项目都必须使用 entry 配置项明确声明项目入口。entry 配置规则比较复杂,支持如下形态:

  • 字符串:指定入口文件路径;
  • 对象:对象形态功能比较完备,除了可以指定入口文件列表外,还可以指定入口依赖、Runtime 打包方式等;
  • 函数:动态生成 Entry 配置信息,函数中可返回字符串、对象或数组;
  • 数组:指明多个入口文件,数组项可以为上述介绍的文件路径字符串、对象、函数形式,Webpack 会将数组指明的入口全部打包成一个 Bundle。
js
module.exports = {
  entry: {
    // 字符串形态
    home: './home.js',
    // 数组形态
    shared: ['react', 'react-dom', 'redux', 'react-redux'],
    // 对象形态
    personal: {
      import: './personal.js',
      filename: 'pages/personal.js',
      dependOn: 'shared',
      chunkLoading: 'jsonp',
      asyncChunks: true
    },
    // 函数形态
    admin: function() {
      return './admin.js';
    }
  },
};

这其中,「对象」 形态的配置逻辑最为复杂,支持如下配置属性:

  • import:声明入口文件,支持路径字符串或路径数组(多入口);
  • dependOn:声明该入口的前置依赖 Bundle;
  • runtime:设置该入口的 Runtime Chunk,若该属性不为空,Webpack 会将该入口的运行时代码抽离成单独的 Bundle;
  • filename:效果与 output.filename 类同,用于声明该模块构建产物路径;
  • library:声明该入口的 output.library 配置,一般在构建 NPM Library 时使用;
  • publicPath:效果与 output.publicPath 相同,用于声明该入口文件的发布 URL;
  • chunkLoading:效果与 output.chunkLoading 相同,用于声明异步模块加载的技术方案,支持 false/jsonp/require/import 等值;
  • asyncChunks:效果与 output.asyncChunks 相同,用于声明是否支持异步模块加载,默认值为 true

dependOnruntime 最为晦涩难懂

dependOn

dependOn 属性用于声明前置 Bundle 依赖,从效果上看能够减少重复代码,优化构建产物质量

js
module.exports = {
  entry: {
    main: "./src/index.js",
    // foo: "./src/foo.js",
    foo: { import: "./src/foo.js", dependOn: "main" },
  },
};

设置前:两边内容并无差异

image-20240827164226430

设置后: foo 产物,代码结构非常清爽

image-20240827164340721

适用场景:例如我们构建了一个主框架 Bundle,其中包含了项目基本框架(如 React),之后还需要为每个页面单独构建 Bundle,这些页面代码也都依赖于主框架代码,此时可用 dependOn 属性优化产物内容,减少代码重复。

runtime

为支持产物代码在各种环境中正常运行,Webpack 会在产物文件中注入一系列运行时代码,用以支撑起整个应用框架。运行时代码的多寡取决于我们用到多少特性,例如:

  • 需要导入导出文件时,将注入 __webpack_require__.r 等;
  • 使用异步加载时,将注入 __webpack_require__.l 等;

不要小看运行时代码量,极端情况下甚至有可能超过业务代码总量!为此,必要时我们可以尝试使用 runtime 配置将运行时抽离为独立 Bundle,例如:

js
module.exports = [{
  mode: 'development',
  output: {
    filename: 'entry-runtime.[name].js',
    path: resolve(__dirname, 'dist'),
  },
  entry: {
    main: { import: "./src/index.js", runtime: "common-runtime" },
    foo: { import: "./src/foo.js", runtime: "common-runtime" },
  }
}]

mainfooentry.runtime 均设置为 common-runtime,将会生成三个文件,common-runtime.jsmainfoo 的公共运行时代码

image-20240827165216780

entry.runtime 是一种常用的应用性能优化手段

output 配置

Webpack 的 output 配置项用于声明:如何输出构建结果,比如产物放在什么地方、文件名是什么、文件编码等。output 支持许多子配置项,包括:

  • output.path:声明产物放在什么文件目录下;
  • output.filename:声明产物文件名规则,支持 [name]/[hash] 等占位符;
  • output.publicPath:文件发布路径,在 Web 应用中使用率较高;
  • output.clean:是否自动清除 path 目录下的内容,调试时特别好用;
  • output.library:NPM Library 形态下的一些产物特性,例如:Library 名称、模块化(UMD/CMD 等)规范;
  • output.chunkLoading:声明加载异步模块的技术方案,支持 false/jsonp/require 等方式。

target 配置

虽然多数时候 Webpack 都被用于打包 Web 应用,但实际上 Webpack 还支持构建 Node、Electron、NW.js、WebWorker 等应用形态,这一特性主要通过 target 配置控制,支持如下数值:

  • node[[X].Y]:编译为 Node 应用,此时将使用 Node 的 require 方法加载其它 Chunk,支持指定 Node 版本,如:node12.13
  • async-node[[X].Y]:编译为 Node 应用,与 node 相比主要差异在于:async-node 方式将以异步(Promise)方式加载异步模块(node 时直接使用 require)。支持指定 Node 版本,如:async-node12.13
  • nwjs[[X].Y]:编译为 NW.js 应用;
  • node-webkit[[X].Y]:同 nwjs
  • electron[[X].Y]-main:构建为 Electron 主进程
  • electron[[X].Y]-renderer:构建为 Electron 渲染进程
  • electron[[X].Y]-preload:构建为 Electron Preload 脚本
  • web:构建为 Web 应用;
  • esX:构建为特定版本 ECMAScript 兼容的代码,支持 es5es2020 等;
  • browserslist:根据浏览器平台与版本,推断需要兼容的 ES 特性,数据来源于 Browserslist 项目,用法如:browserslist: 'last 2 major versions'

比如同时构建 web 和 node 版本

js
export default 'foo'

import('./foo').then(console.log)
js
const { resolve } = require("path");
const { merge } = require("webpack-merge");

const baseConfig = {
  mode: "development",
  target: "web",
  devtool: false,
  entry: {
    main: { import: "./src/index.js" },
  },
  output: {
    clean: true,
    path: resolve(__dirname, "dist"),
  },
};

module.exports = [
  merge(baseConfig, { target: "web", output: { filename: "target-web.js" } }),
  merge(baseConfig, { target: "node", output: { filename: "target-node.js" } }),
]

image-20240827170814137

可以看到左边 web 版本中需要注入使用 JSONP 异步加载 JS 文件的运行时代码;而右边 node 版本则可以直接使用 Node 环境下的 require 实现异步加载,因此不需要注入相关运行时

mode 构建模式

mode 有以下值:

  • production:默认值,生产模式,使用该值时 Webpack 会自动帮我们开启一系列优化措施:Three-Shaking、Terser 压缩代码、SplitChunk 提起公共代码,通常用于生产环境构建;
  • development:开发模式,使用该值时 Webpack 会保留更语义化的 Module 与 Chunk 名称,更有助于调试,通常用于开发环境构建;
  • none:关闭所有内置优化规则。

mode 规则比较简单,一般在开发模式使用 mode = 'development',生产模式使用 mode = 'production' 即可。

Released under the MIT License.