深入学习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;
相关推荐
PBitW4 小时前
为什么vite中TS报错,可以继续运行?Webpack不行?💡
前端·typescript
GISer_Jing4 小时前
一套H5跑通三端:App+小程序内嵌H5跨端适配全栈解决方案
前端·前端框架·ai编程
码云之上4 小时前
项目团队从 5 人扩到 15 人,我写了个 CLI 让 IDE 共享 AI 规则
前端·人工智能·后端
飞天狗4 小时前
LCP / INP / CLS 三个维度的实战调优:把 Web Vitals 从不及格拉到优秀的完整路径
前端·性能优化
网络与设备以及操作系统学习使用者4 小时前
通信的物理与逻辑双维度解析
学习·深度优先
Csvn4 小时前
Vue 3 `<script setup>` 解构 props 后响应式丢了?一个「字段不更新」的排查实录
前端
用户298698530144 小时前
前端 Excel 处理进阶:React 中合并与取消合并单元格的实现
前端·javascript·react.js
Highcharts4 小时前
@highcharts/react完整技术指南|新版Highcharts React使用、迁移、SSR、TS 优化全解
前端·javascript
浩瀚地学5 小时前
【Java基础复习】IO流(二)
java·开发语言·经验分享·笔记·学习