深入学习webpack-tapable

1. Tapable是什么?

Tapable是Webpack的插件系统核心,提供了强大的事件发布-订阅机制。它类似于Node.js的EventEmitter,但功能更强大,支持:

  • 多种执行模式(同步/异步、串行/并行)

  • 返回值传递(瀑布流)

  • 执行控制(可中断、可循环)

2. 9种钩子类型详解

2.1 SyncHook - 同步钩子

所有回调依次执行,互不影响。

javascript 复制代码
const { SyncHook } = require('tapable');

const hook = new SyncHook(['name', 'age']);

// 注册回调
hook.tap('plugin1', (name, age) => {
  console.log(`plugin1: ${name}, ${age}`);
});

hook.tap('plugin2', (name, age) => {
  console.log(`plugin2: ${name}, ${age}`);
});

// 触发
hook.call('Alice', 25);
// 输出:
// plugin1: Alice, 25
// plugin2: Alice, 25
2.2 SyncBailHook - 保险钩子

任何回调返回非undefined,则停止后续执行

javascript 复制代码
const { SyncBailHook } = require('tapable');

const hook = new SyncBailHook(['num']);

hook.tap('plugin1', (num) => {
  console.log(`plugin1: ${num}`);
  return undefined; // 继续执行
});

hook.tap('plugin2', (num) => {
  console.log(`plugin2: ${num}`);
  return 'stop'; // 停止后续
});

hook.tap('plugin3', (num) => {
  console.log(`plugin3: ${num}`); // 不会执行
});

hook.call(10);
// 输出: plugin1: 10, plugin2: 10
2.3 SyncWaterfallHook - 瀑布钩子

上一个回调的返回值会传给下一个回调。

javascript 复制代码
const { SyncWaterfallHook } = require('tapable');

const hook = new SyncWaterfallHook(['value']);

hook.tap('plugin1', (value) => {
  return value + 1;
});

hook.tap('plugin2', (value) => {
  return value * 2;
});

hook.tap('plugin3', (value) => {
  return value - 3;
});

const result = hook.call(5);
console.log(result); // 9
// 流程: 5 -> +1=6 -> *2=12 -> -3=9
2.4 SyncLoopHook - 循环钩子

如果回调返回true,则从头重新执行所有回调。

javascript 复制代码
const { SyncLoopHook } = require('tapable');

const hook = new SyncLoopHook(['name']);
let count = 0;

hook.tap('plugin1', (name) => {
  console.log(`plugin1: ${name}, count: ${count}`);
  count++;
  return count < 3; // 前3次返回true,重新循环
});

hook.tap('plugin2', (name) => {
  console.log(`plugin2: ${name}, count: ${count}`);
  return false; // 不再循环
});

hook.call('Loop');
2.5 AsyncParallelHook - 异步并行钩子

多个异步任务同时执行。

javascript 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name']);

// 方式1: 回调方式
hook.tapAsync('plugin1', (name, callback) => {
  setTimeout(() => {
    console.log(`plugin1: ${name}`);
    callback();
  }, 1000);
});

// 方式2: Promise方式
hook.tapPromise('plugin2', (name) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`plugin2: ${name}`);
      resolve();
    }, 500);
  });
});

// 触发
hook.callAsync('Parallel', () => {
  console.log('所有任务完成');
});
2.6 AsyncSeriesHook - 异步串行钩子

多个异步任务依次执行。

javascript 复制代码
const { AsyncSeriesHook } = require('tapable');

const hook = new AsyncSeriesHook(['name']);

hook.tapAsync('plugin1', (name, callback) => {
  setTimeout(() => {
    console.log(`plugin1: ${name}`);
    callback();
  }, 1000);
});

hook.tapAsync('plugin2', (name, callback) => {
  setTimeout(() => {
    console.log(`plugin2: ${name}`);
    callback();
  }, 500);
});

hook.callAsync('Series', () => {
  console.log('所有任务完成');
});
// 输出顺序: plugin1 (1秒后), plugin2 (再0.5秒后), 完成

3. 钩子使用对比表

钩子类型 执行方式 返回值处理 适用场景
SyncHook 同步顺序 忽略返回值 日志、统计、通知
SyncBailHook 同步顺序,遇非undefined停止 返回第一个非undefined 权限验证、条件检查
SyncWaterfallHook 同步顺序,传递返回值 上一个返回值作为下一个参数 代码转换、数据处理
SyncLoopHook 循环执行 返回true则重新循环 重试机制、验证
AsyncSeriesHook 异步串行 等待每个完成 文件操作、网络请求
AsyncParallelHook 异步并行 等待所有完成 并行任务、资源加载
AsyncSeriesBailHook 异步串行,遇错停止 错误时中断 关键任务链
AsyncSeriesWaterfallHook 异步串行,传递结果 结果传递 流水线处理

5. 自定义Tapable实现(理解原理)

javascript 复制代码
// 实现SyncHook
class MySyncHook {
  constructor(args = []) {
    this.args = args;
    this.taps = [];
  }
  
  tap(name, fn) {
    this.taps.push({ name, fn });
  }
  
  call(...args) {
    const params = args.slice(0, this.args.length);
    for (const tap of this.taps) {
      tap.fn(...params);
    }
  }
}

// 实现SyncBailHook
class MySyncBailHook {
  constructor(args = []) {
    this.args = args;
    this.taps = [];
  }
  
  tap(name, fn) {
    this.taps.push({ name, fn });
  }
  
  call(...args) {
    const params = args.slice(0, this.args.length);
    for (const tap of this.taps) {
      const result = tap.fn(...params);
      if (result !== undefined) return result;
    }
  }
}

// 实现SyncWaterfallHook
class MySyncWaterfallHook {
  constructor(args = []) {
    this.args = args;
    this.taps = [];
  }
  
  tap(name, fn) {
    this.taps.push({ name, fn });
  }
  
  call(...args) {
    const params = args.slice(0, this.args.length);
    let result = params[0];
    for (const tap of this.taps) {
      result = tap.fn(result, ...params.slice(1));
    }
    return result;
  }
}

从0构建Webpack

Compiler核心实现

javascript 复制代码
const { 
  SyncHook, SyncBailHook, SyncWaterfallHook,
  AsyncSeriesHook, AsyncParallelHook 
} = require('tapable');
const fs = require('fs');
const path = require('path');

class Compiler {
  constructor(config) {
    this.config = config;
    this.entry = config.entry;
    this.output = config.output;
    this.modules = new Map();
    
    // 定义生命周期钩子
    this.hooks = {
      // 初始化
      init: new SyncHook(['compiler']),
      
      // 编译流程
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      beforeCompile: new AsyncSeriesHook(['compiler']),
      compile: new SyncHook(['compiler']),
      
      // 模块处理
      beforeResolve: new SyncBailHook(['request']),
      resolve: new SyncHook(['request']),
      afterResolve: new SyncHook(['request']),
      beforeBuildModule: new SyncHook(['module']),
      buildModule: new AsyncSeriesHook(['module']),
      afterBuildModule: new SyncHook(['module']),
      
      // 代码转换
      load: new SyncWaterfallHook(['source', 'filePath']),
      transform: new SyncWaterfallHook(['code', 'filePath']),
      
      // 依赖解析
      parseDependencies: new SyncHook(['module']),
      
      // 输出生成
      beforeEmit: new AsyncSeriesHook(['compilation']),
      emit: new AsyncSeriesHook(['compilation']),
      afterEmit: new AsyncSeriesHook(['compilation']),
      
      // 完成
      done: new SyncHook(['stats']),
      failed: new SyncHook(['error'])
    };
    
    // 应用插件
    if (config.plugins) {
      config.plugins.forEach(plugin => {
        if (plugin.apply) {
          plugin.apply(this);
        }
      });
    }
    
    this.hooks.init.call(this);
  }
  
  async run() {
    try {
      await this.hooks.beforeRun.promise(this);
      await this.hooks.run.promise(this);
      
      const compilation = await this.compile();
      await this.emit(compilation);
      
      this.hooks.done.call({
        startTime: Date.now(),
        modules: this.modules.size
      });
      
      return compilation;
    } catch (error) {
      this.hooks.failed.call(error);
      throw error;
    }
  }
  
  async compile() {
    await this.hooks.beforeCompile.promise(this);
    this.hooks.compile.call(this);
    
    const compilation = {
      modules: [],
      assets: {},
      entry: this.entry
    };
    
    // 构建入口模块
    const entryModule = await this.buildModule(this.entry, process.cwd());
    compilation.modules.push(entryModule);
    
    // 递归构建依赖
    await this.buildDependencies(entryModule, compilation);
    
    return compilation;
  }
  
  async buildModule(filePath, context) {
    const absolutePath = path.resolve(context, filePath);
    
    if (this.modules.has(absolutePath)) {
      return this.modules.get(absolutePath);
    }
    
    this.hooks.beforeBuildModule.call({ filePath: absolutePath });
    
    // 读取文件
    let source = fs.readFileSync(absolutePath, 'utf-8');
    
    // 应用loader(瀑布流)
    source = this.hooks.load.call(source, absolutePath);
    let code = this.hooks.transform.call(source, absolutePath);
    
    // 解析依赖
    const dependencies = this.parseDependencies(code);
    
    // 转换ES6代码
    code = this.transformCode(code);
    
    const module = {
      filePath: absolutePath,
      code,
      dependencies,
      rawSource: source
    };
    
    this.hooks.afterBuildModule.call(module);
    this.modules.set(absolutePath, module);
    
    return module;
  }
  
  async buildDependencies(module, compilation) {
    for (const dep of module.dependencies) {
      // 保险钩子:可跳过依赖
      const skip = this.hooks.beforeResolve.call({
        request: dep,
        parent: module.filePath
      });
      
      if (skip === false) continue;
      
      this.hooks.resolve.call({ request: dep, parent: module.filePath });
      
      const depPath = this.resolvePath(dep, path.dirname(module.filePath));
      
      this.hooks.afterResolve.call({ request: dep, resolved: depPath });
      
      const depModule = await this.buildModule(depPath, path.dirname(module.filePath));
      
      if (!compilation.modules.find(m => m.filePath === depModule.filePath)) {
        compilation.modules.push(depModule);
        await this.buildDependencies(depModule, compilation);
      }
    }
  }
  
  parseDependencies(code) {
    const dependencies = [];
    const importRegex = /import\s+.*?\s+from\s+['"](.*?)['"]/g;
    const requireRegex = /require\(['"](.*?)['"]\)/g;
    
    let match;
    while ((match = importRegex.exec(code)) !== null) {
      dependencies.push(match[1]);
    }
    while ((match = requireRegex.exec(code)) !== null) {
      dependencies.push(match[1]);
    }
    
    this.hooks.parseDependencies.call({ code, dependencies });
    
    return dependencies;
  }
  
  transformCode(code) {
    // import -> require
    code = code.replace(/import\s+(\w+)\s+from\s+['"](.*?)['"]/g, (match, name, path) => {
      return `const ${name} = require('${path}');`;
    });
    
    // export default -> module.exports
    code = code.replace(/export\s+default\s+(\w+)/g, (match, name) => {
      return `module.exports = ${name};`;
    });
    
    // export const/function -> exports.xxx
    code = code.replace(/export\s+(const|function|let|var)\s+(\w+)/g, (match, type, name) => {
      return `${type} ${name}; exports.${name} = ${name};`;
    });
    
    return code;
  }
  
  resolvePath(request, baseDir) {
    if (request.startsWith('.') || request.startsWith('/')) {
      const extensions = ['.js', '.json', ''];
      for (const ext of extensions) {
        const fullPath = path.resolve(baseDir, request + ext);
        if (fs.existsSync(fullPath)) {
          return fullPath;
        }
      }
      return path.resolve(baseDir, request);
    }
    return request;
  }
  
  async emit(compilation) {
    await this.hooks.beforeEmit.promise(compilation);
    
    const chunks = this.createChunks(compilation);
    
    for (const chunk of chunks) {
      const code = this.generateCode(chunk);
      const filename = this.output.filename || 'bundle.js';
      const outputPath = path.resolve(this.output.path, filename);
      
      compilation.assets[filename] = code;
      await this.hooks.emit.promise(compilation);
      
      fs.writeFileSync(outputPath, code);
      console.log(`✅ 生成文件: ${outputPath}`);
    }
    
    await this.hooks.afterEmit.promise(compilation);
  }
  
  createChunks(compilation) {
    return [{
      name: 'main',
      modules: compilation.modules,
      entry: compilation.entry
    }];
  }
  
  generateCode(chunk) {
    const modulesMap = {};
    chunk.modules.forEach(module => {
      modulesMap[module.filePath] = module.code;
    });
    
    return `
      (function(modules) {
        const installedModules = {};
        
        function __webpack_require__(moduleId) {
          if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
          }
          
          const module = installedModules[moduleId] = {
            exports: {}
          };
          
          modules[moduleId](module, module.exports, __webpack_require__);
          
          return module.exports;
        }
        
        return __webpack_require__('${chunk.entry}');
      })({
        ${Object.entries(modulesMap).map(([id, code]) => `
          "${id}": function(module, exports, require) {
            ${code}
          }
        `).join(',\n')}
      });
    `;
  }
}

module.exports = Compiler;
相关推荐
qingwufeiyang_5304 小时前
Mybatis-plus学习笔记1
笔记·学习·mybatis
bingd014 小时前
慕课网、CSDN、菜鸟教程…2026 国内编程学习平台实测对比
java·开发语言·人工智能·python·学习
恋猫de小郭4 小时前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
freewlt4 小时前
从 0 搭建现代前端组件库:2026年完整实战指南
前端
凌冰_5 小时前
Thymeleaf 核心语法详解
java·前端·javascript
AIBox3655 小时前
claude 镜像 api 使用指南(2026 年4 月更新)
java·服务器·前端·人工智能·gpt·前端框架
Sophie_U5 小时前
【Agent开发速成笔记】一、从0到1基础Python学习
笔记·python·学习·agent·智能体
SuperEugene5 小时前
Vue3 配置文件管理:按模块拆分配置,提升配置可维护性|配置驱动开发实战篇
前端·javascript·vue.js·驱动开发
阿凤215 小时前
后端返回文件二进制流
开发语言·前端·javascript·uniapp
落魄江湖行5 小时前
进阶篇四 Nuxt4 Server Routes:写后端 API
前端·vue.js·typescript·nuxt4