深入学习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;
相关推荐
石小石Orz12 分钟前
我发现了开发者AI产品营收的新方向
前端·虚拟现实
老马识途2.022 分钟前
关于跨域问题的总结
java·前端
MartinYeung52 小时前
[论文学习]自主性如何重塑个性化对LLM智能体隐私关切与信任的影响
人工智能·学习
别惊醒渔人2 小时前
Vue3 Diff 优化:最长递增子序列 LIS
前端·javascript·vue.js
颜酱2 小时前
07 | 把字段与指标同步到 Qdrant(生成阶段)
前端·人工智能·后端
用户059540174463 小时前
把AI长期记忆测试从手动验证换成pytest,2天揪出11个隐藏Bug
前端·css
FellAveal3 小时前
【Go语言入门学习笔记】Part18.工作池与WaitGroup(也即协程池)
笔记·学习·golang
菜鸟‍3 小时前
【论文学习】MICCAI 2024 || SGSeg:通过自引导机制实现胸部X光片语言引导分割的无文本推理
人工智能·深度学习·学习
lazy H3 小时前
Git clone 怎么用?克隆项目及常见问题完整教程
大数据·git·后端·学习·搜索引擎·github
菜鸟‍3 小时前
【论文学习】arxiv 2024 || RoSIS:基于鲁棒框架重新审视文本提示式手术器械分割
人工智能·学习·计算机视觉