Webpack 的插件(plugin)和 loader 的执行顺序有所不同。插件的执行顺序是按照它们在配置中的声明顺序执行的,而不是倒序执行。
插件的执行顺序
在 Webpack 配置文件中,插件是按照它们在 plugins 数组中的顺序依次执行的。也就是说,先声明的插件会先执行,后声明的插件会后执行
            
            
              javascript
              
              
            
          
          const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MyCustomPlugin = require('./my-custom-plugin');
module.exports = {
  plugins: [
    new CleanWebpackPlugin(), // 这个插件会首先执行
    new HtmlWebpackPlugin({   // 这个插件会第二个执行
      template: './src/index.html'
    }),
    new MyCustomPlugin()      // 这个插件会最后执行
  ]
};
        为什么插件按顺序执行?
插件的执行顺序是按照声明顺序执行的,因为插件通常会依赖于 Webpack 的生命周期钩子(hooks)。这些钩子在 Webpack 的编译过程中按特定顺序触发,因此插件需要按照声明顺序依次注册和执行,以确保它们在正确的时机进行相应的操作。
插件的生命周期钩子
Webpack 插件通过钩子机制与 Webpack 的编译过程进行交互。常见的钩子包括:
compile:编译开始时触发。compilation:每次新的编译创建时触发。emit:生成资源到输出目录之前触发。done:编译完成时触发。
插件可以在这些钩子上注册回调函数,以执行特定的任务。
自定义插件示例
以下是一个简单的自定义插件示例,展示如何在 Webpack 中创建和使用插件:
            
            
              javascript
              
              
            
          
          class MyCustomPlugin {
  apply(compiler) {
     // 使用 compiler.hooks.done.tap 注册一个钩子
    compiler.hooks.done.tap('MyCustomPlugin', (stats) => {
      console.log('编译完成!');
    });
  }
}
module.exports = MyCustomPlugin;
        在 Webpack 配置文件中使用这个插件:
            
            
              javascript
              
              
            
          
          const MyCustomPlugin = require('./my-custom-plugin');
module.exports = {
  plugins: [
    new MyCustomPlugin()
  ]
};
        总结
- Loader:按相反顺序执行,从后往前。
 - Plugin:按声明顺序执行,从前往后。
 
注意事项
- 
命名插件:
- 在注册钩子时,给插件一个唯一的名称,以便在调试和日志中更容易识别。
 
 - 
使用正确的钩子:
- Webpack 提供了许多钩子,覆盖了编译过程的各个阶段。选择合适的钩子来实现你的功能。例如,
emit钩子在生成资源到输出目录之前触发,done钩子在编译完成时触发。 
 - Webpack 提供了许多钩子,覆盖了编译过程的各个阶段。选择合适的钩子来实现你的功能。例如,
 - 
异步操作:
- 如果插件需要执行异步操作,可以使用异步钩子(如 
tapAsync或tapPromise)并确保在操作完成后调用回调函数或返回 Promise。 
 - 如果插件需要执行异步操作,可以使用异步钩子(如 
 - 
处理错误:
- 在插件中处理可能的错误,并在适当的时候调用 Webpack 的错误处理机制,以确保编译过程不会因为插件的错误而中断。
 
 - 
访问编译资源:
- 通过 
compilation对象可以访问和修改编译资源。确保你对资源的修改是安全和必要的。 
 - 通过 
 
示例:一个异步插件
以下是一个异步插件示例,它在生成资源到输出目录之前执行异步操作:
            
            
              javascript
              
              
            
          
          class AsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('异步操作完成');
        callback();
      }, 1000);
    });
  }
}
module.exports = AsyncPlugin;
        使用异步插件
在 Webpack 配置文件中使用这个异步插件:
            
            
              javascript
              
              
            
          
          const AsyncPlugin = require('./path/to/async-plugin');
module.exports = {
  plugins: [
    new AsyncPlugin()
  ]
};
        - 创建插件类 :插件通常是一个类,包含一个 
apply方法。 - 实现 
apply方法 :通过compiler对象访问 Webpack 的编译生命周期钩子。 - 注册钩子 :在 
apply方法中注册钩子,以便在编译过程的特定阶段执行自定义逻辑。 - 注意事项:命名插件、使用正确的钩子、处理异步操作、处理错误、访问编译资源。
 
编写一个包含所有主要钩子和知识点的 Webpack 插件示例
示例插件:ComprehensivePlugin
这个插件将在编译过程的不同阶段输出日志信息,并在生成资源之前添加一个自定义文件。
1. 创建插件类
首先,创建一个插件类 ComprehensivePlugin:
            
            
              javascript
              
              
            
          
          class ComprehensivePlugin {
  apply(compiler) {
    // 1. compile 钩子:编译开始时触发
    compiler.hooks.compile.tap('ComprehensivePlugin', (params) => {
      console.log('编译开始...');
    });
    // 2. compilation 钩子:每次新的编译创建时触发
    compiler.hooks.compilation.tap('ComprehensivePlugin', (compilation) => {
      console.log('新的编译创建...');
      // 3. optimize 钩子:优化阶段开始时触发
      compilation.hooks.optimize.tap('ComprehensivePlugin', () => {
        console.log('优化阶段开始...');
      });
      // 4. emit 钩子:生成资源到输出目录之前触发(异步)
      compilation.hooks.emit.tapAsync('ComprehensivePlugin', (compilation, callback) => {
        console.log('生成资源到输出目录之前...');
        
        // 添加一个自定义文件
        const content = '这是一个由 ComprehensivePlugin 生成的文件。';
        compilation.assets['custom-file.txt'] = {
          source: () => content,
          size: () => content.length
        };
        // 模拟异步操作
        setTimeout(() => {
          console.log('异步操作完成');
          callback();
        }, 1000);
      });
    });
    // 5. afterEmit 钩子:生成资源到输出目录之后触发(异步)
    compiler.hooks.afterEmit.tapAsync('ComprehensivePlugin', (compilation, callback) => {
      console.log('生成资源到输出目录之后...');
      callback();
    });
    // 6. done 钩子:编译完成时触发
    compiler.hooks.done.tap('ComprehensivePlugin', (stats) => {
      console.log('编译完成!');
    });
    // 7. failed 钩子:编译失败时触发
    compiler.hooks.failed.tap('ComprehensivePlugin', (error) => {
      console.error('编译失败:', error);
    });
  }
}
module.exports = ComprehensivePlugin;
        2. 使用插件
在 Webpack 配置文件中使用这个插件:
            
            
              javascript
              
              
            
          
          const path = require('path');
const ComprehensivePlugin = require('./path/to/ComprehensivePlugin');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new ComprehensivePlugin()
  ]
};
        3. 测试插件
创建一个简单的入口文件 src/index.js:
            
            
              javascript
              
              
            
          
          console.log('Hello, Webpack!');
        
            
            
              javascript
              
              
            
          
          npx webpack
        你应该会在控制台看到插件输出的日志信息,并在 dist 目录下看到一个名为 custom-file.txt 的文件,内容为 这是一个由 ComprehensivePlugin 生成的文件。
总结
这个示例插件展示了以下知识点:
- 使用不同的钩子:展示了如何在编译过程的不同阶段使用 Webpack 的钩子。
 - 处理异步操作 :使用 
tapAsync钩子处理异步操作,并在操作完成后调用回调函数。 - 错误处理:展示了如何在编译失败时处理错误。
 - 访问和修改编译资源 :展示了如何通过 
compilation对象访问和修改编译资源。 
Webpack 插件通过钩子机制与 Webpack 的编译过程进行交互。Webpack 使用 Tapable 库来实现这些钩子。以下是一些常见的 Webpack 插件钩子及其解释:
- Compiler 钩子 :管理整个编译过程,常见钩子包括 
compile、compilation、emit、afterEmit、done、failed。 - Compilation 钩子 :管理具体的编译过程,常见钩子包括 
optimize、optimizeAssets、processAssets、afterOptimizeAssets。 - 同步和异步钩子 :使用 
tap注册同步钩子,使用tapAsync或tapPromise注册异步钩子 
在 Webpack 中,compiler 和 compilation 是两个不同的概念,它们分别代表了不同的编译阶段和作用范围。理解它们的区别对于编写插件和扩展 Webpack 功能非常重要。
Compiler 和 Compilation 的区别
- 
Compiler:
- 作用范围 :
compiler对象代表了 Webpack 的整个编译器实例,管理整个编译过程。 - 生命周期 :
compiler的生命周期从 Webpack 启动到编译结束,贯穿整个编译过程。 - 钩子 :
compiler钩子用于管理整个编译过程的全局事件,例如编译开始、编译完成等。 
 - 作用范围 :
 - 
Compilation:
- 作用范围 :
compilation对象代表了一次具体的编译过程,包含了当前模块资源、编译生成的资源、变化的文件等信息。 - 生命周期 :每次新的编译创建时,都会生成一个新的 
compilation对象。对于初始编译和每次增量编译,都会创建新的compilation对象。 - 钩子 :
compilation钩子用于管理具体的编译过程,例如优化、生成资源等。 
 - 作用范围 :
 
compiler.hooks.compilation 和 compilation 钩子的区别
- 
**
compiler.hooks.compilation**:- 触发时机 :每次新的 
compilation对象创建时触发。 - 作用 :允许插件在新的编译过程开始时,注册 
compilation钩子,以便在具体的编译过程中执行自定义逻辑。 
 - 触发时机 :每次新的 
 
            
            
              javascript
              
              
            
          
          compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  console.log('新的编译创建...');
  // 在这里可以注册 compilation 钩子
  compilation.hooks.optimize.tap('MyPlugin', () => {
    console.log('优化阶段开始...');
  });
});
        compilation 钩子:
- 触发时机:在具体的编译过程中触发,例如优化、生成资源等阶段。
 - 作用:允许插件在具体的编译过程中执行自定义逻辑。
 
            
            
              javascript
              
              
            
          
          compilation.hooks.optimize.tap('MyPlugin', () => {
  console.log('优化阶段开始...');
});
        示例:结合使用 compiler 和 compilation 钩子
        
            
            
              javascript
              
              
            
          
          class MyPlugin {
  apply(compiler) {
    // 在新的 compilation 对象创建时触发
    compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
      console.log('新的编译创建...');
      // 在具体的编译过程中触发
      compilation.hooks.optimize.tap('MyPlugin', () => {
        console.log('优化阶段开始...');
      });
      compilation.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
        console.log('生成资源到输出目录之前...');
        callback();
      });
    });
    // 编译完成时触发
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('编译完成!');
    });
  }
}
module.exports = MyPlugin;
        总结
compiler对象 :代表 Webpack 的整个编译器实例,管理整个编译过程。compiler钩子用于管理全局编译事件。compilation对象 :代表一次具体的编译过程,包含当前模块资源、编译生成的资源等信息。compilation钩子用于管理具体的编译过程。- **
compiler.hooks.compilation**:在新的compilation对象创建时触发,允许插件注册compilation钩子。 compilation钩子:在具体的编译过程中触发,允许插件在编译过程的不同阶段执行自定义逻辑
- 时机:编译开始时触发。
 
            
            
              javascript
              
              
            
          
          compiler.hooks.compile.tap('MyPlugin', (params) => {
  console.log('编译开始...');
});
        - 时机:每次新的编译创建时触发。
 
            
            
              javascript
              
              
            
          
          compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  console.log('新的编译创建...');
});
        - 时机:生成资源到输出目录之前触发(异步)。
 
            
            
              javascript
              
              
            
          
          compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
  console.log('生成资源到输出目录之前...');
  callback();
});
        - 时机:生成资源到输出目录之后触发(异步)。
 
            
            
              javascript
              
              
            
          
          compiler.hooks.afterEmit.tapAsync('MyPlugin', (compilation, callback) => {
  console.log('生成资源到输出目录之后...');
  callback();
});
        - 时机:编译完成时触发。
 
            
            
              javascript
              
              
            
          
          compiler.hooks.done.tap('MyPlugin', (stats) => {
  console.log('编译完成!');
});
        时机:编译失败时触发
            
            
              javascript
              
              
            
          
          compiler.hooks.failed.tap('MyPlugin', (error) => {
  console.error('编译失败:', error);
});
        Compilation 钩子
compilation 对象代表了一次具体的编译过程,包含了当前模块资源、编译生成的资源、变化的文件等信息。以下是一些常见的 compilation 钩子:
- 时机:优化阶段开始时触发。
 
            
            
              javascript
              
              
            
          
          compilation.hooks.optimize.tap('MyPlugin', () => {
  console.log('优化阶段开始...');
});
        - 时机:优化生成的资源时触发(异步)。
 
            
            
              javascript
              
              
            
          
          compilation.hooks.optimizeAssets.tapAsync('MyPlugin', (assets, callback) => {
  console.log('优化生成的资源...');
  callback();
});
        - 时机:处理生成的资源时触发(异步)。
 
            
            
              javascript
              
              
            
          
          compilation.hooks.processAssets.tapAsync('MyPlugin', (assets, callback) => {
  console.log('处理生成的资源...');
  callback();
});
        - 时机:优化生成的资源之后触发。
 
            
            
              javascript
              
              
            
          
          compilation.hooks.afterOptimizeAssets.tap('MyPlugin', (assets) => {
  console.log('优化生成的资源之后...');
});
        Async 和 Sync 钩子
- 同步钩子 :使用 
tap方法注册回调函数。 
            
            
              javascript
              
              
            
          
          compiler.hooks.compile.tap('MyPlugin', (params) => {
  console.log('同步钩子:编译开始...');
});
        异步钩子 :使用 tapAsync 或 tapPromise 方法注册回调函数。
            
            
              javascript
              
              
            
          
          compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
  console.log('异步钩子:生成资源到输出目录之前...');
  callback();
});
compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => {
  return new Promise((resolve) => {
    console.log('异步钩子:生成资源到输出目录之前...');
    resolve();
  });
});