阅读视图

发现新文章,点击刷新页面。

用搬家公司的例子来入门webpack

📚 官方文档Webpack 官网 | Webpack 中文文档

🚀 实践项目webpack-simple-demo - 本文档的完整示例项目

Webpack 主要是干什么的?

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler。简单来说,它就是把你的项目中的各种文件(TSLessJSCSS、图片等)打包成一个或多个浏览器可以直接使用的文件。特别像搬家公司,把你的东西打包进各种盒子里。 英文中"bundle"意为"捆扎、打包",形象地描述了将多个松散文件整合为单一或少量文件的过程。 想象你打包东西,也是一样,把一堆东西打包成一个或多个包装。

核心功能(用简单的话理解)- 打包、依赖、转换、优化

1. 模块打包 📦

  • 作用:把分散的多个文件合并成少数几个文件
  • 比喻:就像把一堆散乱的积木组装成一个完整的模型
  • 实际效果:原本 100 个 JS 文件 → 打包后变成 2-3 个文件

2. 依赖管理 🔗

  • 作用:自动处理文件之间的依赖关系
  • 比喻:就像一个智能管家,知道先加载哪个文件,后加载哪个文件
  • 实际效果:你不用手动管理 <script> 标签的顺序

3. 资源转换 🔄

  • 作用:把各种格式的文件转换成浏览器能理解的格式
  • 例子
    • Sass/SCSS → CSS
    • TypeScript → JavaScript
    • ES6+ → ES5(兼容老浏览器)
    • 图片优化和压缩

4. 代码优化

  • 作用:压缩代码、删除无用代码、合并重复代码
  • 效果:文件变小,加载更快

用生活例子理解:

想象你要搬家:

  • Webpack = 搬家公司
  • 你的文件 = 家里的物品
  • 打包过程 = 整理装箱
  • 最终结果 = 整齐的搬家箱子

搬家公司会:

  1. 把散乱的物品整理好(模块打包
  2. 知道哪些物品要一起放(依赖管理
  3. 把大件拆解成小件方便运输(格式转换
  4. 用最少的箱子装最多的东西(代码优化

Webpack 核心概念详解

1. Entry(入口) 🚪

  • 定义:告诉 Webpack 从哪个文件开始打包
  • 搬家比喻:就像搬家公司问"从哪个房间开始收拾?"
  • 实际例子src/index.js 就是入口,Webpack 从这里开始分析依赖
  • 重要性:入口决定了整个依赖图的起点

2. Output(输出) 📦

  • 定义:告诉 Webpack 把打包后的文件放在哪里
  • 搬家比喻:就像告诉搬家公司"把箱子都放到新家的客厅"
  • 实际例子dist/bundle.js 就是输出文件
  • 配置:可以设置文件名、路径、公共路径等

3. Loaders(加载器) 🔧

  • 定义:告诉 Webpack 如何处理不同类型的文件
  • 搬家比喻:就像搬家公司有专门的工具处理不同物品
    • 易碎品用泡沫包装(CSS Loader
    • 大件家具要拆解(Babel Loader
    • 衣服要折叠压缩(Terser Loader
  • 常见 Loaders
    • babel-loader:处理 JavaScript
    • css-loader:处理 CSS
    • file-loader:处理图片、字体等
    • sass-loader:处理 Sass/SCSS

4. Plugins(插件) 🛠️

  • 定义:扩展 Webpack 功能的工具
  • 搬家比喻:就像搬家公司提供的增值服务
    • 清洁服务(HtmlWebpackPlugin
    • 保险服务(压缩优化插件)
    • 监控服务(开发服务器)
  • 常见 Plugins
    • HtmlWebpackPlugin:自动生成 HTML
    • MiniCssExtractPlugin:提取 CSS
    • CleanWebpackPlugin:清理输出目录

5. Mode(模式) ⚙️

  • 定义:告诉 Webpack 使用哪种模式(开发/生产)
  • 搬家比喻:就像告诉搬家公司"这是临时搬家还是永久搬家"
  • 开发模式:快速打包,保留调试信息
  • 生产模式:优化打包,压缩代码

6. Module(模块) 📄

  • 定义:项目中的每个文件都是一个模块
  • 搬家比喻:就像家里的每个房间、每件物品
  • 特点:模块之间可以有依赖关系
  • 处理Webpack 会分析模块间的依赖关系

7. Chunk(代码块) 🧩

  • 定义:打包后的代码块
  • 搬家比喻:就像整理后的搬家箱子
  • 类型
    • Entry Chunk:入口代码块
    • Vendor Chunk:第三方库代码块
    • Common Chunk:公共代码块

8. Dependency Graph(依赖图) 🕸️

  • 定义:模块之间的依赖关系图
  • 搬家比喻:就像搬家前的物品清单和摆放关系图
  • 作用Webpack 根据依赖图决定打包顺序
  • 特点:有向无环图(DAG

搬家比喻完整版 🏠

想象你要搬家,Webpack 就是专业的搬家公司:

Webpack 概念 搬家比喻 具体说明
Entry 从哪个房间开始收拾 告诉搬家公司从主卧开始
Output 箱子放到新家哪里 所有箱子都放到客厅
Loaders 专业工具处理不同物品 易碎品用泡沫,大件要拆解
Plugins 增值服务 清洁、保险、监控服务
Mode 搬家类型 临时搬家 vs 永久搬家
Module 家里的每个房间/物品 厨房、卧室、客厅等
Chunk 整理后的搬家箱子 按房间分类的箱子
Dependency Graph 物品摆放关系图 知道哪些物品要一起搬

构建项目 - 理解 webpack

💡 完整示例:本文档的所有代码示例都可以在 webpack-simple-demo 项目中找到

mkdir webpack-simple-demo
cd webpack-simple-demo
pnpm init -y

package.json 增加脚本

{
  "name": "webpack-simple-demo",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "build:prod": "webpack --mode=production",
    "dev": "webpack --mode=development"
  },
  "keywords": ["webpack"],
  "license": "MIT",
  "devDependencies": {
    "webpack": "^5.44.0",
    "webpack-cli": "^4.7.2"
  }
}

webpack.config.js

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

src/ 目录结构

src/double.js

function double(a) {
  return a * 2;
}

module.exports = {
  double,
};

src/sumDouble.js

const { double } = require('./double');
function sumDouble(a, b) {
  return double(a + b);
}

module.exports = {
  sumDouble,
};

src/index.js

const { sumDouble } = require('./sumDouble');

console.log(sumDouble(1, 2));

var indexExport = 'indexExport';
module.exports = {
  indexExport,
};

运行构建

pnpm install
pnpm run build

那么会生成一个 dist/bundle.js 文件,这个文件是打包后的文件,包含了所有的依赖和模块。

分析 dist/bundle.js 文件

bundle.js 文件是一个自执行函数,包含了三个主要部分:

  1. 模块定义区域,存储了所有模块(有导出内容的)的代码,以键值对的形式存储。键是模块路径,值是函数,函数接收三个参数,分别是模块对象,导出对象,require 函数,函数内部是模块的代码。
  2. 模块缓存系统,存储了所有模块的缓存结果,以键值对的形式存储。键是模块路径,值是对象,对象的结构是 { exports: {} }exports 是模块的导出对象。
  3. 入口模块执行,执行入口模块的代码,会调用 require 函数,require 函数会调用模块缓存系统,如果模块缓存系统中存在,则直接返回缓存结果,否则会调用模块定义区域,执行模块的代码,设置模块的导出对象,然后缓存结果,最后返回模块的导出对象。

这里注意,src 中的模块里 require 语句会替换为 __webpack_require__ 函数。__webpack_require__ 函数是 Webpack 自己实现的 require 函数,用来替代 Node.jsrequire。它负责加载和管理模块。功能是:检查缓存、创建模块、执行模块、返回导出对象。

(() => {
  // 1.模块定义区域
  var __webpack_modules__ = {
    './src/double.js': (module) => {
      function double(a) {
        return a * 2;
      }
      module.exports = {
        double,
      };
    },
    './src/index.js': (
      module,
      __unused_webpack_exports,
      __webpack_require__
    ) => {
      // require 语句被替换为 __webpack_require__ 函数
      const { sumDouble } = __webpack_require__('./src/sumDouble.js');
      console.log(sumDouble(1, 2));
      var indexExport = 'indexExport';
      module.exports = {
        indexExport,
      };
    },
    './src/sumDouble.js': (
      module,
      __unused_webpack_exports,
      __webpack_require__
    ) => {
      const { double } = __webpack_require__('./src/double.js');
      function sumDouble(a, b) {
        return double(a + b);
      }
      module.exports = {
        sumDouble,
      };
    },
  };
  // 2.模块缓存系统
  var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    // 1.检查缓存
    var cachedModule = __webpack_module_cache__[moduleId];
    // 2.如果缓存中存在,则直接返回缓存模块的导出对象
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // 3.如果缓存中不存在,则创建新的模块对象
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });
    // 4.执行模块的代码
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    // 5.返回模块的导出对象
    return module.exports;
  }

  // 3.入口模块执行
  var __webpack_exports__ = __webpack_require__('./src/index.js');
})();

代码执行逻辑详细分析 🔍

🎯 整体执行流程

这段代码的执行可以分为以下几个关键阶段:

第1阶段:初始化阶段

(() => {
  // 1.模块定义区域
  var __webpack_modules__ = { ... };
  // 2.模块缓存系统
  var __webpack_module_cache__ = {};

作用:创建模块定义对象和缓存系统,为后续模块加载做准备。

第 2 阶段:入口模块加载

var __webpack_exports__ = __webpack_require__('./src/index.js');

作用:从入口模块开始执行,触发整个依赖链的加载。

🔄 详细执行步骤分析

步骤 1:调用入口模块

__webpack_require__('./src/index.js');

执行过程

  1. 检查缓存__webpack_module_cache__['./src/index.js'] 不存在
  2. 创建模块module = { exports: {} }
  3. 执行模块:调用 __webpack_modules__['./src/index.js'](module, module.exports, __webpack_require__)

步骤 2:index.js 模块执行

// index.js 模块代码
const { sumDouble } = __webpack_require__('./src/sumDouble.js');
console.log(sumDouble(1, 2));
var indexExport = 'indexExport';
module.exports = { indexExport };

执行过程

  1. 遇到 require__webpack_require__('./src/sumDouble.js') 被调用
  2. 暂停执行index.js 暂停,等待 sumDouble.js 加载完成

步骤 3:sumDouble.js 模块加载

// sumDouble.js 模块代码
const { double } = __webpack_require__('./src/double.js');
function sumDouble(a, b) {
  return double(a + b);
}
module.exports = { sumDouble };

执行过程

  1. 检查缓存__webpack_module_cache__['./src/sumDouble.js'] 不存在
  2. 创建模块module = { exports: {} }
  3. 遇到 require__webpack_require__('./src/double.js') 被调用
  4. 暂停执行sumDouble.js 暂停,等待 double.js 加载完成

步骤 4:double.js 模块加载

// double.js 模块代码
function double(a) {
  return a * 2;
}
module.exports = { double };

执行过程

  1. 检查缓存__webpack_module_cache__['./src/double.js'] 不存在
  2. 创建模块module = { exports: {} }
  3. 执行模块:定义 double 函数
  4. 设置导出module.exports = { double }
  5. 返回结果:返回 { double }sumDouble.js

步骤 5:sumDouble.js 继续执行

// 现在 double 函数已经可用
function sumDouble(a, b) {
  return double(a + b); // double(1 + 2) = double(3) = 6
}
module.exports = { sumDouble };

执行过程

  1. 继续执行double 函数已经加载完成
  2. 定义函数sumDouble 函数被定义
  3. 设置导出module.exports = { sumDouble }
  4. 返回结果:返回 { sumDouble }index.js

步骤 6:index.js 继续执行

// 现在 sumDouble 函数已经可用
console.log(sumDouble(1, 2)); // 输出:6
var indexExport = 'indexExport';
module.exports = { indexExport };

执行过程

  1. 继续执行sumDouble 函数已经加载完成
  2. 调用函数sumDouble(1, 2) 执行,返回 6
  3. 输出结果console.log(6) 输出 6
  4. 设置导出module.exports = { indexExport }
  5. 返回结果:返回 { indexExport } 给入口

🏠 搬家比喻完整版

想象这是一个三层楼的搬家过程

阶段 搬家比喻 代码执行 状态
初始化 准备搬家工具 创建模块定义和缓存 准备就绪
开始搬家 从 3 楼开始 调用 __webpack_require__('./src/index.js') 开始执行
3 楼需要 2 楼 3 楼需要客厅的东西 require('./src/sumDouble.js') 暂停 3 楼
2 楼需要 1 楼 客厅需要厨房的东西 require('./src/double.js') 暂停 2 楼
1 楼执行 厨房先收拾 double.js 执行完成 1 楼完成
2 楼继续 客厅拿到厨房的东西 sumDouble.js 执行完成 2 楼完成
3 楼继续 3 楼拿到客厅的东西 index.js 执行完成 3 楼完成
搬家完成 所有楼层都收拾好 输出结果:6 全部完成

🔍 关键执行特点

  1. 同步执行
  • 模块加载是同步的,必须等待依赖模块加载完成
  • 不会出现异步加载的情况
  1. 依赖链式加载
index.js → sumDouble.js → double.js
  • 从最底层开始,逐层向上返回
  1. 缓存机制
  • 每个模块只执行一次
  • 后续调用直接返回缓存结果
  1. 作用域隔离
  • 每个模块都有独立的作用域
  • 通过 module.exportsrequire 通信

💡 执行结果

最终执行结果:

  1. 控制台输出6
  2. 模块导出{ indexExport: 'indexExport' }
  3. 缓存状态:三个模块都被缓存

这段代码展示了 Webpack 模块系统的核心机制:

  • 模块化:每个模块独立执行
  • 依赖管理:自动处理模块间的依赖关系
  • 缓存优化:避免重复加载
  • 同步执行:确保依赖关系正确

搬家比喻:就像三层楼的搬家,从顶层开始,逐层向下收集需要的东西,最后完成整个搬家过程!🏠

输出的文件名配置

真实的项目中,output 的输出文件名配置通常是动态的,根据入口文件的名称和哈希值来生成。 而哈希值有三种类型:

  • hash:基于整个项目的构建内容生成全局唯一哈希值,同一次构建过程中所有文件的 hash 值相同,适用于开发环境或需要强制更新所有资源的场景

  • chunkhash:根据入口文件(chunk)及其依赖模块的内容生成独立哈希值,不同入口文件的 chunkhash 互不影响,用于入口 JS 文件命名,例如 [name].[chunkhash].js

  • contenthash:基于文件自身内容生成哈希值,仅当文件内容变化时才会更新,适用于抽离的 CSS 文件或静态资源,避免因关联文件修改导致不必要的缓存失效,例如 [name].[contenthash].css

entry: {
  page1: './src/page1.js',
  page2: './src/page2.js',
},
output: {
  filename: '[name].[chunkhash:8].bundle.js',
  path: path.resolve(__dirname, 'dist'),
}

src 建立四个文件,2 个入口文件,2 个依赖文件:

// src/page1.js
var page1 = require('./page1-a');
console.log('page1');
console.log(page1);

// src/page2.js
var page2 = require('./page2-a');
console.log('page2');
console.log(page2);

// src/page1-a.js
module.exports = 'page1-a';

// src/page2-a.js
module.exports = 'page2-a';

执行 build 之后:

// dist/page1.18656d4d.bundle.js
// dist/page2.18656d4d.bundle.js

如果只有 page1.js 进行修改,那么 page1.bundle.js 的哈希值会变化,而 page2.bundle.js 的哈希值不会变化。

loader 的配置 - 文件类型转换

📖 官方文档Webpack Loaders | Loader API

Webpack 中的 Loader 是用于将非 JavaScript 模块(如 CSS、图片、TypeScript 等)转换为 Webpack 能处理的模块的工具。它们像"翻译员"一样,通过链式调用对文件进行多步转换。

webpack-simple-demo 项目中,我们使用了 ts-loader 来将 ts 文件转换为 js 文件。

webpack.config.js 中,配置了 extensionsmodule.rulesextensions 配置了文件的扩展名,module.rules 配置了 loader 的规则。
rules 中,我们配置了 ts-loader 来将 ts 文件转换为 js 文件。test 属性是正则表达式,用于匹配文件。use 属性是 loader 的名称。exclude 属性是排除的文件。include 属性是包含的文件。includeexclude 是互斥的,只能配置一个。

先配置 package.json

"devDependencies": {
  "typescript": "^5.0.0",
  "ts-loader": "^9.4.0",
  "@types/node": "^20.0.0",
}

然后配置 webpack.config.js

// 模块解析
  resolve: {
    extensions: ['.ts', '.js'],
  },

  // 模块规则
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },

src 的文件结构如下:

// src/page1.ts
import page1A from './page1-a';
console.log(page1A);

// src/page1-a.ts
export default 'page1-a';

// src/page2.ts
import page2A from './page2-a';
console.log(page2A);

// src/page2-a.ts
export default 'page2-a';

先执行 pnpm install,再执行 npm run build 之后:

// dist/page1.95962465.bundle.js
// dist/page2.18656d4d.bundle.js
// dist/page1-a.d.ts
// dist/page2-a.d.ts
// dist/page1.d.ts
// dist/page2.d.ts

这里每个 ts 文件都生成了一个 d.ts 文件,这个文件是类型定义文件,用来定义模块的类型。

如果一个文件需要多个 loader 解析,比如 less 文件,那么需要配置多个 loader,且 loader 的执行顺序是从右到左,也就是数组的最后一个 loader 先执行:

{
  test: /\.less$/,
  use: ['style-loader', 'css-loader', 'less-loader'],
}

这里我们配置了三个 loader,分别是 style-loadercss-loaderless-loader

  • less-loader 用来将 Less 语法转换为普通 CSS,例如:@color: red;color: red;
  • css-loader 处理转换后的 css 文件,解析 @import 语句,处理 url(./image.png) 等资源路径,若启用 modules,会生成类名哈希(如 .class_1234
  • style-loader 将最终 CSS 代码通过 JavaScript 动态插入 DOMdocument.head.innerHTML += '<style>body { color: red; }</style>';

plugin 的配置

📖 官方文档Webpack Plugins | Plugin API

Webpack 中的 plugin(插件)是用于扩展 Webpack 功能的模块,通过在构建流程中注入钩子来实现资源处理、优化等自定义任务。与 loader(专注于文件转换)不同,plugin 作用于 Webpack 的整个编译生命周期,能够执行更广泛的任务,如打包优化、资源管理、环境变量注入等。

webpack-simple-demo 项目中,我们使用了 HtmlWebpackPlugin 来举例说明 plugin 的配置。 HtmlWebpackPluginWebpack 生态中的核心插件,主要用于自动生成 HTML 文件并管理打包资源的注入。

先配置 package.json

"devDependencies": {
  "html-webpack-plugin": "^5.5.3",
}

然后配置 webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

  plugins: [
    // 为 page1 生成 HTML
    new HtmlWebpackPlugin({
      template: './src/templates/page1.html', // 模板文件
      filename: 'page1.html', // 输出文件名
      chunks: ['page1'], // 需要注入到当前HTML的JS模块
      title: 'Page1', // 标题
      meta: { // 元数据
        viewport: 'width=device-width, initial-scale=1',
        description: 'Page1 页面'
      },
      inject: 'body',              // 资源注入位置
      minify: {                    // 生产环境压缩
        removeComments: true,
        collapseWhitespace: true
      }
    }),
    // 为 page2 生成 HTML
    new HtmlWebpackPlugin({
      template: './src/templates/page2.html',
      filename: 'page2.html',
      chunks: ['page2'],
      title: 'Page2',
      meta: {
        viewport: 'width=device-width, initial-scale=1',
        description: 'Page2 页面'
      },
      inject: 'body',
      minify: {
        removeComments: true,
        collapseWhitespace: true
      }
    })
  ],

然后在 srctemplates 里建立两个 html 文件:

// src/templates/page1.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta
      name="description"
      content="<%= htmlWebpackPlugin.options.meta.description %>"
    />
  </head>
  <body>
    <div class="container">
      <h1>Page1</h1>

      <div class="nav">
        <a href="page1.html">Page1</a>
        <a href="page2.html">Page2</a>
      </div>
    </div>
  </body>
</html>

// src/templates/page2.html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta
      name="description"
      content="<%= htmlWebpackPlugin.options.meta.description %>"
    />
  </head>
  <body>
    <div class="container">
      <h1>Page2</h1>

      <div class="nav">
        <a href="page1.html">Page1</a>
        <a href="page2.html">Page2</a>
      </div>
    </div>
  </body>
</html>

先执行 pnpm install,再执行 npm run build 之后:

// dist/page1.html 里面自动注入<script defer src="page1.c4e91d29.bundle.js"></script>
// dist/page2.html 里面自动注入<script defer src="page2.c4e91cb1.bundle.js"></script>

这里生成了两个 HTML 文件,分别是 page1.htmlpage2.html。可以拖动浏览器查看效果。

开发服务器

📖 官方文档Webpack Dev Server | Dev Server API

Webpack 的开发服务器是用于在开发过程中提供实时预览和调试功能的工具。它允许开发者在浏览器中实时查看代码变化,并提供自动刷新和热更新功能,从而加快开发速度和提升开发体验。

webpack-simple-demo 项目中,我们使用了 webpack-dev-server 来举例说明开发服务器的配置。

先配置 package.json

"scripts": {
  "serve": "webpack serve --config webpack.config.js"
},
"devDependencies": {
  "webpack-dev-server": "^4.15.1",
}

然后配置 webpack.config.js

module.exports = {
  // 开发服务器配置
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    compress: true,
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: {
      rewrites: [
        { from: '/', to: '/page1.html' },
        { from: /^\/page1/, to: '/page1.html' },
        { from: /^\/page2/, to: '/page2.html' },
      ],
    },
  },
};

然后执行 npm run serve 之后,浏览器会自动打开 http://localhost:3000 就能看到页面了。

配置区别

一般开发环境配置和生产环境配置的配置是不同的,比如开发环境配置会包含开发服务器配置,而生产环境配置会包含代码压缩配置。

webpack-simple-demo 项目中,我们使用了 webpack-merge 来举例说明配置合并的配置。

先配置 package.json

"devDependencies": {
  "webpack-merge": "^5.8.0",
}

然后配置 webpack.config.base.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 入口文件
  entry: {
    page1: './src/page1.ts',
    page2: './src/page2.ts',
  },

  // 模块解析
  resolve: {
    extensions: ['.ts', '.js'],
  },

  // 模块规则
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },

  // 插件配置
  plugins: [
    // 为 page1 生成 HTML
    new HtmlWebpackPlugin({
      template: './src/templates/page1.html',
      filename: 'page1.html',
      chunks: ['page1'],
      title: 'Page1 - TypeScript Webpack Demo',
      meta: {
        viewport: 'width=device-width, initial-scale=1',
        description: 'Page1 页面 - TypeScript Webpack 示例',
      },
    }),
    // 为 page2 生成 HTML
    new HtmlWebpackPlugin({
      template: './src/templates/page2.html',
      filename: 'page2.html',
      chunks: ['page2'],
      title: 'Page2 - TypeScript Webpack Demo',
      meta: {
        viewport: 'width=device-width, initial-scale=1',
        description: 'Page2 页面 - TypeScript Webpack 示例',
      },
    }),
  ],
};

然后配置 webpack.config.dev.js

const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.config.base');

// @ts-ignore
module.exports = merge(baseConfig, {
  // 模式
  mode: 'development',

  // 开发工具
  devtool: 'eval-cheap-module-source-map',

  // 输出配置(开发环境)
  output: {
    filename: '[name].bundle.js',
    path: require('path').resolve(__dirname, 'dist'),
    clean: true,
  },

  // 开发服务器配置
  devServer: {
    static: {
      directory: require('path').join(__dirname, 'dist'),
    },
    compress: true,
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: {
      rewrites: [
        { from: '/', to: '/page1.html' },
        { from: /^\/page1/, to: '/page1.html' },
        { from: /^\/page2/, to: '/page2.html' },
      ],
    },
  },

  // 开发环境优化
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10,
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
  plugins: [],
});

然后配置 webpack.config.prod.js

const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const baseConfig = require('./webpack.config.base');
// @ts-ignore
module.exports = merge(baseConfig, {
  // 模式
  mode: 'production',

  // 开发工具
  devtool: 'source-map',

  // 输出配置(生产环境)
  output: {
    filename: '[name].[contenthash:8].bundle.js',
    path: require('path').resolve(__dirname, 'dist'),
    clean: true,
  },

  // 生产环境优化
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log', 'console.info', 'console.debug'],
          },
          mangle: {
            safari10: true,
          },
        },
        extractComments: false,
      }),
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10,
          enforce: true,
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true,
          enforce: true,
        },
      },
    },
  },

  // 性能配置
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,
    maxAssetSize: 512000,
  },
});

开发环境的主要特性

  • 模式:使用 development
  • 开发工具:使用 eval-cheap-module-source-map
  • 输出配置:使用 [name].bundle.js
  • 热更新:支持代码修改后自动刷新
  • 开发服务器:内置开发服务器
  • 路由支持:支持单页应用路由
  • 自动打开:启动后自动打开浏览器

这样构建出来的文件:文件不压缩,便于调试;包含源码映射;快速构建;文件大小较大。

生产环境的主要特性

  • 模式:使用 production
  • 开发工具:使用 source-map
  • 输出配置:使用 [name].[contenthash:8].bundle.js
  • 代码压缩:使用 TerserPlugin
  • 文件哈希:使用 contenthash
  • 性能优化:使用 splitChunks
  • 源码映射:使用 source-map
  • 控制台清理:移除 console.log 等调试代码

这样构建出来的文件:代码压缩,文件更小;移除调试代码;文件哈希命名;性能优化。

开发一个自己的 loader - mdhtml

📖 官方文档Writing a Loader | Loader Examples

💡 实践示例:查看 webpack-simple-demo 中的 md2html-loader.js 实现

loader 本质是一个导出函数的 Node.js 模块,接收源文件内容并返回 js 代码字符串或 Buffer 类型数据。 基本模板如下:

module.exports = function (source) {
  // source里最后含有module.exports=xx
  return source;
};

现在想实现,能通过 import xx from './xx.md' 的方式引入 md 文件,并转换为 html 代码,然后插入到 html 文件中。

首先写一个 md2html-loader.js

// 简单的 Markdown 解析器(基础功能)
function simpleMarkdownParser(text) {
  return (
    text
      // 标题
      .replace(/^### (.*$)/gim, '<h3>$1</h3>')
      .replace(/^## (.*$)/gim, '<h2>$1</h2>')
      .replace(/^# (.*$)/gim, '<h1>$1</h1>')
      // 粗体
      .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
      // 斜体
      .replace(/\*(.*)\*/gim, '<em>$1</em>')
      // 代码块
      .replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>')
      // 行内代码
      .replace(/`([^`]*)`/gim, '<code>$1</code>')
      // 链接
      .replace(/\[([^\]]*)\]\(([^)]*)\)/gim, '<a href="$2">$1</a>')
      // 换行
      .replace(/\n/gim, '<br>')
  );
}

module.exports = function (source) {
  const options = this.getOptions() || {};
  console.log('获取use的options配置', options);
  // 解析 Markdown 为 HTML
  const html = simpleMarkdownParser(source);

  // 开发环境下输出转换结果
  if (process.env.NODE_ENV === 'development') {
    console.log('📝 Markdown 转换结果:');
    console.log('原文:', source);
    console.log('HTML:', html);
  }

  // 返回 JavaScript 模块
  return `module.exports = ${JSON.stringify(html)}`;
};

然后配置 webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.md$/,
        use: {
          loader: path.resolve(__dirname, 'md2html-loader'),
          options: {
            breaks: true, // 支持换行符
            headerIds: true, // 生成标题 ID
            mangle: false, // 不混淆 HTML 属性
          },
        },
        exclude: /node_modules/,
      },
    ],
  },
};

然后再 src 里创建一个 page1-a.md 文件:

# page1-a

我是 page1-a 文件

再在 page1.js 文件中引入 page1-a.md 文件:

// page1 入口文件
import page1A from './page1-a';
import page1Md from './page1-a.md';

function renderMd() {
  const div = document.createElement('div');
  div.innerHTML = page1Md;
  document.body.appendChild(div);
}

renderMd();

console.log(page1A);

然后执行 npm run serve 之后,就能在浏览器看到 md 文件的内容了。

开发 plugin 的前置知识

📖 官方文档Writing a Plugin | Plugin API

💡 实践示例:查看 webpack-simple-demo 中的 file-list-plugin.js 实现

plugin 本质是一个导出函数的 Node.js 模块,接收 Compiler 对象,然后通过 Compiler 对象的钩子函数,在构建过程中执行自定义任务。

基本模板如下:

class MyPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    const hooks = compiler.hooks;
    // compile编译开始时触发
    hooks.compile.tap('MyPlugin', (compilation, callback) => {});
    // emit生成资源到输出目录前
    hooks.emit.tap('MyPlugin', (compilation, callback) => {
      console.log('MyPlugin');
    });

    // done编译完成后触发
    hooks.done.tap('MyPlugin', (compilation, callback) => {});
  }
}

module.exports = MyPlugin;

使用的时候:

const MyPlugin = require('./my-plugin');
module.exports = {
  plugins: [new MyPlugin({ text: '1' })],
};

compilercompilation

compilercompilationwebpack 的两大核心对象,它们在插件开发中扮演着重要角色。

compiler

compiler:代表 Webpack 的完整运行环境,从启动到关闭全程存在,仅实例化一次,包含全局配置(如 optionsloadersplugins)和生命周期钩子(如 rundone),插件常通过 compiler.hooks 监听构建事件(如修改配置、触发新编译)

compiler 的核心属性:

  • options:包含 webpack 配置(如 modeentryoutput
  • loaders:包含所有 loader 配置
  • plugins:包含所有 plugin 配置
  • hooks:包含所有钩子函数,插件常通过 compiler.hooks 监听构建事件,常用的钩子函数有:compileemitdone,详见:webpack.js.org/api/compile…

compiler 的核心方法:

  • run:启动编译
  • watch:监听文件变化
  • compile:触发编译
  • make:创建 compilation
  • emit:触发资源输出
  • done:编译完成

compilation

compilation:代表单次编译过程,每次文件变动或重新构建时创建新实例,包含模块依赖图(modules)、代码分块(chunks)和输出资源(assets)等动态数据,插件通过 compilation 操作具体模块(如 emitAsset 添加文件)或优化编译结果

compilation 的核心属性:

  • modules:包含所有模块依赖图
  • chunks:包含所有代码分块
  • assets:包含所有输出资源

compilation 的核心方法:

  • addEntry:添加入口
  • addChunk:添加代码分块
  • addAsset:添加输出资源
  • emitAsset:触发资源输出
  • done:编译完成

联系

当文件修改触发重新构建时:

  • Compiler 持续存在,调用 compile() 方法
  • 创建新的 Compilation 实例处理模块解析和资源生成
  • 通过 compilation.assets 修改输出内容后,由 Compiler 完成最终写入

这种设计实现了配置与编译状态的隔离,确保 watch 模式下仅需重建 Compilation 而非整个环境。

开发一个自己的 plugin

💡 实践示例:查看 webpack-simple-demo 中的 file-list-plugin.js 完整实现

现在想实现,输出一个 md 文件,里面的内容是打包的文件数量和文件的 list

首先写一个 file-list-plugin.js

class FileListPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    const hooks = compiler.hooks;
    // emit生成资源到输出目录前
    hooks.emit.tap('FileListPlugin', (compilation, callback) => {
      const fileNames = Object.keys(compilation.assets);
      const content = fileNames.map((filename) => `- ${filename}`).join('\n');
      compilation.assets[this.options.outputFile] = {
        source: function() {
          return `文件数量是 ${fileNames.length},\n文件列表是:\n${content}`;
        },
        size: function() {
          return Buffer.byteLength(this.source(), 'utf8');
        }
      };
      callback && callback();
    });

    
  }
}

module.exports = FileListPlugin;

然后配置 webpack.config.js

module.exports = {
  plugins: [new FileListPlugin({ outputFile: 'assets.md' })],
};

然后执行 npm run build:dev 之后,就能在 dist 目录下看到 assets.md 文件了。


📚 相关资源

官方文档

核心概念文档

开发工具

高级主题

实践项目

  • webpack-simple-demo - 本文档的完整示例项目
    • 包含所有代码示例
    • 自定义 Loader 和 Plugin 实现
    • 完整的 Webpack 配置
    • TypeScript 支持
    • 开发和生产环境配置

社区资源


🎯 总结

通过本文档,你学会了:

  1. Webpack 基础概念 - 理解了模块打包的核心思想
  2. 配置文件编写 - 掌握了基础、开发、生产环境的配置
  3. Loader 开发 - 学会了如何编写自定义加载器
  4. Plugin 开发 - 掌握了插件系统的使用方法
  5. 实际项目应用 - 通过 webpack-simple-demo 项目实践

继续深入学习 Webpack,建议:

Happy Coding! 🚀

Turbopack vs Webpack vs Vite:前端构建工具三分天下,谁将胜出?

作为一名前端开发者,我们正经历着构建工具快速迭代的时代。从Webpack一统天下,到Vite横空出世,再到今天Turbopack的强势来袭,这个领域正在发生深刻的变革。本文将深度对比这三款工具,帮你做出最合适的技术选型。

性能对决:数据说话

开发服务器启动时间对比(单位:秒)

模块数量 Webpack Vite Turbopack
1,000 3.40 4.80 0.87
5,000 10.70 19.20 3.00
10,000 20.10 37.20 6.10
30,000 76.60 109.50 20.30

热更新响应时间对比(单位:秒)

模块数量 Webpack Vite Turbopack
1,000 0.13 0.09 0.01
5,000 0.39 0.09 0.01
10,000 1.07 0.11 0.01
30,000 3.40 0.26 0.01

从数据可以看出:

  • Turbopack在各方面表现最为出色,特别是热更新几乎恒定为0.01秒
  • Vite在小到中型项目中表现优秀,但在超大型项目中启动时间较长
  • Webpack在项目规模增大时,性能下降最为明显

架构深度解析

Webpack:成熟的打包大师

核心特点:

  • 基于JavaScript的打包器
  • 强大的插件系统和代码分割能力
  • 成熟的生态和社区支持

优势:

// Webpack的配置灵活性是最大优势
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      }
    ]
  }
};

痛点:

  • 随着项目规模增长,构建速度显著下降
  • 配置相对复杂,学习曲线较陡
  • 内存占用较高

Vite:革命性的ESM架构

核心特点:

  • 基于原生ES模块
  • 开发环境No Bundle,生产环境Rollup打包
  • 极快的冷启动和热更新

工作原理:

<!-- Vite利用浏览器原生ES模块支持 -->
<script type="module" src="/src/main.ts"></script>

<!-- 开发服务器直接返回源码,无需打包 -->

优势:

  • 开发环境启动速度极快
  • 配置简单,开箱即用
  • 优秀的开发体验

局限:

  • 超大项目时,浏览器需要加载大量小文件
  • 某些特殊场景下兼容性需要考虑

Turbopack:下一代增量引擎

核心特点:

  • 基于Rust编写的Turbo引擎
  • 增量计算和函数级缓存
  • 极致的按需编译

技术架构:

Turbopack架构:
├── Rust语言层(高性能基础)
├── Turbo引擎(增量计算核心)
├── SWC编译器(替代Babel)
└── 函数级缓存系统(避免重复工作)

突破性创新:

  1. 请求级编译:只编译浏览器实际请求的代码
  2. 持久化缓存:跨会话保持缓存状态
  3. 智能依赖分析:仅重新计算受影响的部分

功能特性对比

特性 Webpack Vite Turbopack
构建语言 JavaScript Go(Rollup JS) Rust
开发体验 良好 优秀 卓越
生产构建 成熟稳定 基于Rollup稳定 快速但较新
生态成熟度 非常成熟 快速成长 早期阶段
TypeScript 需要loader 原生支持 原生支持+快速
CSS处理 完善 完善 完善
框架支持 通用 Vue/React优先 Next.js深度集成
学习成本 较高 较低 中等

实际应用场景

选择Webpack的情况:

  • 企业级复杂应用,需要高度定制化构建流程
  • 项目依赖大量Webpack特有插件
  • 团队对Webpack有深入了解
  • 稳定性优先于构建速度

选择Vite的情况:

  • 新项目,特别是Vue或React技术栈
  • 追求优秀的开发体验和快速启动
  • 项目规模中小型,不需要极端优化
  • 希望配置简单,快速上手

选择Turbopack的情况:

  • 超大型项目,构建速度成为瓶颈
  • Next.js技术栈项目
  • 追求极致的开发体验和热更新速度
  • 愿意尝试新技术,接受一定风险

迁移成本考量

从Webpack迁移:

  • 到Vite:中等难度,需要调整配置和可能处理兼容性问题
  • 到Turbopack:较高难度,目前主要推荐Next.js项目

从Vite迁移:

  • 到Turbopack:中等难度,两者理念相似但配置不同

未来趋势展望

Turbopack的发展势头:

  • Next.js 15.4中Turbopack已通过所有集成测试
  • 配置方式从实验性标志升级为顶级配置
  • 越来越多的特性得到支持

Vite的持续进化:

  • Vite 5进一步优化了性能和稳定性
  • 生态系统持续丰富
  • 在Vue和React社区地位稳固

Webpack的坚守:

  • 仍然是大多数企业的默认选择
  • 持续迭代,性能不断优化
  • 生态系统的深度和广度难以替代

实战建议

对于新项目:

# 小型到中型项目
npm create vite@latest

# Next.js项目,追求极致性能
npx create-next-app@latest --turbo

# 需要高度定制的企业项目
# 仍然可以考虑Webpack

对于现有项目:

  • Webpack项目:如果构建速度可接受,不建议盲目迁移
  • Vite项目:继续使用,体验已经很优秀
  • 考虑部分迁移:大型项目可以逐步迁移到Turbopack

结论

经过全面对比,我们可以得出:

  1. Turbopack在性能上确实领先,特别是对于超大型项目
  2. Vite在开发体验和易用性上找到了完美平衡
  3. Webpack在稳定性和生态成熟度上仍有优势

个人预测: 未来3年内,前端构建工具将呈现三足鼎立格局,不同场景下选择不同工具。但Turbopack代表的增量计算架构,无疑是未来的发展方向。

作为开发者,我们应该根据项目实际需求和技术团队情况,选择最适合的工具,而不是盲目追求最新技术。


你目前在用哪个构建工具?体验如何?欢迎在评论区分享交流!

点赞关注,获取更多前端技术深度分析!

从零开始理解 Webpack:构建现代前端工程的基石

引言

在现代前端开发中,几乎每个项目都离不开构建工具。而提到构建工具,Webpack 无疑是过去十年中最具影响力的代表之一。尽管如今 Vite、Rollup、esbuild 等新秀崛起,Webpack 依然在大型项目、复杂依赖管理和插件生态方面占据重要地位。

本文将带你从零开始理解 Webpack 的核心概念,并通过一个极简示例,揭示它如何将多个模块“打包”成浏览器可运行的代码。


一、为什么需要 Webpack?

早期的前端开发,JavaScript 代码通常直接写在 <script> 标签中,模块化能力几乎为零。随着项目规模扩大,出现了以下问题:

  • 全局变量污染
  • 依赖管理混乱
  • 无法使用 ES6+、TypeScript、Sass 等现代语法
  • 资源(图片、字体、CSS)难以统一处理

Webpack 的出现,正是为了解决这些问题。它通过 模块化打包(Module Bundling),将项目中的所有资源视为“模块”,统一处理、依赖分析、转换和输出。


二、Webpack 的核心概念

Webpack 有五大核心概念,掌握它们就掌握了 80% 的使用逻辑:

  1. Entry(入口)
    打包的起点,Webpack 从这里开始分析依赖图。

    entry: './src/index.js'
    
  2. Output(输出)
    打包后文件的输出位置和命名。

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    }
    
  3. Loader(加载器)
    Webpack 默认只理解 JavaScript。要处理 CSS、图片、TS 等,需通过 Loader 转换。

    module: {
      rules: [
        { test: /\.css$/, use: ['style-loader', 'css-loader'] },
        { test: /\.ts$/, use: 'ts-loader' }
      ]
    }
    
  4. Plugin(插件)
    用于执行更广泛的任务,如压缩代码、生成 HTML、清理目录等。

    plugins: [
      new HtmlWebpackPlugin({ template: './public/index.html' })
    ]
    
  5. Mode(模式)
    developmentproduction,影响代码压缩、source map 等行为。


三、动手实践:一个极简 Webpack 项目

1. 初始化项目

mkdir my-webpack-app && cd my-webpack-app
npm init -y
npm install webpack webpack-cli --save-dev

2. 创建目录结构

my-webpack-app/
├── src/
│   ├── index.js
│   └── math.js
├── dist/
├── webpack.config.js
└── package.json

3. 编写模块

src/math.js

export const add = (a, b) => a + b;

src/index.js

import { add } from './math.js';
console.log('2 + 3 =', add(2, 3));

4. 配置 Webpack

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development'
};

5. 运行打包

npx webpack

查看 dist/bundle.js,你会发现 Webpack 已将两个模块合并为一个可在浏览器中运行的文件!


四、Webpack 的现代演进

虽然 Webpack 启动速度不如 Vite 快,但它在以下场景仍有不可替代的优势:

  • 复杂代码分割(Code Splitting)
  • 强大的插件生态(如 DLLPlugin、Module Federation)
  • 企业级项目的稳定性与兼容性

Webpack 5 还引入了 持久化缓存资源模块(Asset Modules)原生支持 Top-Level Await 等特性,持续保持竞争力。


结语

Webpack 不仅仅是一个打包工具,更是现代前端工程化的缩影。理解它的工作原理,有助于我们更好地选择和使用其他构建工具。

建议:即使你日常使用 Vite,也值得花一天时间亲手配置一个 Webpack 项目——因为“知道轮子怎么造”,才能“更好地开车”。


参考资料


欢迎在评论区分享你的 Webpack 使用经验或踩坑故事!如果你觉得本文有帮助,别忘了点赞、收藏和转发~ 🚀

❌