深刻Webpack-编写Loader

Loader 就像是一个翻译员,能把源文件通过转化后输出新的结果,而且一个文件还能够链式的通过多个翻译员翻译。css

以处理 SCSS 文件为例:html

  1. SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  2. 把 sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  3. 把 css-loader 输出的 CSS 交给 style-loader 处理,转换成经过脚本加载的 JavaScript 代码;

能够看出以上的处理过程须要有顺序的链式执行,先 sass-loader 再 css-loader 再 style-loader。 以上处理的 Webpack 相关配置以下:node

module.exports = {
  module: {
    rules: [
      {
        // 增长对 SCSS 文件的支持
        test: /\.scss/,
        // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
        use: [
          'style-loader',
          {
            loader:'css-loader',
            // 给 css-loader 传入配置项
            options:{
              minimize:true, 
            }
          },
          'sass-loader'],
      },
    ]
  },
};
复制代码

Loader 的职责

由上面的例子能够看出:一个 Loader 的职责是单一的,只须要完成一种转换。 若是一个源文件须要经历多步转换才能正常使用,就经过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每一个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。webpack

因此,在你开发一个 Loader 时,请保持其职责的单一性,你只需关心输入和输出。git

Loader 基础

因为 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块须要导出一个函数。 这个导出的函数的工做就是得到处理前的原内容,对原内容执行处理后,返回处理后的内容。github

一个最简单的 Loader 的源码以下:web

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数须要返回处理后的内容,这里简单起见,直接把原内容返回了,至关于该 Loader 没有作任何转换
  return source;
};
复制代码

因为 Loader 运行在 Node.js 中,你能够调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:npm

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};
复制代码

Loader 进阶

以上只是个最简单的 Loader,Webpack 还提供一些 API 供 Loader 调用,下面来一一介绍。json

得到 Loader 的 options

在最上面处理 SCSS 文件的 Webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader。 如何在本身编写的 Loader 中获取到用户传入的 options 呢?须要这样作:api

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};
复制代码

返回其它结果

上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还须要返回除了内容以外的东西。

例如以用 babel-loader 转换 ES6 代码为例,它还须要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一块儿随着 ES5 代码返回给 Webpack,能够这样写:

module.exports = function(source) {
  // 经过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};
复制代码

其中的 this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通讯。 this.callback 的详细使用方法以下:

this.callback(
    // 当没法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 若是本次转换为原内容生成了 AST 语法树,能够把这个 AST 返回,
    // 以方便以后须要 AST 的 Loader 复用该 AST,以免重复生成 AST,提高性能
    abstractSyntaxTree?: AST
);
复制代码

Source Map 的生成很耗时,一般在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否须要 Source Map。 若是你编写的 Loader 会生成 Source Map,请考虑到这点。

同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,由于它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你须要经过网络请求才能得出结果,若是采用同步的方式网络请求就会阻塞整个构建,致使构建很是缓慢。

在转换步骤是异步时,你能够这样:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 经过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};
复制代码

处理二进制数据

在默认的状况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就须要 Webpack 给 Loader 传入二进制格式的数据。 为此,你须要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也能够是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也能够返回 Buffer 类型的结果
    return source;
};
// 经过 exports.raw 属性告诉 Webpack 该 Loader 是否须要二进制数据 
module.exports.raw = true;
复制代码

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

缓存加速

在有些状况下,有些转换操做须要大量计算很是耗时,若是每次构建都从新执行重复的转换操做,构建将会变得很是缓慢。 为此,Webpack 会默认缓存全部 Loader 的处理结果,也就是说在须要被处理的文件或者其依赖的文件没有发生变化时, 是不会从新调用对应的 Loader 去执行转换操做的。

若是你想让 Webpack 不缓存该 Loader 的处理结果,能够这样:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};
复制代码

其它 Loader API

除了以上提到的在 Loader 中能调用的 Webpack API 外,还存在如下经常使用 API:

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src

  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1

  • this.resourcePath:当前处理文件的路径,例如 /src/main.js

  • this.resourceQuery:当前处理文件的 querystring。

  • this.target:等于 Webpack 配置中的 Target,详情见 2-7其它配置项-Target

  • this.loadModule:但 Loader 在处理一个文件时,若是依赖其它文件的处理结果才能得出当前文件的结果时, 就能够经过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去得到 request 对应文件的处理结果。

  • this.resolve:像 require 语句同样得到指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))

  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会从新调用 Loader 处理该文件。使用方法为 addDependency(file: string)

  • this.addContextDependency:和 addDependency 相似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)

  • this.clearDependencies:清除当前正在处理文件的全部依赖,使用方法为 clearDependencies()

  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

其它没有提到的 API 能够去 Webpack 官网 查看。

加载本地 Loader

在开发 Loader 的过程当中,为了测试编写的 Loader 是否能正常工做,须要把它配置到 Webpack 中后,才可能会调用该 Loader。 在前面的章节中,使用的 Loader 都是经过 Npm 安装的,要使用 Loader 时会直接使用 Loader 的名称,代码以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};
复制代码

若是还采起以上的方法去使用本地开发的 Loader 将会很麻烦,由于你须要确保编写的 Loader 的源码是在 node_modules 目录下。 为此你须要先把编写的 Loader 发布到 Npm 仓库后再安装到本地项目使用。

解决以上问题的便捷方法有两种,分别以下:

Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能作到在不发布模块的状况下,把本地的一个正在开发的模块的源码连接到项目的 node_modules 目录下,让项目能够直接使用本地的 Npm 模块。 因为是经过软连接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤以下:

  1. 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  2. 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  3. 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块连接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

连接好 Loader 到项目后你就能够像使用一个真正的 Npm 模块同样使用本地的 Loader 了。

ResolveLoader

2-7其它配置项 中曾介绍过 ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认状况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 须要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则须要以下配置:

module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有前后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}
复制代码

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,若是找不到,会再去 ./loaders/ 目录下寻找。

实战

上面讲了许多理论,接下来从实际出发,来编写一个解决实际问题的 Loader。

该 Loader 名叫 comment-require-loader,做用是把 JavaScript 代码中的注释语法

// @require '../style/index.css'
复制代码

转换成

require('../style/index.css');
复制代码

该 Loader 的使用场景是去正确加载针对 Fis3 编写的 JavaScript,这些 JavaScript 中存在经过注释的方式加载依赖的 CSS 文件。

该 Loader 的使用方法以下:

module.exports = {
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件经过 comment-require-loader 去转换 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};
复制代码

该 Loader 的实现很是简单,完整代码以下:

function replace(source) {
    // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css'); 
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};
复制代码

本实例提供项目完整代码

《深刻浅出Webpack》全书在线阅读连接

阅读原文

相关文章
相关标签/搜索