Webpack 核心原理与自定义 Loader/Plugin 实战

一、Webpack 打包流程详解

Webpack 本质上是一个静态模块打包工具 ,它的核心流程可以概括为三个阶段:初始化 → 编译 → 输出

1.1 整体流程概览

1.2 详细阶段拆解

阶段一:初始化

  • 读取配置文件 webpack.config.js
  • 创建 Compiler 对象,它是整个编译过程的"指挥官"
  • 注册所有内置插件和用户配置的插件
  • 调用 compiler.run() 启动编译

阶段二:编译构建

  1. 确定入口 :从 entry 配置开始,读取入口文件内容
  2. 模块解析
    1. 将文件内容解析为 AST(抽象语法树)
    2. 找出文件中所有的 import / require 语句
    3. 递归解析所有依赖,构建依赖关系图
  3. Loader 处理:在解析过程中,匹配到的 Loader 会对模块源码进行转换
  4. 生成 Chunk:根据依赖关系图,将模块分组为不同的 Chunk

阶段三:输出文件

  • 根据 output 配置,将每个 Chunk 渲染成一个独立的 bundle 文件
  • 执行 Plugin 的 emit 钩子,允许修改最终输出的资源
  • 写入磁盘

1.3 核心数据结构

概念 说明 示例
Module 一个文件就是一个 Module .js, .css, .png
Chunk 代码块,由多个 Module 组成 入口 chunk、异步 chunk
Bundle 最终输出的文件 main.js, vendors.js

二、Loader 深入理解

2.1 Loader 是什么?

Loader 是一个导出为函数的 Node 模块,用于对模块源码进行转换。因为 Webpack 原生只认识 JavaScript 和 JSON,Loader 负责将其他类型的文件(CSS、图片、TypeScript 等)转换为 Webpack 能够处理的模块。

2.2 Loader 的执行顺序

java 复制代码
javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.less$/,
        use: [
          'style-loader',  // ③ 最后执行
          'css-loader',    // ② 中间执行
          'less-loader'    // ① 最先执行
        ]
      }
    ]
  }
}

执行顺序规律 :从右到左,从下到上(类似于函数组合 compose

2.3 Loader 的分类

类型 特点 示例
同步 Loader 直接返回转换结果 babel-loader
异步 Loader 通过 this.async() 回调 file-loader
Raw Loader 接收 Buffer 而非字符串 处理图片、字体
Pitch Loader 在正常执行之前运行 style-loader 的 pitch

三、Plugin 深入理解

3.1 Plugin 是什么?

Plugin 是一个具有 apply 方法的 JavaScript 类 。它通过监听 Webpack 生命周期中的钩子(Hook) ,在特定时机介入编译过程,从而实现自动化操作。

3.2 Plugin 与 Loader 的区别

维度 Loader Plugin
作用 转换模块内容 扩展功能、干预流程
运行时机 模块解析阶段 整个编译生命周期
本质 函数 类(含 apply 方法)
能力 有限,只能处理文件内容 强大,可访问 Compiler 和 Compilation

四、自定义 Loader 实战

4.1 自定义同步 Loader:移除 console.log

js 复制代码
// loaders/strip-console-loader.js
/**
 * 功能:移除代码中的所有 console.log 语句
 * 用法:{
 *   test: /.js$/,
 *   use: './loaders/strip-console-loader.js'
 * }
 */
module.exports = function(source) {
  // source: 源文件内容(字符串)
  // this: Webpack 提供的 Loader API
  
  // 使用正则替换所有 console.log
  const result = source.replace(/console.log([^)]*);?/g, '');
  
  // 同步 Loader 必须返回处理后的内容
  return result;
};

4.2 自定义异步 Loader:添加版权注释

js 复制代码
// loaders/copyright-loader.js
const path = require('path');


module.exports = function(source) {
  // 标记为异步 Loader
  const callback = this.async();
  
  // 模拟异步操作(比如读取版权文件)
  setTimeout(() => {
    const copyright = `
/**
 * Copyright © ${new Date().getFullYear()} 技术分享会
 * Author: Webpack 实战
 */
`;
    // 在文件头部插入版权注释
    const result = copyright + source;
    
    // 异步 Loader 必须调用 callback
    callback(null, result);
  }, 100);
};

4.3 使用自定义 Loader

js 复制代码
// webpack.config.js
const path = require('path');


module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: [
          './loaders/copyright-loader.js',
          './loaders/strip-console-loader.js'
        ]
      }
    ]
  }
};

五、自定义 Plugin 实战

5.1.1 自定义 Plugin:生成构建报告

js 复制代码
// plugins/build-report-plugin.js
class BuildReportPlugin {
  constructor(options) {
    // 接收用户配置
    this.options = options || {};
    this.filename = options.filename || 'build-report.json';
  }


  // apply 方法是 Plugin 的入口,接收 compiler 参数
  apply(compiler) {
    // 监听 compilation 钩子,获取编译对象
    compiler.hooks.compilation.tap('BuildReportPlugin', (compilation) => {
      
      // 监听 processAssets 钩子,在优化 chunk 资源时执行
      compilation.hooks.processAssets.tap(
        {
          name: 'BuildReportPlugin',
          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
        },
        (assets) => {
          // 收集构建信息
          const report = {
            timestamp: new Date().toISOString(),
            totalModules: compilation.modules.size,
            chunks: [],
            assets: []
          };


          // 遍历所有 chunk
          for (const chunk of compilation.chunks) {
            report.chunks.push({
              id: chunk.id,
              name: chunk.name,
              files: Array.from(chunk.files),
              size: chunk.size()
            });
          }


          // 遍历所有资源
          for (const [filename, source] of Object.entries(assets)) {
            report.assets.push({
              name: filename,
              size: source.size()
            });
          }


          // 将报告作为额外资源添加到输出
          const reportContent = JSON.stringify(report, null, 2);
          compilation.emitAsset(
            this.filename,
            new compiler.webpack.sources.RawSource(reportContent)
          );
        }
      );
    });


    // 监听 done 钩子,在构建完成后打印信息
    compiler.hooks.done.tap('BuildReportPlugin', (stats) => {
      console.log(`\n📊 构建报告已生成:${this.filename}`);
      console.log(`⏱  耗时:${stats.endTime - stats.startTime}ms`);
      console.log(`📦 输出文件数:${Object.keys(stats.compilation.assets).length}\n`);
    });
  }
}


module.exports = BuildReportPlugin;

5.1.2 自定义 Plugin:自动上传到 CDN

js 复制代码
// plugins/upload-to-cdn-plugin.js
const path = require('path');
const fs = require('fs');


class UploadToCDNPlugin {
  constructor(options) {
    this.options = options || {};
    this.cdnUrl = options.cdnUrl || 'https://cdn.example.com';
    this.bucket = options.bucket || 'default';
  }


  apply(compiler) {
    // 监听 afterEmit 钩子,在文件写入磁盘后执行
    compiler.hooks.afterEmit.tapAsync('UploadToCDNPlugin', (compilation, callback) => {
      const outputPath = compilation.outputOptions.path;
      
      // 遍历所有输出的资源
      const assets = compilation.getAssets();
      const uploadPromises = assets.map(async (asset) => {
        const filePath = path.join(outputPath, asset.name);
        
        // 模拟上传到 CDN
        console.log(`☁️  正在上传 ${asset.name} 到 CDN...`);
        
        // 这里可以接入真实的 CDN SDK,比如阿里云 OSS、AWS S3
        // await s3.putObject({ Bucket: this.bucket, Key: asset.name, Body: fs.createReadStream(filePath) });
        
        // 模拟上传延迟
        await new Promise(resolve => setTimeout(resolve, 200));
        
        console.log(`✅ ${asset.name} 上传完成`);
        console.log(`🔗 CDN URL: ${this.cdnUrl}/${asset.name}`);
      });


      Promise.all(uploadPromises).then(() => {
        console.log('\n🎉 所有资源已上传到 CDN!');
        callback();
      }).catch((err) => {
        console.error('❌ 上传失败:', err);
        callback(err);
      });
    });
  }
}


module.exports = UploadToCDNPlugin;

5.2 使用自定义 Plugin

js 复制代码
// webpack.config.js
const BuildReportPlugin = require('./plugins/build-report-plugin');
const UploadToCDNPlugin = require('./plugins/upload-to-cdn-plugin');


module.exports = {
  // ... 其他配置
  plugins: [
    new BuildReportPlugin({
      filename: 'report.json'
    }),
    new UploadToCDNPlugin({
      cdnUrl: 'https://my-cdn.example.com',
      bucket: 'production'
    })
  ]
};

六、Loader vs Plugin 对比总结

对比维度 Loader Plugin
本质 函数(Function) 类(Class)
核心方法 module.exports = function(source) {} class X { apply(compiler) {} }
作用对象 单个文件(Module) 整个构建过程(Compilation)
触发时机 模块解析和转换时 构建生命周期的各个阶段
能力范围 文件内容转换 资源管理、环境变量、优化、输出等
配置方式 module.rules数组 plugins数组
API 来源 this上下文(Loader Context) compiler和 compilation对象

七、推荐阅读

相关推荐
37.2℃99515 分钟前
专业的Claude Design解决方案
前端·算法
Jolyne_19 分钟前
ng-zorro-table列宽resize后宽度是NaN的源码调试记录
前端
liwulin050624 分钟前
【ollama】自定义结构化输出
linux·前端·数据库·ollama
前端甜糖36 分钟前
记录We码开发者工具的一个bug
前端·javascript
程序员黑豆1 小时前
鸿蒙应用开发教程:以红绿灯切换为例,掌握条件渲染的核心用法
前端·harmonyos
猫3281 小时前
echarts 日常问题解决
前端·javascript·echarts
新中地GIS开发老师1 小时前
地信职业百科④:GIS开发工程师
前端·数据库·gis·webgis·三维gis开发
我是大卫1 小时前
【图】React源码解析-从数据结构、调度器、源码设计模式到状态计算引擎,深挖useReducer原理
前端·react.js·源码
willes1 小时前
列表中的倒计时组件
前端
LVZ1 小时前
用了半年 AI 写代码,最烦的不是代码,是环境
前端·后端·开源