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

七、推荐阅读

相关推荐
web打印社区6 小时前
网页静默打印热敏小票:从 HTML 到出纸的完整指南
前端·javascript·vue.js·electron·pdf·html
剪刀石头布啊7 小时前
设计一个多计时任务的任务管理功能
前端
ModyQyW8 小时前
vite-plugin-uni-manifest 更新了什么
前端·typescript·uni-app
Asize9 小时前
AI 协作开发新范式:我用 SDD 做了个排版 npm 包
前端·人工智能
kyriewen10 小时前
我把今年流传的前端 AI 面试题整理了一遍——4 类场景题+回答框架(附速查表)
前端·面试·程序员
计算机魔术师11 小时前
国产多模态模型正面硬刚Opus旗舰:差距从30%缩到3%
前端
风骏时光牛马11 小时前
程序员进阶:深度思考,解锁职场成长的底层逻辑
前端
IT_陈寒11 小时前
用了Proxy才发现以前的JavaScript白写了
前端·人工智能·后端
爱丶不疚11 小时前
Eval: Agent 说的 Eval 是什么?从单测、TDD 到 Sentry 聊起
前端·ai编程·vibecoding
求道於盲11 小时前
python中的类型标注
前端