重新学习前端之Webpack

1 阅读19分钟

Webpack

一、核心概念(Entry、Output、Loader、Plugin)

1. Webpack 是什么?它的核心概念有哪些?

定义

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(Module Bundler)。它能够分析项目结构,找到 JavaScript 模块以及浏览器不能直接运行的扩展语言(如 TypeScript、SCSS 等),并将其转换和打包为浏览器可用的格式。

核心概念

Webpack 有四个核心概念:

概念说明
Entry(入口)指示 Webpack 从哪个文件开始打包,是构建依赖图的起点
Output(输出)指示 Webpack 将打包后的文件输出到哪里,以及如何命名
Loader转换非 JavaScript 模块,让 Webpack 能够处理各种类型的文件
Plugin(插件)执行范围更广的任务,包括打包优化、资源管理、环境变量注入等
原理

Webpack 的工作原理可以概括为以下几个步骤:

  1. 初始化参数:读取配置文件(webpack.config.js)和命令行参数
  2. 开始编译:根据参数初始化 Compiler 对象,加载所有插件
  3. 确定入口:根据 Entry 配置找到所有入口文件
  4. 编译模块:从入口文件出发,递归构建依赖关系图
  5. 完成编译:所有模块编译完成后,确定输出的 Chunk
  6. 输出资源:根据 Output 配置将 Chunk 输出到文件系统
配置示例
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 入口
  entry: './src/index.js',
  
  // 输出
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash].js',
  },
  
  // Loader
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  
  // Plugin
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};
常见误区
  • 误区 1:Webpack 只能处理 JavaScript 文件。实际上通过 Loader,Webpack 可以处理任何类型的文件
  • 误区 2:Loader 和 Plugin 功能相同。Loader 用于模块转换,Plugin 用于扩展打包流程
  • 误区 3:Webpack 只适用于大型项目。即使是小型项目,Webpack 也能提供模块化和优化的能力

2. Entry(入口)的作用和配置

定义

Entry 是 Webpack 构建依赖关系图的起点。Webpack 从 Entry 开始,递归分析所有依赖的模块,最终将所有模块打包成 Bundle。

配置方式

单入口(字符串形式)

module.exports = {
  entry: './src/index.js'
};

单入口(数组形式)

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

多入口(对象形式)

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js',
    vendor: ['react', 'react-dom']
  }
};
不同入口类型的区别
类型语法适用场景
单入口(字符串)'./src/index.js'单页面应用(SPA)
单入口(数组)['./src/polyfill.js', './src/index.js']需要在入口前注入 polyfill
多入口(对象){ app: './src/app.js', admin: './src/admin.js' }多页面应用(MPA)
原理

当 Webpack 启动时,它会:

  1. 读取 Entry 配置
  2. 解析入口文件的绝对路径
  3. 读取入口文件内容
  4. 分析 AST(抽象语法树),找出所有的依赖模块(import/require)
  5. 递归处理每个依赖模块,构建完整的依赖关系图
常见误区
  • 误区 1:入口文件只能有一个。实际上可以配置多个入口
  • 误区 2:Entry 必须是 JS 文件。Entry 可以是任何 Webpack 能解析的文件类型

3. Output(输出)的配置

定义

Output 配置指示 Webpack 如何输出打包后的文件,包括输出路径、文件名、公共路径等。

常用配置项
const path = require('path');

module.exports = {
  output: {
    // 输出目录(必须是绝对路径)
    path: path.resolve(__dirname, 'dist'),
    
    // 输出文件名
    filename: 'bundle.js',
    
    // 按需加载的 Chunk 文件名
    chunkFilename: '[name].[contenthash].chunk.js',
    
    // 公共资源路径
    publicPath: '/',
    
    // 清理输出目录
    clean: true,
    
    // 跨域加载资源
    crossOriginLoading: 'anonymous'
  }
};
文件名占位符
占位符说明示例
[name]Chunk 名称app.js
[hash]编译哈希值bundle.a1b2c3d4.js
[contenthash]基于文件内容的哈希值bundle.e5f6g7h8.js
[id]Chunk IDbundle.0.js
[fullhash]完整编译哈希bundle.x1y2z3w4.js
多入口配置示例
module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].[contenthash:8].js',
    path: path.resolve(__dirname, 'dist')
  }
};
// 输出: dist/app.a1b2c3d4.js, dist/admin.e5f6g7h8.js
常见误区
  • 误区 1:使用 [hash] 进行缓存控制。应该使用 [contenthash],因为 [hash] 是全局的,任何一个文件变化都会导致所有文件名变化
  • 误区 2:output.path 使用相对路径。必须使用绝对路径

4. Loader 的作用和使用

定义

Loader 是 Webpack 的模块转换器,它能够将各种类型的文件转换为 Webpack 能够处理的有效模块。本质上,Loader 就是一个函数,接收源文件内容作为参数,返回转换后的结果。

原理

Loader 的工作流程:

  1. Webpack 在解析模块时,根据配置的 rules 匹配文件
  2. 匹配成功后,调用对应的 Loader 函数
  3. Loader 接收文件内容作为输入,进行处理后返回新的内容
  4. 如果有多个 Loader,按照配置的顺序依次执行
Loader 配置方式

方式一:use(单个)

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};

方式二:use(多个)

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

方式三:inline(内联)

import styles from 'css-loader!./style.css';

方式四:CLI

webpack --module-bind 'css=style-loader!css-loader'
Loader 的执行顺序
Loader 的执行顺序是从右到左,从下到上

示例:
use: ['style-loader', 'css-loader', 'postcss-loader']

执行顺序:
1. postcss-loader(最先执行)
2. css-loader
3. style-loader(最后执行)
常见 Loader
Loader作用
babel-loader将 ES6+ 转换为 ES5
css-loader解析 CSS 文件中的 @import 和 url()
style-loader将 CSS 注入到 DOM 中
sass-loader将 Sass/SCSS 编译为 CSS
less-loader将 Less 编译为 CSS
postcss-loader使用 PostCSS 处理 CSS
ts-loader将 TypeScript 转换为 JavaScript
file-loader将文件发送到输出目录
url-loader将文件转换为 base64 Data URL
eslint-loader使用 ESLint 检查 JavaScript 代码
自定义 Loader 示例
// my-loader.js
module.exports = function(source) {
  // source 是源文件内容
  // 进行转换处理
  const result = source.replace(/foo/g, 'bar');
  // 返回转换后的结果
  return result;
};

// 使用
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve('my-loader.js')
      }
    ]
  }
};

5. Plugin 的作用和使用

定义

Plugin(插件)用于扩展 Webpack 的功能。与 Loader 不同,Plugin 能够执行更广泛的任务,包括打包优化、资源管理、环境变量注入等。Plugin 基于 Tapable 事件流机制,在 Webpack 生命周期的不同阶段执行特定操作。

原理

Webpack Plugin 的工作原理:

  1. Plugin 是一个具有 apply 方法的 JavaScript 对象
  2. apply 方法会被 Webpack compiler 调用
  3. apply 方法中,通过 compiler 对象挂载各种钩子(Hook)
  4. 当 Webpack 执行到对应生命周期时,会触发这些钩子
Plugin 配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new CleanWebpackPlugin()
  ]
};
自定义 Plugin 示例
class MyWebpackPlugin {
  apply(compiler) {
    // compilation 钩子在每次构建时触发
    compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
      // 在输出目录添加一个文件
      compilation.assets['info.txt'] = {
        source: () => '构建信息',
        size: () => '构建信息'.length
      };
      callback();
    });
    
    // done 钩子在构建完成后触发
    compiler.hooks.done.tap('MyWebpackPlugin', (stats) => {
      console.log('构建完成!');
    });
  }
}

module.exports = MyWebpackPlugin;
Webpack 生命周期钩子
钩子说明
entryOption入口配置处理完成后
afterPlugins插件加载完成后
run开始读取 records
compile开始编译
compilation创建 compilation 对象时
emit输出资源到 output 目录之前
afterEmit输出资源到 output 目录之后
done构建完成

6. Loader 与 Plugin 的区别

对比分析
维度LoaderPlugin
定义模块转换器,用于转换文件内容扩展程序,用于扩展 Webpack 功能
功能将非 JS 文件转换为 Webpack 可处理的模块执行打包优化、资源管理、环境变量注入等
本质一个函数,接收源文件,返回转换后的内容一个类,具有 apply 方法
执行时机在模块加载时执行在 Webpack 生命周期的不同阶段执行
配置方式module.rules 中配置plugins 数组中配置
执行顺序从右到左,从下到上按注册顺序执行
工作范围针对单个文件针对整个构建流程
典型场景转译 JS、编译 CSS、处理图片等生成 HTML、清理目录、压缩代码等
选择策略
  • 使用 Loader:当你需要将一种文件格式转换为另一种格式时
  • 使用 Plugin:当你需要影响整个构建流程或添加额外功能时
示例对比
module.exports = {
  module: {
    rules: [
      // Loader:处理单个文件
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']  // 转换 CSS 文件
      }
    ]
  },
  plugins: [
    // Plugin:影响整个构建流程
    new HtmlWebpackPlugin()  // 生成 HTML 文件并注入打包后的资源
  ]
};

二、模块打包原理

7. Webpack 打包原理

定义

Webpack 打包原理是指 Webpack 如何从入口文件开始,分析模块依赖关系,并将所有模块合并为一个或多个 Bundle 的过程。

原理分析

Webpack 打包的核心流程:

  1. 初始化阶段

    • 读取配置文件和命令行参数
    • 初始化 Compiler 对象
    • 加载所有插件
    • 创建 compilation 对象
  2. 构建阶段

    • 从 Entry 开始,解析入口文件
    • 使用 Loader 处理不同类型的文件
    • 分析 AST(抽象语法树),找出所有依赖(import/require)
    • 递归处理依赖,构建完整的依赖关系图(Module Graph)
  3. 优化阶段

    • Tree Shaking 消除无用代码
    • 代码分割(Code Splitting)
    • 模块合并和压缩
  4. 输出阶段

    • 将优化后的模块组装成 Chunk
    • 将 Chunk 转换为最终输出的 Bundle
    • 写入文件系统
源码解读
// Webpack 打包的核心逻辑(简化版)
const webpack = require('webpack');

function runWebpack(config) {
  // 1. 初始化 Compiler
  const compiler = webpack(config);
  
  // 2. 开始编译
  compiler.run((err, stats) => {
    if (err) {
      console.error(err);
      return;
    }
    console.log('构建完成');
  });
}
打包后的代码结构
// Webpack 打包后的简化结构
(function(modules) {
  // 模块缓存
  const installedModules = {};
  
  // require 函数
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    
    const module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    
    modules[moduleId].call(
      module.exports, 
      module, 
      module.exports, 
      __webpack_require__
    );
    
    module.l = true;
    return module.exports;
  }
  
  // 加载入口模块
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js": (function(module, exports, __webpack_require__) {
    const utils = __webpack_require__("./src/utils.js");
    console.log(utils.add(1, 2));
  }),
  "./src/utils.js": (function(module, exports) {
    exports.add = (a, b) => a + b;
  })
});

8. Module、Chunk、Bundle 的区别

定义
概念定义
Module(模块)源代码中的单个文件,如一个 JS 文件、CSS 文件、图片文件等
Chunk(代码块)Webpack 在构建过程中生成的代码块,由多个 Module 组成
Bundle(打包文件)最终输出的文件,由一个或多个 Chunk 生成
关系说明
Module → Chunk → Bundle

1. Module 是源代码中的单个文件
2. Webpack 将多个 Module 组合成一个或多个 Chunk
3. 每个 Chunk 最终生成一个或多个 Bundle
详细解释

Module

  • 就是项目中的各个文件
  • Webpack 支持多种 Module 格式:ES Modules、CommonJS、AMD
  • 通过 Loader,任何文件都可以成为 Module

Chunk

  • 是 Webpack 打包过程中的中间产物
  • 一个 Entry 生成一个 Chunk
  • 动态导入(import())也会生成额外的 Chunk
  • SplitChunksPlugin 可以将公共模块提取为单独的 Chunk

Bundle

  • 是最终输出的文件
  • 一个 Chunk 可能生成多个 Bundle(如 JS + Source Map)
  • 最终部署到服务器的文件
示例说明
// webpack.config.js
module.exports = {
  entry: {
    app: './src/app.js',
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].bundle.js'
  }
};
输入:
  - src/app.js (Module)
  - src/utils.js (Module)
  - node_modules/react/index.js (Module)
  - node_modules/react-dom/index.js (Module)

Webpack 处理:
  - app Chunk: app.js + utils.js
  - vendor Chunk: react + react-dom

输出:
  - app.bundle.js (Bundle)
  - vendor.bundle.js (Bundle)

9. 模块依赖图(Dependency Graph)

定义

模块依赖图是 Webpack 用来记录模块之间依赖关系的数据结构。Webpack 从入口文件开始,递归分析所有依赖,最终构建出一个完整的依赖图。

原理
依赖图构建流程:

1. 从 Entry 开始
2. 解析文件内容,生成 AST
3. 分析 AST 中的导入语句(import/require)
4. 找到被导入的模块
5. 递归处理每个模块
6. 构建完整的依赖关系图

示例:
index.js
  ├── app.js
  │     ├── utils.js
  │     └── api.js
  └── styles.css
        └── variables.css
依赖类型

Webpack 支持的模块导入方式:

类型语法说明
ES Modulesimport foo from './foo'静态导入,编译时确定
CommonJSrequire('./foo')动态导入,运行时确定
AMDrequire(['./foo'], callback)异步模块定义
动态导入import('./foo')返回 Promise,用于代码分割

三、代码分割与懒加载

10. 代码分割(Code Splitting)

定义

代码分割是 Webpack 最重要的特性之一,它允许将代码分割成多个 Bundle,按需加载或并行加载,从而减小首屏加载体积,提高页面加载速度。

为什么需要代码分割?
  • 首屏加载优化:只加载当前页面需要的代码
  • 缓存优化:将不常变化的代码(如第三方库)单独打包
  • 并行加载:多个小文件可以并行下载
代码分割策略

策略一:多入口分割

module.exports = {
  entry: {
    app: './src/app.js',
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].[contenthash].js'
  }
};

策略二:动态导入(推荐)

// 使用 import() 动态导入
button.addEventListener('click', async () => {
  const { default: Modal } = await import('./Modal');
  modal.show();
});

// React 路由懒加载
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

策略三:SplitChunksPlugin

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',  // all | async | initial
      minSize: 20000,  // 最小体积
      maxSize: 0,      // 最大体积
      minChunks: 1,    // 最小引用次数
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};
SplitChunksPlugin 配置详解
配置项说明默认值
chunks选择哪些 Chunk 进行分割async
minSize生成 Chunk 的最小体积20000
maxSize生成 Chunk 的最大体积0
minChunks被引用次数1
maxAsyncRequests按需加载时的最大并行请求数30
maxInitialRequests入口点的最大并行请求数30
cacheGroups缓存组,定义分割规则-

11. 懒加载(Lazy Loading)

定义

懒加载是一种按需加载资源的策略。只有当用户实际需要某个模块时,才去加载它,而不是一开始就加载所有代码。

实现方式

方式一:动态 import()

// 基础用法
const loadModule = async () => {
  const module = await import('./module.js');
  module.doSomething();
};

// 条件加载
if (needFeature) {
  import('./feature.js').then(module => {
    module.init();
  });
}

方式二:React.lazy

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

方式三:Vue 路由懒加载

const routes = [
  {
    path: '/home',
    component: () => import('./Home.vue')
  },
  {
    path: '/about',
    component: () => import('./About.vue')
  }
];
原理

动态 import() 的实现原理:

  1. Webpack 在编译时遇到 import(),会将其视为一个代码分割点
  2. import() 引用的模块及其依赖单独打包成一个 Chunk
  3. 运行时,import() 返回一个 Promise
  4. 当 Promise 解析时,通过 JSONP 或 Fetch 加载对应的 Chunk
  5. 加载完成后执行模块代码
最佳实践
  • 路由级别懒加载:按页面分割代码
  • 组件级别懒加载:对不首屏显示的组件进行懒加载
  • 事件触发的懒加载:对需要用户交互才展示的模块进行懒加载
  • 避免过度分割:过多的小文件会增加 HTTP 请求数量

12. 预加载(Preload)和 Prefetch

定义
类型说明适用场景
Preload预加载当前页面可能需要的资源,优先级高当前页面马上会用到的资源
Prefetch预加载未来页面可能需要的资源,优先级低未来页面可能用到的资源
Webpack 实现

Preload

// webpackPrefetch: false(默认)
// webpackPreload: true
import(/* webpackPreload: true */ './Chart.js');

Prefetch

import(/* webpackPrefetch: true */ './LoginModal.js');
对比
维度PreloadPrefetch
优先级高(与当前页面资源相同)低(浏览器空闲时加载)
加载时机立即加载空闲时加载
适用场景当前页面需要的资源下一个页面可能需要的资源
缓存立即缓存空闲时缓存
示例
// 预加载:当前页面马上会展示的模态框
import(/* webpackPreload: true */ './CriticalModal.js');

// Prefetch:用户可能会点击的下一个页面
button.addEventListener('mouseenter', () => {
  import(/* webpackPrefetch: true */ './NextPage.js');
});

四、Tree Shaking

13. Tree Shaking 的原理

定义

Tree Shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(Dead Code)。它依赖于 ES Modules 的静态结构分析。

原理分析

Tree Shaking 的工作流程:

  1. ES Modules 静态分析:Webpack 利用 ES Modules 的静态导入导出特性,在编译时确定哪些模块被使用
  2. 标记未使用代码:通过静态分析,标记未被引用的导出
  3. Uglify/Terser 压缩:在压缩阶段,移除标记为未使用的代码
必要条件

Tree Shaking 生效需要满足以下条件:

条件说明
ES Modules必须使用 ES Modules 语法(import/export)
production 模式需要在 production 模式下或手动配置优化
sideEffects 配置正确配置 sideEffects 标记无副作用的模块
pure 标记使用 /*#__PURE__*/ 标记纯函数
配置示例
// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,     // 标记使用的导出
    minimize: true,         // 启用压缩
    minimizer: [
      new TerserPlugin()    // 移除未使用代码
    ]
  }
};
// package.json
{
  "name": "my-app",
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}
代码示例
// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// index.js
import { add } from './math.js';
console.log(add(1, 2));

// Tree Shaking 后,subtract 函数会被移除
常见误区
  • 误区 1:CommonJS 也支持 Tree Shaking。实际上 CommonJS 是动态的,无法静态分析
  • 误区 2:只要用了 ES Modules 就自动生效。还需要配置 optimization 和 sideEffects
  • 误区 3:所有未使用的代码都会被移除。有副作用的代码不会被移除

14. sideEffects 的作用

定义

sideEffects 用于告诉 Webpack 哪些模块有副作用(修改全局变量、修改原型、产生输出等),以便 Tree Shaking 时保留这些模块。

副作用的定义

副作用是指函数或模块在执行时,除了返回值之外还对外部环境产生了影响,如:

  • 修改全局变量
  • 修改内置对象原型
  • 发送网络请求
  • 操作 DOM
  • 引入 CSS 文件
配置方式

方式一:package.json

{
  "name": "my-library",
  "sideEffects": false
}
{
  "name": "my-library",
  "sideEffects": [
    "./src/polyfill.js",
    "*.css",
    "*.scss"
  ]
}

方式二:webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        sideEffects: false
      }
    ]
  }
};
示例说明
// polyfill.js(有副作用)
Array.prototype.customMethod = function() {
  // 修改 Array 原型
};

// styles.css(有副作用)
// CSS 文件天然有副作用,会修改页面样式

// utils.js(无副作用)
export function add(a, b) {
  return a + b;
}

五、热模块替换(HMR)

15. HMR(Hot Module Replacement)原理

定义

HMR(Hot Module Replacement,热模块替换)是 Webpack 提供的最有用的功能之一。它允许在运行时更新各种类型的模块,而无需完全刷新页面。

原理分析

HMR 的工作流程:

1. 启动 Webpack Dev Server
2. 在浏览器和服务器之间建立 WebSocket 连接
3. 文件发生变化
4. Webpack 重新编译修改的模块
5. 通过 WebSocket 通知浏览器
6. 浏览器请求更新的模块(JSONP)
7. 运行时替换旧模块
8. 模块接受更新,页面不刷新
详细流程

服务器端

  1. Webpack 监听文件变化
  2. 文件变化时,重新编译受影响的模块
  3. 将新模块的 hash 和更新内容发送到内存中
  4. 通过 WebSocket 通知浏览器有更新

客户端

  1. 浏览器接收到更新通知(包含新 hash)
  2. 通过 AJAX 请求获取更新的 manifest 和 chunk
  3. 运行时检查模块是否接受更新
  4. 如果模块配置了 module.hot.accept,执行替换逻辑
  5. 如果没有配置,向上冒泡到父模块
配置方式
// webpack.config.js
const webpack = require('webpack');

module.exports = {
  devServer: {
    hot: true,          // 启用 HMR
    open: true,         // 自动打开浏览器
    port: 3000,
    host: 'localhost'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};
模块接受更新
// index.js
import { add } from './math.js';
console.log(add(1, 2));

if (module.hot) {
  module.hot.accept('./math.js', () => {
    console.log('math.js 已更新');
    // 重新执行更新后的逻辑
  });
}
框架集成

React HMR

// 使用 react-refresh 或 @pmmmwh/react-refresh-webpack-plugin
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: {
    hot: true
  },
  plugins: [
    new ReactRefreshWebpackPlugin()
  ]
};

Vue HMR

// Vue 的 vue-loader 自动支持 HMR
// 无需额外配置,修改 .vue 文件会自动更新

16. webpack-dev-server 的使用

定义

webpack-dev-server 是一个小型的 Node.js Express 服务器,用于开发环境提供实时重载和 HMR 功能。

常用配置
module.exports = {
  devServer: {
    // 服务器配置
    port: 3000,
    host: 'localhost',
    open: true,
    
    // 热更新
    hot: true,
    liveReload: true,
    
    // 静态文件
    static: {
      directory: path.join(__dirname, 'public'),
    },
    
    // 代理
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        pathRewrite: { '^/api': '' },
        changeOrigin: true
      }
    },
    
    // 路由回退
    historyApiFallback: true,
    
    // 压缩
    compress: true
  }
};

六、性能优化

17. Webpack 性能优化策略

优化方向

Webpack 性能优化主要分为两个方向:

方向目标方法
构建速度优化减少编译时间缓存、多线程、缩小范围等
构建体积优化减小输出文件大小Tree Shaking、压缩、代码分割等

18. 构建速度优化

方法一:缩小文件搜索范围
module.exports = {
  // 指定模块解析路径
  resolve: {
    modules: [path.resolve('node_modules')],
    extensions: ['.js', '.json', '.vue'],
    alias: {
      '@': path.resolve('src')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        // 排除不需要处理的目录
        exclude: /node_modules/,
        // 只处理指定目录
        include: path.resolve('src'),
        use: 'babel-loader'
      }
    ]
  }
};
方法二:使用缓存

Webpack 5 内置缓存

module.exports = {
  cache: {
    type: 'filesystem',  // 使用文件系统缓存
    cacheDirectory: path.resolve('.webpack-cache')
  }
};

cache-loader(Webpack 4)

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['cache-loader', 'babel-loader']
      }
    ]
  }
};
方法三:多线程处理

thread-loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'thread-loader',  // 放在其他 loader 前面
          'babel-loader'
        ]
      }
    ]
  }
};

HappyPack(Webpack 4)

const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'happypack/loader'
      }
    ]
  },
  plugins: [
    new HappyPack({
      loaders: ['babel-loader']
    })
  ]
};
方法四:DLL 预编译

DllPlugin 配置

// webpack.dll.js
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve('dll'),
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.resolve('dll/[name]-manifest.json')
    })
  ]
};

DllReferencePlugin 使用

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor-manifest.json')
    })
  ]
};
方法五:externals 排除依赖
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    vue: 'Vue'
  }
};

通过 CDN 引入这些库,避免打包它们。


19. 构建体积优化

方法一:Tree Shaking
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    minimize: true
  }
};
方法二:代码分割
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};
方法三:代码压缩

JavaScript 压缩

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,  // 并行压缩
        terserOptions: {
          compress: {
            drop_console: true,  // 移除 console
            drop_debugger: true
          }
        }
      })
    ]
  }
};

CSS 压缩

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  }
};
方法四:图片压缩
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024  // 8kb 以下转 base64
          }
        }
      }
    ]
  }
};
方法五:Gzip 压缩
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,  // 10kb 以上才压缩
      minRatio: 0.8
    })
  ]
};
优化效果对比
优化方法优化前优化后效果
Tree Shaking500kb350kb-30%
代码分割500kb首屏 200kb-60%
代码压缩200kb100kb-50%
Gzip 压缩100kb30kb-70%

七、多环境配置

20. 多环境配置方案

定义

多环境配置是指针对不同运行环境(开发、测试、生产)使用不同的 Webpack 配置。

目录结构
webpack/
├── webpack.common.js    # 公共配置
├── webpack.dev.js       # 开发环境配置
├── webpack.prod.js      # 生产环境配置
└── webpack.test.js      # 测试环境配置
使用 webpack-merge
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    hot: true,
    open: true,
    port: 3000
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ]
  }
});
package.json 配置
{
  "scripts": {
    "dev": "webpack serve --config webpack/webpack.dev.js",
    "build": "webpack --config webpack/webpack.prod.js",
    "build:test": "webpack --config webpack/webpack.test.js"
  }
}

21. 环境变量配置

使用 DefinePlugin
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
      'process.env.API_URL': JSON.stringify('https://api.example.com'),
      __DEV__: JSON.stringify(false)
    })
  ]
};
使用 EnvironmentPlugin
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.EnvironmentPlugin(['NODE_ENV', 'API_URL'])
  ]
};
使用 --env 参数
// webpack.config.js
module.exports = (env) => {
  console.log('NODE_ENV: ', env.NODE_ENV);
  
  return {
    mode: env.NODE_ENV === 'production' ? 'production' : 'development',
    devtool: env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map'
  };
};
webpack --env NODE_ENV=production

八、常用 Loader 与 Plugin

22. 常用 Loader 详解

babel-loader
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        ['@babel/preset-env', {
          targets: '> 1%, last 2 versions',
          useBuiltIns: 'usage',
          corejs: 3
        }]
      ],
      plugins: [
        ['@babel/plugin-transform-runtime', {
          corejs: 3
        }]
      ]
    }
  }
}
css-loader + style-loader
{
  test: /\.css$/,
  use: [
    'style-loader',      // 将 CSS 注入 DOM
    {
      loader: 'css-loader',
      options: {
        modules: true,    // 启用 CSS Modules
        importLoaders: 1  // 在 css-loader 前应用的 loader 数量
      }
    }
  ]
}
sass-loader / less-loader
{
  test: /\.scss$/,
  use: [
    'style-loader',
    'css-loader',
    'sass-loader'
  ]
}
postcss-loader
{
  test: /\.css$/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            require('autoprefixer'),
            require('postcss-preset-env')
          ]
        }
      }
    }
  ]
}
url-loader / file-loader
// Webpack 5 推荐使用 Asset Modules
{
  test: /\.(png|jpg|gif|svg)$/,
  type: 'asset',
  parser: {
    dataUrlCondition: {
      maxSize: 8 * 1024  // 8kb 以下转 base64
    }
  },
  generator: {
    filename: 'images/[name].[hash:8][ext]'
  }
}

23. 常用 Plugin 详解

HtmlWebpackPlugin
new HtmlWebpackPlugin({
  template: './public/index.html',
  filename: 'index.html',
  title: 'My App',
  minify: {
    collapseWhitespace: true,
    removeComments: true,
    removeRedundantAttributes: true
  },
  chunks: ['app', 'vendor']
})
CleanWebpackPlugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  plugins: [
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['dist/**/*']
    })
  ]
};
MiniCssExtractPlugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css'
    })
  ]
};
ProvidePlugin
new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
  React: 'react'
});
BannerPlugin
new webpack.BannerPlugin({
  banner: `Build Date: ${new Date().toLocaleDateString()}`,
  raw: false,
  entryOnly: true
});
BundleAnalyzerPlugin
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server',
      analyzerPort: 8888,
      openAnalyzer: true
    })
  ]
};

九、Webpack 工作流程

24. Webpack 构建流程详解

完整流程
Webpack 构建流程

1. 初始化阶段
   - 读取配置文件
   - 初始化 Compiler 对象
   - 加载所有插件
   - 调用插件的 apply 方法

2. 编译阶段
   - 创建 compilation 对象
   - 从 Entry 开始,解析入口文件
   - 使用 Loader 处理文件
   - 分析 AST,找出依赖模块
   - 递归处理所有依赖

3. 优化阶段
   - Tree Shaking
   - 代码分割
   - 模块合并

4. 输出阶段
   - 组装 Chunk
   - 生成 Bundle
   - 写入文件系统
核心对象

Compiler

// Compiler 代表完整的 Webpack 环境配置
// 在启动时创建一次
const compiler = webpack(config);

compiler.hooks.run.tap('MyPlugin', () => {
  console.log('开始编译');
});

Compilation

// Compilation 代表一次新的构建
// 每次文件变化都会创建新的 Compilation
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  compilation.hooks.optimize.tap('MyPlugin', () => {
    console.log('优化模块');
  });
});
Tapable 机制
const { SyncHook, AsyncSeriesHook } = require('tapable');

class MyCompiler {
  constructor() {
    this.hooks = {
      run: new SyncHook(['params']),
      compile: new AsyncSeriesHook(['params'])
    };
  }
  
  run() {
    this.hooks.run.call('start');
  }
  
  async compile() {
    await this.hooks.compile.promise('compile');
  }
}
Webpack 生命周期钩子
钩子类型说明
initializeSyncBailHook初始化
afterPluginsSyncHook插件加载完成后
afterResolversSyncHookresolver 配置完成后
environmentSyncHook环境准备完成后
beforeRunAsyncSeriesHook运行之前
runAsyncSeriesHook开始读取 records
emitAsyncSeriesHook输出资源之前
afterEmitAsyncSeriesHook输出资源之后
doneSyncHook构建完成

25. 自定义 Loader

定义

自定义 Loader 是一个导出函数的模块,该函数接收文件内容作为参数,返回转换后的结果。

示例
// replace-loader.js
module.exports = function(source) {
  // source 是源文件内容
  const options = this.getOptions();
  const result = source.replace(
    new RegExp(options.find, 'g'),
    options.replace
  );
  // 返回转换后的内容
  return result;
};

// 使用
{
  test: /\.js$/,
  use: {
    loader: path.resolve('replace-loader.js'),
    options: {
      find: 'foo',
      replace: 'bar'
    }
  }
}
Loader API
API说明
this.getOptions()获取 Loader 配置选项
this.callback()返回多个值
this.async()声明异步 Loader
this.cacheable()设置缓存
this.addDependency()添加文件依赖
异步 Loader
module.exports = function(source) {
  const callback = this.async();
  
  setTimeout(() => {
    const result = source.toUpperCase();
    callback(null, result);
  }, 1000);
};

26. 自定义 Plugin

定义

自定义 Plugin 是一个具有 apply 方法的类,在 apply 方法中通过 compiler 对象挂载钩子函数。

示例
class FileListPlugin {
  constructor(options) {
    this.options = options;
  }
  
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'FileListPlugin',
      (compilation, callback) => {
        // 生成文件列表
        let fileList = 'Files in this build:\n\n';
        
        for (let filename in compilation.assets) {
          fileList += `- ${filename}\n`;
        }
        
        // 添加到输出
        compilation.assets['filelist.md'] = {
          source: () => fileList,
          size: () => fileList.length
        };
        
        callback();
      }
    );
  }
}

module.exports = FileListPlugin;
Compiler vs Compilation
维度CompilerCompilation
生命周期整个 Webpack 生命周期单次构建生命周期
创建时机Webpack 启动时每次编译时
包含信息配置、插件、Loader 等模块、依赖、资源等
常用场景全局性操作模块级操作

十、Webpack 5 新特性

27. Webpack 5 新特性

持久化缓存
module.exports = {
  cache: {
    type: 'filesystem',  // 使用文件系统缓存
    buildDependencies: {
      config: [__filename]  // 配置变化时重建缓存
    }
  }
};
资源模块(Asset Modules)

Webpack 5 内置了资源模块,替代了 file-loader、url-loader、raw-loader。

module.exports = {
  module: {
    rules: [
      // 自动选择
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset'
      },
      // 导出为单独文件
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource'
      },
      // 导出为 Data URI
      {
        test: /\.svg$/,
        type: 'asset/inline'
      },
      // 导出为源代码
      {
        test: /\.txt$/,
        type: 'asset/source'
      }
    ]
  }
};
模块联邦(Module Federation)
// 主机应用配置
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js'
      },
      shared: ['react', 'react-dom']
    })
  ]
};

// 远程应用配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button'
      },
      shared: ['react', 'react-dom']
    })
  ]
};
其他新特性
特性说明
更好的 Tree Shaking嵌套的 Tree Shaking 支持
更好的模块反馈改进的模块大小反馈
Node.js polyfills 移除不再自动填充 Node.js 模块
优化持久缓存缓存算法改进
自动公共路径publicPath: 'auto'

十一、构建工具对比

28. Webpack 与 Vite 对比

定义对比
维度WebpackVite
打包方式Bundler(打包所有代码)ESM + unbundled(按需编译)
开发服务器启动慢,需要打包全部代码启动快,按需编译
HMR随着项目增大而变慢始终保持快速
构建输出生产环境打包使用 Rollup 构建
生态成熟度非常成熟快速发展中
配置复杂度配置较多约定优于配置
原理对比

Webpack

1. 启动时打包整个应用
2. 将所有模块打包成 Bundle
3. 启动开发服务器
4. 文件变化时重新编译相关模块

Vite

1. 利用浏览器原生 ES Modules
2. 按需编译和加载模块
3. 启动开发服务器(秒级)
4. 文件变化时仅重新编译修改的文件
选择策略
场景推荐
大型项目,需要精细控制Webpack
新项目,追求开发体验Vite
需要复杂代码分割Webpack
Vue/React 新项目Vite
需要稳定的生态Webpack
快速原型开发Vite

29. Webpack 与 Rollup 对比

对比分析
维度WebpackRollup
定位应用程序打包工具类库打包工具
代码分割强大的代码分割基础代码分割
HMR原生支持不支持
Tree Shaking支持非常优秀
生态丰富相对简单
配置复杂简单
选择策略
场景推荐
Web 应用Webpack
JavaScript 类库Rollup
需要复杂插件生态Webpack
追求最小打包体积Rollup
需要 HMRWebpack
纯 ES Modules 项目Rollup

30. Webpack 与 Parcel 对比

对比分析
维度WebpackParcel
配置需要配置文件零配置
学习曲线较陡平缓
性能可优化到极致开箱即用
灵活性
生态非常成熟相对较小
HMR支持支持
代码分割灵活配置自动处理
选择策略
场景推荐
需要精细控制Webpack
快速上手Parcel
企业级项目Webpack
小型项目/原型Parcel

十二、其他特性

31. Webpack Source Map

定义

Source Map 是一个映射文件,它将压缩/编译后的代码映射回源代码,方便调试。

devtool 配置选项
选项构建速度重新构建速度生产环境说明
eval++++++no每个模块用 eval() 执行
cheap-eval-source-map++++no廉价映射,不包含列信息
cheap-module-eval-source-map+++no包含 Loader 的 Source Map
eval-source-map--+no高质量映射
source-map-----yes完整 Source Map
hidden-source-map-----yes生成但不引用
nosources-source-map-----yes无源代码内容
推荐配置
// 开发环境
module.exports = {
  devtool: 'eval-cheap-module-source-map'
};

// 生产环境
module.exports = {
  devtool: 'source-map'  // 或 false(不生成)
};

32. Proxy 代理配置

定义

在开发环境中,由于同源策略,前端直接请求后端 API 会遇到跨域问题。Webpack Dev Server 提供了 Proxy 功能,将 API 请求代理到后端服务器。

基础配置
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        pathRewrite: { '^/api': '' },
        changeOrigin: true,
        secure: false
      }
    }
  }
};
多代理配置
module.exports = {
  devServer: {
    proxy: [
      {
        context: ['/auth', '/api'],
        target: 'http://localhost:3001'
      },
      {
        context: '/cdn',
        target: 'http://cdn.example.com'
      }
    ]
  }
};
代理配置选项
选项说明
target代理目标服务器地址
pathRewrite重写请求路径
changeOrigin修改请求头中的 host 为目标 URL
secure是否验证 SSL 证书
bypass绕过代理的函数
headers添加请求头

33. historyApiFallback

定义

在使用 HTML5 History API(pushState/replaceState)的 SPA 应用中,刷新时可能会返回 404 错误。historyApiFallback 会将所有请求重定向到 index.html。

配置
module.exports = {
  devServer: {
    historyApiFallback: {
      rewrites: [
        { from: /^\/$/, to: '/views/landing.html' },
        { from: /^\/subpage/, to: '/views/subpage.html' },
        { from: /./, to: '/views/404.html' }
      ]
    }
  }
};

34. CDN 加速

配置方式
module.exports = {
  output: {
    publicPath: 'https://cdn.example.com/assets/',
    filename: '[name].[contenthash].js'
  }
};
externals 排除第三方依赖
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM'
  }
};
<!-- index.html -->
<script src="https://cdn.example.com/react.production.min.js"></script>
<script src="https://cdn.example.com/react-dom.production.min.js"></script>

十三、常见问题解答

35. Webpack 常见问题

Q1: Loader 执行顺序为什么是从右到左?

Webpack 使用 compose 函数组合 Loader,遵循函数组合的数学规律,从右到左执行。

Q2: 如何处理 CSS 中的图片路径?
{
  test: /\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        url: true  // 处理 url()
      }
    }
  ]
}
Q3: 如何提取公共代码?
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        common: {
          chunks: 'initial',
          minChunks: 2,
          name: 'common'
        }
      }
    }
  }
};
Q4: 如何优化大型项目构建速度?
  1. 使用 Webpack 5 文件系统缓存
  2. 使用 thread-loader 多线程
  3. 缩小 Loader 处理范围(include/exclude)
  4. 使用 DllPlugin 预编译第三方库
  5. 升级硬件(SSD、大内存)