详解 Webpack Loader

93 阅读4分钟

Loader 是一个 NodeJS 的普通模块,通过 module.exports 挂载出一个纯函数。

/**
 *
 * @param {string} content
 * @returns
 */

module.exports = function(content) {
  // 对content进行处理,eg:
  return content.replace('hello', '你好');
};

content 通常是字符串类型,如果设置了 module.exports.raw = true,content就是Buffer类型。

Loader有什么用

本质就是接受输入,可以对输入进行各式各样的修改后返回。

解决兼容性问题

我们可以随意写ES6的语法,CSS的语法,通过 Loader 可以帮助我们完成兼容性问题处理。

使用 JSX

为了提升开发效率,我们使用 JSX 编写模板更加简单快速, Loader 可以将编写的 JSX 转化成框架API。

// JSX
<div onClick={sayHello}>
    <h1>hello</h1>
</div>

// 通过 babel-loader 转成 React API,也是实际上在浏览器运行的JS代码。
React.createElement("div", {
  onClick: sayHello
}, React.createElement("h1", null, "hello"));

Loader之间如何协作

这里直接引用官方的例子,非常直观。

module.exports = {

  //...

  module: {
    rules: [
      {
        //...
        use: [
          'a-loader',
          'b-loader',
          'c-loader'
        ]
      }
    ]
  }
};

那么执行的顺序应该是

|- c-loader normal execution

|- b-loader normal execution

|- a-loader normal execution

可以看到 loader 是倒序执行的,如果我们想要顺序的时候去做一些事情怎么办?

我们可以给 loader对象挂载一个 pitch 方法。

/**
 *
 * @param {string} content
 * @returns
 */
module.exports = function(content) {
  // 对content进行处理,eg:
  return content.replace('hello', '你好');
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
    console.log("pitch");
};
|- a-loader `pitch`

  |- b-loader `pitch`

    |- c-loader `pitch`

      |- requested module is picked up as a dependency

    |- c-loader normal execution

  |- b-loader normal execution

|- a-loader normal execution

如果 b-loader的pitch方法返回了值,则忽略c-loader,从a-loader开始倒序执行 normal execution。

module.exports = function(content) {
  return someSyncOperation(content);
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return 'cache content';
  }
};

一旦 someCondition() 满足,执行的顺序如下:

|- a-loader `pitch`

  |- b-loader `pitch` returns a module

|- a-loader normal execution

基于 Pitch 的机制,我们可以缓存之前 loader 处理的结果,比如已存在b-loader、c-loader处理后的数据,在a-loader中判断满足缓存条件,直接返回缓存数据,跳过b、c的处理,提升构建效率。

更改loader执行顺序

如果我们希望改变loader的执行顺序,可以通过 rule.enforce 来实现:

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: [
          {
             loader: 'a-loader',
             enforce: 'pre'
          },
          {
             loader: 'b-loader',
             enforce: 'post'
          },
          'c-loader'
        ]
      }
    ]
  }
};

那么实际的顺序就是

|- b-loader `pitch`

  |- c-loader `pitch`

    |- a-loader `pitch`

      |- requested module is picked up as a dependency

    |- a-loader normal execution

  |- c-loader normal execution

|- b-loader normal execution

那为什么不直接调整loader的顺序,非要通过enforce来指定呢?如果只是单纯调整loader顺序,那么所有的loader还是会被执行,但是当我们指定enforce,我们可以将loader分类,控制各种类型loader的执行:

  • 所有普通 loader 可以通过在请求中加上 ! 前缀来忽略(覆盖)。
  • 所有普通和前置 loader 可以通过在请求中加上 -! 前缀来忽略(覆盖)。
  • 所有普通,后置和前置 loader 可以通过在请求中加上 !! 前缀来忽略(覆盖)。
// 禁用普通 loaders
import { a } from '!./file1.js';

// 禁用前置和普通 loaders
import { b } from  '-!./file2.js';

// 禁用所有的 laoders
import { c } from  '!!./file3.js';

Loader的工作方式

Loader 支持同步和异步的处理方式

同步

module.exports = function(content, map, meta) {
  return content;
};

// 或者

module.exports = function(content) {
  this.callback(null, content);
  return;
};

异步

module.exports = function(content, map, meta) {

  var callback = this.async();

  someAsyncOperation(content, function(err, result) {

    if (err) return callback(err);

    callback(null, result, map, meta);

  });

};

Loader 和 Webpack 之间如何协作

直接上图,省略了较多步骤。 流程图👇

执行 loader 主要是 loader-runner 这个库,作为独立的npm包,可以单独使用。

Loader 和 Plugin 的区别

Loader 是对一个个的文件进行处理,它是一个转换器,将A文件进行编译成B文件。

Plugin 是贯穿在整个构建生命周期,可以对不同阶段的构建产物进行处理。

如何写一个高质量的 Loader

对于比较复杂的Loader,通常需要我们设置一些参数,比如babel-loader设置预设和插件:

module: {
  rules: [
    {
      test: /.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          plugins: ['@babel/plugin-proposal-object-rest-spread']
        }
      }
    }
  ]
}

为了保证参数的准确性,Webpack提供了 loader-utilsschema-utils 这两个库进行校验。

import { getOptions } from 'loader-utils';

import { validate } from 'schema-utils';

// loader所需的options类型
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string'
    }
  }
};

export default function(source) {

  // 获取参数
  const options = getOptions(this);

  // 进行验证
  validate(schema, options, {
    name: 'Example Loader',
    baseDataPath: 'options'
  });

  // Apply some transformations to the source...
  return `export default ${ JSON.stringify(source) }`;

}

另外,官方也提出了一下建议:

  • 功能单一,尽量简单。
  • 保证链接性,也就是接受前一个loader返回的,输出给下一个loader。
  • 输出的代码也需要满足模块化。
  • 满足无状态,FP的核心,每次运行应该始终独立于其他已编译模块以及同一模块的以前编译。
  • 显示声明依赖,通过 this.addDependency,保证依赖修改后缓存失效。
  • 不要在Loader中使用绝对路径,避免修改目录名称后hash失效。