Loading...

Webpack 核心概念

2025-06-14 · 18min

前言

要深入理解 Webpack 就需要先理解其核心概念跟原理,主要记些核心概念的个人理解和原理,概念描述看 Webpack 文档 就好了。

前置概念

文档中虽有列举了核心概念的目录,但当中的描述存在部分没有描述的词汇,似乎应该是默认看文档的人就肯定已经提前懂的这个词汇的概念了,但翻译成中文我也还是难以理解这种命名...

module

这个不难翻译,就是模块的意思,但其实真正的意思就是指项目的源文件以及依赖!包括源代码文件、图片字体等资源,只要是任何能够被 import / require / loader 处理的东西,都叫做 module!!!

按理来说这个是最好理解的,但是跟后面的 chunk 在一些教程里也被翻译成了模块!真的很让人误解。

chunk

翻译过来也是块的意思,但指代码块,本质含义是指由 Webpack 内部处理依赖关系得到由 module 组成的中间产物。

简单来说,就是

bundle

直接翻译过来就是捆的意思,但其实应该想表达打包的意思。

本质含义其实是指 chunk 经过处理后的磁盘文件,既打包后的最终产物。

内容组成来说:

  • JS bundle:包含编译后的代码+运行时逻辑。
  • CSS bundle: mini-css-extract-plugin 会从 JS chunk 中抽离出 CSS 文件。
  • Asset bundle:图片/字体等资源,由 file-loader 等处理。

总结

简单来说:

  • module 就是项目中你写的代码、第三方库、图片字体等资源。
  • chunk 是 module 经过 loader 处理分组后,得到的中间产物。
  • bundle 是 chunk 经过优化压缩等一系列最终处理后输出的最终打包产物。

核心概念&原理

入口(entry)

构建依赖图的起点,Webpack 会以此作为根节点递归解析所有依赖。

核心机制

  • 单入口:生成单一的 chunk,所有模块合并成一个 bundle。
  • 多入口:根据对象语法(例如 {app: "./app.js", admin: "./admin.js"})生成独立的 chunk
  • 依赖解析:读取到的代码文本通过词法分析、语法分析,输出 Tokens,组成 AST 分析 import/require 语句,来逐个构建模块依赖图。依赖解析的过程其实就跟 Markdown 渲染的解析过程一样,只不过输入不是 Markdown 文本,而是我们的代码文本,要输出的格式也不一样。

输出(output)

控制 bundle 生成位置、命名以及资源加载路径。

核心配置

  • filename:
    • [name]:基于入口 chunk 名称动态命名,如 app.js。
    • [hash]:文件内容哈希值,用于确保缓存更新。
  • path:bundle 输出的绝对路径,如 dist/。
  • publicPath:资源引用的前缀,如 assets/,也可以换成 CDN 地址,影响 HTML 中资源路径。
  • chunkFilename:非入口 chunk(如动态导入模块)的命名规则

Loader

官方文档概念描述是将 JS 和 JSON 以外的 CSS/图片等资源转换为 Webpack 可以处理的 JS 模块。

但这个说法不严谨,比如 babel-loader 是用于将 ES6+ 语法的 JS 转换为 ES5 语法的 Loader。

所以准确来说,Loader 是一个文件处理器,本质上是一个**接受一个源文件内容,返回转换后内容的 ** 函数。

工作流程

  • 文件匹配:通过 Webpack 配置文件中的 test 规则匹配文件。
  • Loader 排序:按照 use 数组倒序排序。
  • 内容读取:读取文件内容,一般为字符串或者 Buffer。
  • Loader 链执行:将内容一次传递给排序好后的 Loader 处理。如 use:['style-loader', 'css-loader'] 会先执行 css-loader。
  • JS模块生成:最终输出成 Webpack 可识别的 JS 模块。如 export default 'image.png'。

配置

Webpack 在配置文件中,通过 module[rules] 设置不同类型的文件,用相应的 Loader 进行处理,例如:

  • /\.(js|mijs|jsx|ts|tsx)$/ 使用 babel-loader 处理。
  • /\.css$/ 使用 css-loader 处理。
  • /\.(scss|sass)$/ 使用 sass-loader 处理。
  • /\.less$/ 使用 less-loader 处理。
  • [/\.bmg$/, /\.gif$/, /\.jpe?g$/, /\.png$/] 使用 url-loader 处理。

一般来说,rules里的每个对象包含 test、include、exclude、use 等字段,use 字段则是接受一个对象数组,里面包含了需要使用的 Loader 和各自的 options。

完整 Webpack 配置举例:

// webpack.config.js
const path = require("path");
const buildPath = path.resolve(__dirname, "dist");
const srcPath = path.resolve(__dirname, "src")

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: buildPath,
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: srcPath,
        exclude: [/node_modules/],
        use: [
          {
            loader: require.resolve("babel-loader"),
            options: {
              presets: [require.resolve("@babel/preset-react")]
            }
          }
        ]
      }
    ]
  }
}

// src/index.js
import React from "react";
import { createRoot } from "react-dom/client";

const App = () => {
  return <h1>Hello this is React</h1>;
};

createRoot(document.getElementById("root")).render(<App />);

运行 webpack 命令后会生成 dist/bundle.js,将它放到一个 HTML 中

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="bundle.js"></script>
</body>
</html>

运行效果:

通过 Webpack 运行 React

这样就打包好了一个非常简单的 React 项目。

当然完整的 React 项目肯定不是这么简单就打包完成的,还需要将其他非 JS 模块以及优化需要处理,有的可以找到第三方 loader 处理,如果没有自己满意还可以自己开发自定义 loader。

插件(Plugin)

插件通过事件钩子干预构建流程,实现优化、资源生成等高级功能。

与 Loader 不同,Loader 是针对单个文件进行转换的,Plugin 则是涉及到了整个 Webpack 的编译流程,对流程进行干预。

在正常开发项目中,打包生成的文件名带有哈希值的,我们不可能每次都手动自己去修改的,但可以通过 Plugin,自动将生成的 bundle 引入到 HTML 文件。

以 create-react-app 的 Webpack Plugin 为例:

{
  //...
  plugins: [
      new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appHtml,
          },
          isEnvProduction
            ? {
                minify: {
                  removeComments: true,
                  collapseWhitespace: true,
                  removeRedundantAttributes: true,
                  useShortDoctype: true,
                  removeEmptyAttributes: true,
                  removeStyleLinkTypeAttributes: true,
                  keepClosingSlash: true,
                  minifyJS: true,
                  minifyCSS: true,
                  minifyURLs: true,
                },
              }
            : undefined
        )
      ),
  ]
}

通过 html-webpack-plugin 自动将生成 bundle 引入到 HTML 文件中,这个插件可以说是 Webpack 构建到前端项目中必备插件。

除此之外常用的插件还有:

  • webpack.DefinePlugin :常量注入插件,用于注入环境变量等。
  • mini-css-extract-plugin:把 CSS 代码拆分到独立的文件中,需要搭配配套的 Loader使用,一般在打包时使用,实现 CSS 按需加载。

Webpack 为插件提供了有丰富功能的上下文对象(this),通过 Webpack 自研的 Tapable 事件流提供了9种钩子来让插件进行事件注册,但这个要讲也是非常多....

开发服务器(DevServer)

之前讲过构建工具主要功能之一就是提供开发服务器了,Webpack 通过 webpack-dev-server 来实现此功能。

配置非常多,也非常复杂, create-react-app 甚至专门新起了一个文件来导出 devServer 的配置,要把所有配置都记完不显示,主要记下比较核心常用的就好了:

static

该配置项允许配置从目录提供静态文件的选项,主要两个配置:

  • directory:目录地址,默认 public
  • watch:是否监听 directory 目录下的文件更改,默认开启。

compress

启用 GZIP Compression。

hot

热更新开启,相信也没人会去关。

但是 Webpack 的热更新效果会触发页面刷新,热更新的效果应该不触发页面刷新的...

在 create-react-app 中也是通过 react-refresh 和

@pmmmwh/react-refresh-webpack-plugin 来实现 React 的热更新的。

优化(Optimization)

就是优化配置了,配置多的也是离谱,完全记住全部根本不可能吧!我觉得正常来说只需要记住几个比较关键的配置项即可,真有不知道再去查文档好了,开发不是靠死记硬背的。

splitchunks

代码分割/拆包,目的是为了将公共模块或者符合指定条件的模块分到单独的 chunk 中,避免重复打包,提高打包速度,打包产物后可以利用浏览器缓存没改变的第三方库,优化首屏和后续页面的加载速度,是大型项目性能优化的核心配置。

配置项:

  • chunks:选择哪些 chunk 进行优化。有效值是
    • all : 符合条件的全都拆分,也是最常用的,一般来说只会用这个。
    • async:只拆分 import() 这种方式加载的模块成为一个单独 chunk,拆分效果就跟 React 和 Vue 的懒加载拆分时一样。
    • initial:只拆分同步处理的模块。
    • 自定义 Function:如果不使用 all ,可以传递一个函数对每一个 chunk 进行判断,需要返回一个布尔值。
  • miniSize:生成 chunk 的最小体积。小于该体积的模块不会被拆分出去,默认2000(20KB)。
  • minchunks:模块至少被多少个 chunk 引用时才被提取为公共 chunk,默认1。
  • maxAsyncRequest:按需加载时(异步 chunk)最大并行请求数,默认30,用于限制拆分过多导致大量请求。
  • maxInitialRequests:入口点最大并行请求数,也是默认是30,同样用于限制入口加载时大量请求。
  • automaticNameDelimiter:生成 chunk 名称时使用的分隔符,默认 ~。
  • name: 默认为 true,让 Webpack 基于 cacheGroups 和 chunks 自动生成名称,可以字节设置定义函数返回名称,一般来说也没人会设置 false。
  • cacheGroup:splitchunks 核心,定义分组规则来提取模块:
    optimization: {
      splitchunks: {
        cacheGroups: {
          // 提取 node_modules 中的第三方库
          vendors: {
            test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的模块
            name: 'vendors', // 提取出来的 chunk 名称 (可选,默认根据 chunks/module 自动生成)
            chunks: 'all', // 推荐 'all'
            priority: 10, // 优先级,防止被其他规则覆盖。数字越大优先级越高
            // reuseExistingchunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则重用该模块
          },
          // 提取公共模块 (被多个入口/异步 chunk 引用的业务代码)
          common: {
            minchunks: 2, // 至少被 2 个入口/chunk 引用
            name: 'common',
            chunks: 'all',
            priority: 5, // 优先级低于 vendors
            minSize: 0, // 有时公共模块可能较小,可以适当降低 minSize 或设为 0
          },
          // 按特定库拆分 (例如单独拆分 React, ReactDOM, Lodash)
          react: {
            test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
            name: 'react-vendor',
            chunks: 'all',
            priority: 20, // 比 vendors 优先级高,确保单独拆分出来
          },
        }
      }
    }
    

    常用的拆分规则:
    • 将 node_modules 提取到单独的 Vendors chunk 中,可以长效缓存。
    • 提取公共业务代码到 Common chunk 。
    • 将超大且不常变动的库单独拆分,例如 react、Echarts 等。
    • 结合 import() 异步加载。

Minimize & Minimizer

minimize 开启代码压缩,默认开启。

minimizer: 代码压缩配置,通常是一个数组,配置具体的压缩插件实例,需要覆盖默认值,并自定义选项:

// 压缩 JS (Webpack 5+ 内置,但需显式引入配置)
const TerserPlugin = require('terser-webpack-plugin'); 
// 压缩 CSS
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 

optimization: {
  minimizer: [
    // 压缩 JS (覆盖默认的 terser 配置)
    new TerserPlugin({
      parallel: true, // 使用多进程并行运行以提高构建速度(强烈推荐!)
      terserOptions: {
        compress: {
          drop_console: true, // 生产环境移除 console.* (谨慎使用!调试时要去掉)
          // ... 其他 terser 压缩选项 
        },
        format: {
          comments: false, // 移除注释
        },
        // ... 其他 terser 选项
      },
      extractComments: false, // 不将注释提取到单独的文件
    }),
    // 压缩 CSS (使用 cssnano 或其他引擎)
    new CssMinimizerPlugin({
      parallel: true, // 并行压缩
      minimizerOptions: {
        preset: [
          'default',
          {
            discardComments: { removeAll: true }, // 移除所有注释
            // ... 其他 cssnano 选项 
          },
        ],
      },
    }),
  ],
}

需要注意:

  • minimizer 会覆盖 Webpack 默认压缩配置,所以必须显式添加 terser-webpack-plugin 和 CssMinimizerPlugin 才能压缩 JS 和 CSS。
  • parallel: true 极大提升压缩速度。
  • drop_console 等选项需谨慎,确保线上环境不需要日志。

runtimechunk

提取运行时代码,将 Webpack 的运行时代码(管理模块加载以及交互代码) 提取到一个单独且体积小的 chunk 中,提取后仅当运行时代码变化时,不会导致业务代码或第三方库的 Vendors / chunk 的哈希发生变化,从而利用浏览器可以长效缓存 Vendors / chunk,还能避免运行时代码重复注入多个入口 chunk。

optimization: {
  runtimechunk: {
    name: (entrypoint) => `runtime-${entrypoint.name}`, // 推荐:为每个入口生成单独的 runtime (更细粒度缓存)
    // 或简写为:
    // runtimechunk: 'single' // 所有入口共享一个 runtime chunk (简单应用)
  }
}

moduleIds & chunkIds

控制 Webpack 如何给 module和 chunk 生成唯一的 ID,不同的 ID 生成策略会影响模块增删时,未变动模块的 ID 是否稳定,从而影响长效缓存。

optimization: {
  // 生产环境推荐配置 (Webpack 4+)
  moduleIds: 'deterministic', // 或 'hashed' (Webpack 4)。deterministic 是 Webpack 5 的默认值,提供稳定哈希。
  chunkIds: 'deterministic',  // 同上。

  // 开发环境推荐
  // moduleIds: 'named', // 使用易读的名称 (便于调试)
  // chunkIds: 'named',  // 同上
}

usedExports / sideEffects (Tree Shaking)

Tree Shaking 的配置,生效有前提条件:

  • 只对 ES6 语法有效,Common JS 和 AMD 模块无法有效进行 Tree Shaking。
  • 需要在项目中的 package.json 中设置 sideEffects: false 或者指定有副作用的文件数组,例如 [".css", "*.global.js"]。
  • 确保 Babel 等编译器没有将 ES6 语法的模块转换成 Common JS,需要检查 Babel 配置。
  • 需要使用同样支持 Tree Shaking 的第三方库,提供 ES Module 版本并且在其他 package.json 正确配置好 sideEffects。

配置项:

  • usedExports :设置为 true 时(Webpack 4+ 生产环境时默认开启),会比较哪些模块被导出使用了。
  • sideEffects:设置为 true 时(Webpack 4+ 生产环境时默认开启),会根据 package.json 中的 sideEffects 属性移除整个为使用的模块或文件。

concatenateModules

开启后,Webpack 会分析模块间的依赖关系,尽可能将可以安全合并的模块内联到一个函数里,能够通过消除大量模块包装函数来减小代码提及,减少函数作用域创建和查找开销从而提升运行速度。

需要先设置 optimization.providedExports 和 optimization.usedExports 为启用状态。

Webpack 中在生产环境中默认开启,其他环境中默认禁用,通常由 Webpack 自动完成即可,效果取决于代码结构与模块语法,使用 ES Module 效果最好。

emitOnErrors

构建出错处理。当编译出错时,是否继续输出资源。

通常来说,生产环境下设置为 false,出错时则不输出资源;开发模式通常为 true,可方便调试。

总结

Webpack 核心概念大概就以上这些,但其实像 Loader、Plugin、优化,都还有很多可以深入的地方,但真的要一次性搞懂全部太困难了,还是先从整体的核心概念理解入手,在逐步深入好些。