深入学习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;
相关推荐
代码猎人1 分钟前
前端怎么精通Vue2核心原理
前端
测试运维日常笔记1 分钟前
SQL 快速学习笔记:1~2 小时掌握面试重点
笔记·sql·学习
夏天要喝冰可乐2 分钟前
我用 WorkBuddy 做了个「GitHub 考古所」:输入一个用户名,出土你的编程人格档案
前端·github
乘风gg6 分钟前
AI coding 时代最值钱的东西,软件工程(下)
前端·ai编程·claude
用户114818678948417 分钟前
虚拟滚动与无限滚动的完整心法
前端
核验笔记的老王17 分钟前
前端表单防呆实践:如何排查自动填充导致的提交失败?
前端
用户4061220811117 分钟前
Vue vs React 核心差异全对比
前端
李纲明21 分钟前
微信小程序 web-view 打开空白或提示未配置业务域名:校验文件、HTTPS 与跳转域名排查
前端·微信小程序·https·web-view·小程序开发·业务域名
fanged28 分钟前
Audio学习4(DSP)(TODO)
学习
涛涛ing28 分钟前
一个框架为了“不水合”,把HTML注释都删了
前端