Skip to content

Webpack ​

从「模块打包工具」这个本质出发,Webpack 的一切概念——Entry、Output、Loader、Plugin——都围绕一个核心目标:把浏览器无法直接理解的各类资源,转换、组织、打包成可运行的静态文件。本篇按「核心概念 → 模块化原理 → 编译与产物 → 工程化进阶 → 对比与选型」五个维度,串联 Webpack 从入门到原理的完整知识体系。

导航目录 ​

一、核心概念篇 ​

二、模块化原理篇 ​

三、编译与产物篇 ​

四、工程化进阶篇 ​

五、对比与选型篇 ​

Webpack 介绍 ​

  • Webpack 是一个 JavaScript 应用程序的静态模块打包工具,它可以将多个模块打包成一个或多个 bundle 文件

核心概念

Webpack 把项目里的一切资源(JS、CSS、图片、字体等)都视为模块。它从入口出发,递归分析模块间的依赖关系,构建出一张依赖图(dependency graph),再按规则将这些模块转换、合并成浏览器可运行的静态资源。

Webpack 的运行离不开五个核心概念,它们贯穿整个构建过程:

概念作用
Entry依赖图的起点,告诉 Webpack 从哪个模块开始构建
Output打包结果输出到哪里、如何命名
Loader让 Webpack 能处理非 JS 资源(转换为有效模块)
Plugin参与构建生命周期,执行更广泛的任务(优化、注入)
Modedevelopment / production,启用对应内置优化

入口 (Entry) ​

  • 入口起点(entry point)指示 Webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始
  • 进入入口起点后,Webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的

单入口 vs 多入口

  • 单入口:entry: './src/index.js',适合单页应用(SPA)
  • 多入口:entry: { app: './app.js', admin: './admin.js' },适合多页应用(MPA),每个入口生成独立的 bundle

输出 (Output) ​

  • output 属性告诉 Webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
javascript
module.exports = {
  output: {
    path: path.resolve(__dirname, "dist"), // 输出目录(绝对路径)
    filename: "[name].[contenthash].js", // 使用 contenthash 做长效缓存
    clean: true, // 构建前清空输出目录
  },
};

占位符与缓存

[contenthash] 会根据文件内容生成哈希,内容不变则哈希不变,配合 HTTP 强缓存可最大化利用浏览器缓存;相比之下 [hash](整个构建的哈希)粒度太粗,[chunkhash](chunk 级别)则介于两者之间。


Loader ​

  • Webpack 只能理解 JavaScript 和 JSON 文件
  • Loader 让 Webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中
  • 多个 loader 从右往左执行(或从下往上)

核心概念

Loader 本质是一个导出为函数的 JS 模块,接收源文件内容作为参数,返回转换后的内容。它是一个「转换器」,让 Webpack 具备处理任意资源的能力。

Loader 的执行顺序:配置数组中的 loader 从右到左(从下到上)依次执行,前一个 loader 的输出作为后一个的输入,形成一条转换流水线。

text
原始 .scss 源码
  ──► sass-loader(SCSS 编译为 CSS)
       ──► css-loader(解析 CSS 依赖)
            ──► style-loader(注入到 DOM)
                 ──► 有效 JS 模块

以处理样式为例,配置 use: ['style-loader', 'css-loader', 'sass-loader'],实际执行顺序为 sass-loader → css-loader → style-loader。

常用 Loader:

Loader作用
babel-loader将 ES6+ / JSX 转译为兼容性代码
css-loader / style-loader解析 CSS 依赖 / 将 CSS 注入 DOM
sass-loader / less-loader编译 SCSS / Less 为 CSS
postcss-loader自动加前缀、压缩等 CSS 后处理
ts-loader编译 TypeScript
Asset Modules(webpack5 内置)替代 file-loader / url-loader 处理资源

Loader 编写要点

  • 单一职责:每个 loader 只做一件事,便于组合复用
  • 无状态:不应在多次编译间保留状态
  • 使用 this.async() 处理异步操作

插件 (Plugin) ​

  • loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务
  • 插件功能包括:打包优化,资源管理,注入环境变量等

核心概念

Plugin 是一个带有 apply 方法的类。Webpack 启动时调用 apply(compiler),插件通过 compiler.hooks 注册到构建生命周期的各个**钩子(Hook)**上,从而在合适的时机介入构建流程。它基于 Webpack 的 Tapable 事件流机制实现。

Loader 与 Plugin 的区别:

维度LoaderPlugin
本质转换函数带 apply 方法的类
作用转换单个模块的内容介入整个构建生命周期
运行时机模块加载/解析阶段从初始化到输出的任意钩子
粒度文件级构建级

常用 Plugin:

Plugin作用
HtmlWebpackPlugin自动生成 HTML 并注入打包后的资源
MiniCssExtractPlugin将 CSS 抽离为独立文件
DefinePlugin注入全局常量(如环境变量)
CleanWebpackPlugin构建前清理输出目录(webpack5 可用 output.clean)
TerserPluginJS 压缩混淆(生产模式默认启用)
BundleAnalyzerPlugin可视化分析产物体积

一个最简 Plugin 的形态

javascript
class MyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap("MyPlugin", (stats) => {
      console.log("构建完成!");
    });
  }
}

CommonJS 模块打包原理 ​

核心概念

Webpack 打包后的产物,本质上是一个自执行函数(IIFE),内部用一个 modules 对象存放所有模块(键为模块路径,值为包裹模块代码的函数),并实现了一套简易的 require 函数:通过 cache 缓存已加载模块,首次加载时执行模块函数并把结果挂到 module.exports 上。这就是「模块化」在浏览器里得以运行的底层机制。

下面通过手写实现,逐步还原 Webpack 对不同模块系统的打包处理。

CommonJS 模块 简易实现 ​

功能说明:展示 CommonJS 模块的基本加载机制,包括模块缓存和导出处理。

js
(() => {
  let modules = {
    "./src/title.js": (module) => {
      // CommonJS 模块
      module.exports = "title";
    },
  };
  let cache = {};
  function require(moduleId) {
    let cacheModule = cache[moduleId];
    if (cacheModule) {
      return cacheModule.exports;
    }
    let module = (cache[moduleId] = {
      exports: {},
    });
    modules[moduleId](module, module.exports, require);
    return module.exports;
  }

  (() => {
    let title = require("./src/title.js");
    console.log(title);
  })();
})();

CommonJS 加载 CommonJS 模块 ​

功能说明:展示 CommonJS 模块之间的相互加载和导出机制。

js
(() => {
  let modules = {
    "./src/title.js": (module, exports) => {
      // CommonJS 模块
      exports.name = "title_hl";
      exports.age = "title_18";
    },
  };
  let cache = {};
  function require(moduleId) {
    let cacheModule = cache[moduleId];
    if (cacheModule) {
      return cacheModule.exports;
    }
    let module = (cache[moduleId] = {
      exports: {},
    });
    modules[moduleId](module, module.exports, require);
    return module.exports;
  }

  (() => {
    let title = require("./src/title.js");
    console.log(title);
  })();
})();

CommonJS 加载 ES6 模块 ​

功能说明:展示 CommonJS 如何加载 ES6 模块,包括 ES6 模块的导出转换机制。

js
(() => {
  let modules = {
    "./src/title.js": (module, exports) => {
      // ES6 模块导出
      require.r(exports);
      require.d(exports, {
        default: () => DEFAULT_EXPORTS, // 值是一个 getter
        age: () => age,
      });
      // 默认导出
      const DEFAULT_EXPORTS = "title";
      // 命名导出
      const age = 18;
    },
  };

  let cache = {};
  function require(moduleId) {
    let cacheModule = cache[moduleId];
    if (cacheModule) {
      return cacheModule.exports;
    }
    let module = (cache[moduleId] = {
      exports: {},
    });
    modules[moduleId](module, module.exports);
    return module.exports;
  }

  // 判断对象是否具有某个属性
  require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

  // 给 exports 添加属性,标记为 ES 模块
  require.r = (exports) => {
    Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    Object.defineProperty(exports, "__esModule", { value: true });
  };

  // 绑定属性
  require.d = (exports, definition) => {
    for (let key in definition) {
      // 如果 definition 有,exports 没有,则绑定
      if (require.o(definition, key) && !require.o(exports, key)) {
        // getter
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };

  (() => {
    let title = require("./src/title.js");
    console.log(title);
  })();
})();

ES6 模块加载 ES6 模块 ​

功能说明:展示 ES6 模块之间的相互加载机制,包括 ES6 模块的动态绑定特性。

js
(() => {
  let modules = {
    "./src/title.js": (module, exports) => {
      // ES6 模块的导出
      require.r(exports);
      require.d(exports, {
        default: () => DEFAULT_EXPORTS, // 值是一个 getter
        age: () => age,
      });
      // 默认导出
      const DEFAULT_EXPORTS = "title";
      // 命名导出
      let age = 18;
      // ES Module 获取的是最终的值
      // 导出的值的引用
      setTimeout(() => {
        age = 20;
      }, 1000);
    },
  };

  let cache = {};
  function require(moduleId) {
    let cacheModule = cache[moduleId];
    if (cacheModule) {
      return cacheModule.exports;
    }
    let module = (cache[moduleId] = {
      exports: {},
    });
    modules[moduleId](module, module.exports);
    return module.exports;
  }

  // 判断对象是否具有某个属性
  require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

  // 给 exports 添加属性,标记为 ES 模块
  require.r = (exports) => {
    Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    Object.defineProperty(exports, "__esModule", { value: true });
  };

  // 绑定属性
  require.d = (exports, definition) => {
    for (let key in definition) {
      // 如果 definition 有,exports 没有,则绑定
      if (require.o(definition, key) && !require.o(exports, key)) {
        // getter
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };
  let exports = {};
  (() => {
    // 只要打包前的模块是一个 ES module,那么就会调用 require.r 方法进行处理
    require.r(exports);
    let title = require("./src/title.js");
    console.log(title);
  })();
})();

ES6 模块加载 CommonJS 模块 ​

功能说明:展示 ES6 模块如何加载 CommonJS 模块,包括模块适配机制。

js
(() => {
  let modules = {
    "./src/title.js": (module, exports) => {
      // CommonJS 模块
      module.exports = {
        name: "title_name",
        age: "title_age",
      };
    },
  };

  let cache = {};
  function require(moduleId) {
    let cacheModule = cache[moduleId];
    if (cacheModule) {
      return cacheModule.exports;
    }
    let module = (cache[moduleId] = {
      exports: {},
    });
    modules[moduleId](module, module.exports);
    return module.exports;
  }

  // 判断对象是否具有某个属性
  require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

  // 给 exports 添加属性,标记为 ES 模块
  require.r = (exports) => {
    Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    Object.defineProperty(exports, "__esModule", { value: true });
  };

  // 绑定属性
  require.d = (exports, definition) => {
    for (let key in definition) {
      // 如果 definition 有,exports 没有,则绑定
      if (require.o(definition, key) && !require.o(exports, key)) {
        // getter
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };

  // 处理默认导出
  require.n = (module) => {
    var getter =
      module && module.__esModule ? () => module["default"] : () => module;
    return getter;
  };

  let exports = {};
  (() => {
    // 只要打包前的模块是一个 ES module,那么就会调用 require.r 方法进行处理
    require.r(exports);
    let title = require("./src/title.js");
    var title_default = require.n(title);
    console.log(title_default());
    console.log(title.age);
  })();
})();

模块异步加载原理 ​

功能说明:展示 Webpack 如何实现模块的异步加载,包括 JSONP 加载机制和 chunk 管理。

核心概念

当代码中使用 import() 动态导入时,Webpack 会把该模块单独打包成一个 chunk 文件。运行时通过 JSONP 方式(动态创建 script 标签)按需加载这个 chunk,加载完成后触发回调,把模块合并进 modules 并 resolve 对应的 Promise。

  • 1、通过 chunkId 去查找对应的代码块文件(如 hello.main.js)
  • 2、通过动态创建 script 标签加载 chunk 文件(JSONP 方式)
  • 3、加载完成后,会调用 webpackJsonpCallback 函数,将 chunk 文件中的模块添加到 modules 中,并通过 installedChunks 记录 chunk 文件加载状态
text
动态 import ──► 生成 chunkId ──► 查找 chunk 文件 ──► JSONP 加载 script
  ──► chunk 执行 ──► 执行 webpackJsonpCallback ──► 触发回调 resolve ──► Promise 返回模块
js
let modules = {};
let cache = {};

function require(moduleId) {
  let cacheModule = cache[moduleId];
  if (cacheModule) {
    return cacheModule.exports;
  }
  let module = (cache[moduleId] = {
    exports: {},
  });
  modules[moduleId](module, module.exports);
  return module.exports;
}

// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

// 给 exports 添加属性,标记为 ES 模块
require.r = (exports) => {
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
  Object.defineProperty(exports, "__esModule", { value: true });
};

// 绑定属性
require.d = (exports, definition) => {
  for (let key in definition) {
    // 如果 definition 有,exports 没有,则绑定
    if (require.o(definition, key) && !require.o(exports, key)) {
      // getter
      Object.defineProperty(exports, key, {
        enumerable: true,
        get: definition[key],
      });
    }
  }
};

require.f = {};

// 返回此文件对应的访问路径
require.p = "";
// 返回此代码块对应的文件名
require.u = function (chunkId) {
  return chunkId + ".main.js";
};

// 存放加载的代码块的状态
// key 是代码块的名字
// 0 表示已经加载完成了
var installedChunks = {
  main: 0,
  // 'hello': [resolve, reject, promise]
};

require.e = (chunkId) => {
  let promises = [];
  require.f.j(chunkId, promises);
  return Promise.all(promises);
};

/**
 * 通过 JSONP 异步加载一个 chunkId 对应的代码块文件,其实就是 hello.main.js
 * 会返回一个 Promise
 * @param {*} chunkId 代码块 ID
 * @param {*} promises promise 数组
 */
require.f.j = function (chunkId, promises) {
  // 当前的代码块的数据
  let installedChunkData;
  // 创建一个 promise
  const promise = new Promise((resolve, reject) => {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  installedChunkData[2] = promise;
  promises.push(promise);
  const url = require.p + require.u(chunkId);
  require.l(url);
};

// 加载远程的模块
require.l = function (url) {
  let script = document.createElement("script");
  script.src = url;
  document.head.appendChild(script);
};

var chunkLoadingGlobal = (window["webpack5"] = []);
chunkLoadingGlobal.push = webpackJsonpCallback;
/**
 *
 * @param {*} chunkIds 代码块 ID 数组
 * @param {*} moreModules 额外的模块定义
 */
function webpackJsonpCallback([chunkIds, moreModules]) {
  const resolves = [];
  for (let i = 0; i < chunkIds.length; i++) {
    const chunkId = chunkIds[i];
    resolves.push(installedChunks[chunkId][0]);
    installedChunks[chunkId] = 0; // 表示此代码块已经下载完毕
  }
  // 合并模块定义到 modules 去
  for (const moduleId in moreModules) {
    modules[moduleId] = moreModules[moduleId];
  }
  // 依次取出 resolve 方法并执行 => promise resolve
  while (resolves.length) {
    resolves.shift()();
  }
}

/**
 * require.e 异步加载 hello 代码块文件 hello.main.js
 * promise 成功后会把 hello.main.js 里面的代码定义合并到 require.m 对象上,也就是 modules 上
 * 调用 require 方法加载 ./src/hello.js 模块,获取模块的导出对象,进行打印
 */
require
  .e("hello")
  .then(require.bind(require, "./src/hello.js"))
  .then((result) => {
    console.log(result);
  });

Webpack 编译流程 ​

功能说明:展示 Webpack 的完整编译流程,从初始化到输出的各个步骤。

text
初始化参数(合并配置)
  └► 创建 Compiler 对象
       └► 加载插件(调用 apply)
            └► 执行 run 开始编译
                 └► 从 entry 构建依赖图
                      └► 调用 Loader 编译模块 ◄─┐
                           │  有依赖? ── 是 ─────┘
                           │  否
                           └► 组装 Chunk
                                └► 生成输出文件
                                     └► 写入文件系统
  1. 初始化参数:从配置文件和 Shell 语句中读取参数,与 Webpack 默认参数合并,得出最终的配置对象
  2. 初始化 Compiler 对象:使用上一步得到的参数创建 Compiler 对象
  3. 加载插件:加载所有配置的插件,调用插件的 apply 函数并传递 Compiler 对象
  4. 执行编译:执行 Compiler 对象的 run 方法开始执行编译
  5. 构建依赖图:执行 Compilation 对象的 build,根据配置中的 entry 找出入口文件
  6. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译
  7. 递归处理依赖:找出该模块依赖的模块,递归编译直到所有入口依赖的文件都处理完毕
  8. 生成 Chunk:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  9. 生成输出文件:把每个 Chunk 转换成一个单独的文件加入到输出列表
  10. 写入文件系统:根据配置确定输出的路径和文件名,把文件内容写入到文件系统

两个核心对象

  • Compiler:全局唯一,代表完整的 Webpack 环境配置,从启动到关闭的整个生命周期只创建一次
  • Compilation:代表一次资源构建,watch 模式下文件每次变化都会创建新的 Compilation,因此支持增量构建

Module、Chunk、Bundle ​

  • Module:各个源码文件,Webpack 中一切皆模块
  • Chunk:多模块合并成的代码块
  • Bundle:最终的输出文件

三者关系

它们是构建过程中不同阶段的产物:Module 是输入(开发者编写的一个个源文件),Chunk 是中间过程(Webpack 根据依赖关系与分包策略把模块组织成的代码块),Bundle 是输出(Chunk 最终生成的物理文件)。多数情况下 Chunk 与 Bundle 一一对应,但如 MiniCssExtractPlugin 可能让一个 Chunk 产出 JS 和 CSS 两个 Bundle。

text
Module a.js  ┐
Module b.js  ├──► Chunk main ──┬──► Bundle main.js
Module c.css ┘                 └──► Bundle main.css

Tree Shaking ​

核心概念

Tree Shaking(摇树优化)是指在打包时移除未被使用的代码(dead code)。它依赖 ES Module 的静态结构特性:import / export 在编译阶段就能确定依赖关系,因此 Webpack 能静态分析出哪些导出从未被引用,从而在压缩阶段删除它们。

生效前提:

  • 使用 ES Module 语法(import / export),CommonJS 因为是运行时动态加载无法 Tree Shaking
  • 生产模式(mode: 'production')下自动开启,依赖 Terser 完成删除
  • 依赖包需支持 ESM,且 package.json 中合理声明 sideEffects

sideEffects 副作用陷阱

如果一个模块被 import 但只产生副作用(如引入全局 CSS、polyfill),却没有导出被使用,Tree Shaking 可能误删它。可在 package.json 中声明:

json
{
  "sideEffects": ["*.css", "./src/polyfill.js"]
}

设为 false 表示所有模块都无副作用(可放心摇树),数组则表示这些文件有副作用、不可删除。


HMR 热更新原理 ​

核心概念

HMR(Hot Module Replacement,模块热替换)能在不刷新整个页面的前提下,用新模块替换旧模块,保留应用状态。它是 webpack-dev-server 提供的开发体验核心能力。

工作流程:

text
文件修改
  └► Webpack 重新编译(生成 hash 与补丁)
       └► dev-server 通过 WebSocket 通知浏览器 hash
            └► 浏览器 HMR runtime 发起 JSONP 拉取补丁
                 └► 拿到更新的 module
                      └► 执行 module.hot.accept 替换旧模块
                           └► 界面局部更新(状态保留)
  • 1、webpack-dev-server 启动时与浏览器建立 WebSocket 长连接
  • 2、文件变化触发重新编译,生成本次编译的 hash 和描述变更的 manifest(.json) 与 补丁(.js)
  • 3、通过 WebSocket 把新 hash 推送给浏览器端的 HMR runtime
  • 4、runtime 通过 JSONP / fetch 拉取补丁文件
  • 5、通过 module.hot.accept 接受更新,替换模块并触发回调,实现局部刷新

框架的封装

React 的 react-refresh、Vue 的 vue-loader 内部都调用了 module.hot.accept,帮你处理组件级的热替换与状态保留,因此业务代码通常无需手写 HMR API。


Module Federation 模块联邦 ​

核心概念

Module Federation(模块联邦)是 Webpack 5 的重磅特性,允许多个独立构建的应用在运行时相互共享模块。一个应用(Remote)可以暴露模块,另一个应用(Host)可以在运行时动态加载这些远程模块,是实现微前端的重要方案之一。

  • Host(消费方):通过 remotes 配置消费远程模块
  • Remote(生产方):通过 exposes 配置暴露自己的模块
  • shared:声明共享依赖(如 React),避免重复加载,实现单例
javascript
// remote 应用
new ModuleFederationPlugin({
  name: "remoteApp",
  filename: "remoteEntry.js",
  exposes: { "./Button": "./src/Button" },
  shared: ["react", "react-dom"],
});

// host 应用
new ModuleFederationPlugin({
  name: "hostApp",
  remotes: { remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js" },
  shared: ["react", "react-dom"],
});

与传统方案的区别

相比 npm 包(构建时集成)或 iframe(运行时但完全隔离),Module Federation 实现了运行时的代码级共享,各应用可独立部署、独立发版,同时共享依赖,兼顾了解耦与性能。


性能优化 ​

Webpack 性能优化分为构建速度和产物体积两个维度。

一、提升构建速度 ​

手段说明
cache(webpack5 内置)持久化缓存,二次构建大幅提速
thread-loader多进程处理耗时 loader(如 babel)
include / exclude缩小 loader 处理范围(如排除 node_modules)
resolve.alias / extensions减少模块查找路径与后缀尝试
DllPlugin(较老方案)预编译第三方库,webpack5 已可用持久化缓存替代

二、优化产物体积 ​

手段说明
Tree Shaking移除未使用代码
代码分割(SplitChunks)拆包 + 按需加载,减小首屏体积
压缩TerserPlugin(JS)、CssMinimizerPlugin(CSS)
Gzip / Brotlicompression-webpack-plugin 生成压缩产物
图片优化image-minimizer-webpack-plugin 压缩图片
按需引入如 babel-plugin-import 按需加载组件库

优化前先分析

使用 webpack-bundle-analyzer 可视化产物构成,先定位体积大头再优化,避免盲目配置。构建慢时可用 speed-measure-webpack-plugin 定位耗时环节。


Vite 为什么比 Webpack 启动速度快? ​

核心差异

根本区别在于开发环境的构建策略:Webpack 是 Bundle-based(先打包再启动),Vite 是 ESM-based(不打包,按需编译)。项目越大,Webpack 启动越慢,而 Vite 的启动速度几乎与项目规模无关。

Vite ​

  • 1、Vite 利用浏览器原生 ES Module 的支持,省略了打包步骤,直接启动开发服务器,当代码被请求时,根据需要动态编译模块
  • 2、Vite 利用 esbuild 预编译依赖,esbuild 使用 Go 编写,比 JS 编写的打包器预编译速度更快

Webpack ​

  • 1、Webpack 一切皆为模块,启动后会从 entry 开始递归解析 entry 依赖的所有模块,在编译过程中会根据配置的 Loader 对模块进行编译,编译完后再找出该模块依赖的模块,然后递归编译,所以 Webpack 启动会先编译 entry 依赖的所有模块,启动时间较慢

生产环境的取舍

Vite 快主要体现在开发环境。生产构建时 Vite 仍使用 Rollup 打包(为兼容性与产物优化)。Webpack 生态成熟、配置灵活、社区插件丰富,在复杂工程与老项目中仍是稳妥选择。

CommonJS 和 ES Module ​

特性CommonJSES Module
导入语法require()import
导出语法module.exports / exportsexport default / export
加载方式同步加载异步加载
编译时机运行时加载编译时静态解析
Node.js 支持所有版本默认支持v12+ 支持,需 .mjs 扩展名或 "type": "module"

关键差异说明: ​

  1. 加载机制

    • CommonJS 在运行时同步加载模块(阻塞执行)
    • ES Module 在编译阶段构建依赖关系图,异步加载
  2. 解析方式

    javascript
    // CommonJS (动态)
    const module = condition ? "A" : "B";
    const lib = require(module); // 运行时确定
    
    // ES Module (静态)
    import lib from "./module.js"; // 编译时解析 - 路径必须是字面量!
  3. 作用域

    • CommonJS 导出值的拷贝(基础类型复制,引用类型共享地址)
    • ES Module 导出值的值的引用(所有类型均为动态引用)
  4. 循环引用处理

    • CommonJS:可能获得未初始化的中间状态
    • ES Module:通过 "live binding" 机制保证最终一致性
  5. 浏览器兼容

    • CommonJS 不能直接在浏览器运行(需打包工具转换)
    • ES Module 被所有现代浏览器原生支持 (<script type="module">)

Webpack 分包与拆包 ​

Webpack 分包,也叫代码分割,是指将代码拆分成多个 bundle,实现按需加载或并行加载的技术。

它的核心价值主要有三点:

  1. 减少首屏加载时间:只加载当前页面需要的代码,显著提升首屏速度
  2. 提高缓存利用率:将不常变的第三方库单独打包,利用浏览器缓存
  3. 优化用户体验:按需加载减少初始下载量,非阻塞加载提升交互体验

在实际项目中,我们通常会对路由级别、大型组件和第三方库进行分包,实现最优的加载性能。

有哪些方式可以实现代码分割? ​

Webpack 提供了三种主要的代码分割方式:

第一种是入口起点:

javascript
module.exports = {
  entry: { app: "./app.js", admin: "./admin.js" },
};

这种方式简单直接,但缺点是重复代码无法自动去重。

第二种是动态导入(最常用):

javascript
// 使用 import() 语法
const loadModule = () => import("./module");

// React 项目中使用
const LazyComponent = React.lazy(() => import("./LazyComponent"));

这是目前最推荐的方式,可以实现真正的按需加载。

第三种是 SplitChunksPlugin: 这是 Webpack 4 之后内置的插件,功能最强大,可以智能提取公共模块。

在实际项目中,我们通常会组合使用动态导入和 SplitChunksPlugin,达到最佳效果。

SplitChunksPlugin 的配置和使用 ​

SplitChunksPlugin 是 Webpack 内置的智能分包插件,我通常这样配置:

javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all", // 对所有模块进行分割
      minSize: 20000, // 超过20KB才单独打包
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          priority: 10,
        },
        // 公共模块
        common: {
          name: "common",
          minChunks: 2, // 被2个以上chunk引用
          priority: 5,
        },
      },
    },
  },
};

关键配置说明:

  • chunks: 'all' 是最佳实践,会对所有类型模块进行分割
  • minSize 避免生成过小文件,减少请求数
  • cacheGroups 是核心,可以定义不同的提取策略

在实际项目中,我还会进一步细分第三方库,比如把 React、Vue 这些基础框架单独打包,因为它们更新频率低,可以充分利用缓存。

你在项目中如何进行代码分割?有什么最佳实践? ​

在我的项目中,我采用分层分包策略:

第一层:路由级分割

javascript
// 每个路由单独打包
const Home = lazy(() => import(/* webpackChunkName: "home" */ "./Home"));
const About = lazy(() => import(/* webpackChunkName: "about" */ "./About"));

第二层:组件级分割 对于超过 50KB 的大型组件或复杂功能模块,我会单独分包。

第三层:第三方库优化

javascript
cacheGroups: {
  react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/ },
  utils: { test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/ }
}

代码分割可能会带来什么问题?如何避免? ​

代码分割确实可能带来一些问题,我主要关注以下几点:

问题一:请求过多 过度分割会导致 HTTP 请求数增加,反而影响性能。 解决方案:合理设置 minSize(通常 20-30KB),避免生成过小 chunk。

问题二:加载顺序问题 动态加载的模块可能存在依赖关系问题。 解决方案:使用 Webpack 的魔法注释确保依赖关系:

javascript
import(/* webpackPreload: true */ "./critical-dep");

结语

Webpack 的核心可以浓缩为一句话:以依赖图为骨架,用 Loader 做转换、用 Plugin 管流程,最终打包成可运行的静态产物。理解了「模块 → Chunk → Bundle」的流转,以及 Loader/Plugin 各自的介入时机,再面对 Tree Shaking、HMR、代码分割、Module Federation 这些进阶特性时,就能从「会配置」上升到「懂原理」。工具会更迭(Vite、Rspack、Turbopack),但打包器解决的核心问题——模块化、依赖分析、产物优化——是相通的。