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对象

七、推荐阅读

相关推荐
小林ixn2 小时前
从拼多多手机号验证到模板引擎:深入正则表达式与 JS 字符串处理
开发语言·javascript·正则表达式
智码看视界2 小时前
Web Storage 的无障碍实践与工程化应用
前端·javascript·web
孟陬2 小时前
国外技术周刊 #140:在 Jeff Bezos 的私密 Campfire 峰会上,我学到了关于亿万富翁的事
前端·后端
槑有老呆2 小时前
Bun:一个让 Node 开发者原地起飞的 JS/TS 运行时
前端
小小小小宇2 小时前
AI Agent 核心流程与底层逻辑
前端
wuhen_n2 小时前
RAG 实战:语义检索 + 大模型生成精准问答
前端·langchain·ai编程
沉尘5882 小时前
ACE-GCM加解密微信小程序
前端
半个烧饼不加肉2 小时前
JS 底层探究-- 普通函数和构造函数
开发语言·javascript·原型模式