深入学习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;
相关推荐
半路_出家ren2 小时前
Nginx基础学习
运维·网络·网络协议·学习·nginx·网络安全
工边页字2 小时前
AI产品面试题:什么是 Function Calling?
前端·人工智能·后端
Mintopia2 小时前
一份合格的软件 VI 文字文档简单版
前端·css·人工智能
星幻元宇VR2 小时前
VR生产安全学习机|将安全教育带入沉浸式实训新时代
科技·学习·安全·vr·虚拟现实
四千岁2 小时前
如何精准统计 Token 消耗,使用对账工具控制成本?
前端·javascript·vue.js
开心码农1号2 小时前
前端web页面请求后端服务时,接口出现50s初始连接超时
前端
啥咕啦呛2 小时前
java打卡学习2:Stream高级与Optional
java·windows·学习
试试勇气2 小时前
Linux学习笔记(十九)--生产消费模型与线程安全
java·笔记·学习
jiayong232 小时前
0基础学习VUE3 第 3 课:任务页怎么把列表、筛选、表单、弹窗串起来
前端·javascript·学习