普通视图

发现新文章,点击刷新页面。
昨天以前首页

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

作者 颜酱
2025年10月10日 22:22

📚 官方文档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! 🚀

❌
❌