返回首页

Loader 和 Plugin 的区别?

问题解析

Loader 和 Plugin 是 Webpack 中两个核心但容易混淆的概念。面试官通过此题考察候选人对 Webpack 架构设计的理解深度。

核心概念

特性 Loader Plugin
作用 文件转换/预处理 功能扩展/生命周期介入
执行时机 模块加载时 构建生命周期各阶段
配置位置 module.rules plugins
数据流向 从右到左、从下到上 基于事件流
实现基础 纯函数转换 Tapable 事件机制

详细解答

Loader:资源的翻译官

Loader 是一个导出函数的 Node 模块,用于对模块源码进行转换。

基本结构

// my-loader.js
module.exports = function(source) {
  // source: 文件源码字符串
  const result = source.replace(/console\.log\(/g, '// console.log(');
  return result;
};

配置方式

module.exports = {
  module: {
    rules: [
      // 每项是一个对象
      {
        test: /\.js$/,
        use: [
          'babel-loader',
          'eslint-loader'
        ],
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: { modules: true }
          }
        ]
      }
    ]
  }
};

执行顺序

Loader 执行遵循 从右到左、从下到上 的管道式顺序:

use: ['a-loader', 'b-loader', 'c-loader']
// 执行顺序: c-loader → b-loader → a-loader

Plugin:功能的扩展者

Plugin 基于 Tapable 事件流框架,可以在 Webpack 构建的各个阶段执行自定义逻辑。

基本结构

class MyPlugin {
  constructor(options) {
    this.options = options;
  }

  // 必须实现 apply 方法
  apply(compiler) {
    // 注册钩子
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      console.log('资源发射前执行');
    });
  }
}

module.exports = MyPlugin;

配置方式

const MyPlugin = require('./my-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [
    // 每项是 Plugin 实例
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new MyPlugin({ option: 'value' })
  ]
};

深入理解

Loader 的工作原理

// 简化版 Loader 执行流程
function runLoaders(loaders, resource) {
  // 1. 将 Loader 字符串路径转为函数
  const loaderFunctions = loaders.map(loader => require(loader));

  // 2. 从右到左执行
  let result = resource;
  for (let i = loaderFunctions.length - 1; i >= 0; i--) {
    result = loaderFunctions[i](result);
  }

  return result;
}

Plugin 的工作原理

// Tapable 事件流机制
const { SyncHook, AsyncSeriesHook } = require('tapable');

class Compiler {
  constructor() {
    this.hooks = {
      compile: new SyncHook(['params']),
      emit: new AsyncSeriesHook(['compilation']),
      done: new SyncHook(['stats'])
    };
  }

  run() {
    this.hooks.compile.call({});
    // ... 编译过程
    this.hooks.emit.callAsync(compilation, () => {
      this.hooks.done.call(stats);
    });
  }
}

两者协作示例

// 处理 Sass 文件的完整流程
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',    // 将 CSS 注入 DOM (Loader)
          'css-loader',      // 解析 CSS 中的 @import 和 url() (Loader)
          'sass-loader'      // 将 Sass 编译为 CSS (Loader)
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({  // 提取 CSS 为独立文件 (Plugin)
      filename: '[name].css'
    }),
    new OptimizeCSSAssetsPlugin() // 压缩 CSS (Plugin)
  ]
};

最佳实践

1. 何时使用 Loader

  • 需要转换文件内容(如 ES6+ 转 ES5)
  • 需要处理非 JS 资源(如图片、样式、字体)
  • 需要在模块加载时进行预处理

2. 何时使用 Plugin

  • 需要修改构建输出(如生成 HTML)
  • 需要在特定生命周期执行操作
  • 需要添加全局功能(如热更新、压缩)

3. 编写自定义 Loader

// 一个简单的 Markdown Loader
const marked = require('marked');

module.exports = function(source) {
  // 使用 this.query 获取参数
  const options = this.query;

  // 异步处理
  const callback = this.async();

  marked(source, options, (err, html) => {
    if (err) return callback(err);

    // 返回 JS 模块代码
    callback(null, `module.exports = ${JSON.stringify(html)}`);
  });
};

4. 编写自定义 Plugin

// 生成文件清单的插件
class ManifestPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('ManifestPlugin', (compilation) => {
      const manifest = {};

      for (const name of Object.keys(compilation.assets)) {
        manifest[name] = {
          size: compilation.assets[name].size(),
          source: compilation.assets[name].source()
        };
      }

      // 添加新资源到输出
      compilation.assets['manifest.json'] = {
        source: () => JSON.stringify(manifest, null, 2),
        size: () => JSON.stringify(manifest).length
      };
    });
  }
}

面试要点

  1. 核心区别一句话概括:Loader 是文件转换器,Plugin 是功能扩展器
  2. 配置方式差异:Loader 在 module.rules 中配置为对象数组,Plugin 在 plugins 中配置为实例数组
  3. 执行机制:Loader 是管道式顺序执行,Plugin 是基于 Tapable 的事件流
  4. 实际举例:结合项目经验说明使用的 Loader(如 babel-loader)和 Plugin(如 HtmlWebpackPlugin)
  5. 进阶理解:Loader 处理单个文件,Plugin 可以操作整个构建过程