Loader

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-27

一个 Loader的职责是单一的,只需要完成一种转换。如果一个源文件需要经历多步转换才能正常使用,就通过多个Loader去转换。在调用多个Loader去转换一个文件时,每个Loader都会链式地顺序执行。第1个Loader将会拿到需处理的内容,上一个Loader处理后的结果会被传给下一个Loader接着处理,最后的Loader将处理后的最终结果返回给 Webpack。所以,在开发一个 Loader时,请保持其职责的单一性,我们只需关心输入和输出。

Webpack 是运行在 Node.js上的,一个 Loader 其实就是一个 Node.js模块,这个模块需要导出一个函数。这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

// 最基本的结构
// Loader运行在Node.js中,所以可以调用任何Node.js的API
const sass = require('sass');

module.exports = function (source) {
  // source为compiler传递给Loader的一个文件的原内容
  return sass(source)
}

关于获取Loader的options

// 借助loader-utils
const loaderUtils = require('loader-utils');

module.exports = function (source) {
  // 获取用户为当前Loader 传入的options
  const options = loaderUtils.getOptions(this);
  return source
}

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

以用babel-loader 转换ES6 代码为例,它还需要输出转换后的ES5代码对应的 SourceMap ,以方便调试源码。为了将 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的详细使用方法,即可以接受哪些内容从Loader中传到webpack

this.callback(
  // 当无法转换原内容时,为Webpack 返回一个Error
  err: Error | null,
  // 原内容转换后的内容
  content: string | Buffer,
  // 可选,用于通过转换后的内容得出原内容的Source Map ,以方便调试
  // webpack 会通过 this.sourceMap API 告诉Loader用户是否配置需要sourceMap
  sourceMap?: SourceMap,
  // 如果本次转换为原内容生成了AST 语法树,则可以将这个AST 返回,
  // 以方便之后需要AST 的Loader 复用该AST ,避免重复生成AST ,提升性能
  abstractSyntaxTree?: AST 
)

同步与异步

Loader 有同步和异步之分,上面介绍的都是同步的 Loader ,因为它们的转换流程都是同步的,转换完成后再返回结果。但在某些场景下转换的步骤只能是异步完成的,例如我们需要通过网络请求才能得出结果,如果采用同步的方式,则网络请求会阻塞整个构建,导致构建非常缓慢。

如果是异步转换,则我们可以这样做:

module.exports = function(source) {
  // 告诉Webpack 本次转换是异步的, Loader 会在callback 中回调结果
  var callback = this.async();

  // 异步操作函数
  function someAsyncOperation(source, cb) {
    var err, result, sourceMaps, ast;

    // 模拟异步
    new Promise((res, rej) => {
      resolve();
    }).then(res => {
      // ...异步操作成功后 将正确处理过需要返回的值返回
      cb(err, result, sourceMaps, ast)
    })
  }

  someAsyncOperation(source, function(err, result, sourceMaps, ast){
    // 通过callback 返回异步执行后的结果
    callback(err, result, sourceMaps, ast)
    // this.async()(err, result, sourceMaps, ast)
  })
}

处理二进制数据

在默认情况下, Webpack 传给 Loader 的原内容都是UTF-8 格式编码的字符串。但在某些场景下 Loader 不会处理文本文件,而会处理二进制文件如fil e-loader ,这时就需要 WebpackLoader 传入二进制格式的数据。为此,我们需要这样编写Loader

module.exports = function(source) {
  console.log(source instanceof Buffer);
  // true , webpack 传进来的source会是二进制数据

  // 但是不管module.exports.raw 是不是 true, Loader都可以返回二进制数据
  return source
}

// 通过exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
// 如果没有这一行 则Loader只能拿到字符串
module.exports.raw = true;

缓存加速

在某些情况下,有些转换操作需要大量的计算,非常耗时,如果每次构建都重新执行重复的转换操作,则构建将会变得非常缓慢。为此,Webpack会默认缓存所有Loader的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,是不会重新调用对应的Loader去执行转换操作的。

// 如果我们不想让Webpack 不缓存该Loader 的处理结果,则可以这样:

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

其他Loader API

Loader中还可以使用以下API

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

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

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

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

  • this.target:等于 Webpack 配置中的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(file:string) : 为当前处理的文件添加其依赖的文件,以便其依赖的文件发生发生变化时,重新调用 Loader 处理该文件

  • this.addContextDependency(file: string) : 同上,是将整个目录加入当前正在处理的文件的依赖中,以便其依赖的文件发生发生变化时,重新调用Loader 处理该目录下的文件。

  • this.clearDependencies :清除当前正在处理的文件的所有依赖

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

加载本地Loader

如果向采用第三方Loader的方式去使用本地开发的 Loader,将会很麻烦,因为我们需要确保编写的 Loader 的源码在 node_modules 目录下。为此需要先将编写的Loader 发布到 Npm仓库, 再安装到本地项目中使用。

有两种方法,可以解决上述问题,不需要加本地 Loader发布到 Npm 中。

Npm link

Npm link 专门用于开发和调试本地的 Npm 模块,能做到在不发布模块的情况下,将本地的一个正在开发的模块的源码链接到项目的node_modules 目录下,让项目可以直接使用本地的 Npm 模块。由于是通过软链接的方式实现的,编辑了本地的 Npm 模块的代码,所以在项目中也能使用到编辑后的代码。

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

Resolveloader

配置resolveLoader,配置如何寻找Loader,默认会去node_modules中寻找。

module.exports = {
  resolveLoader: {
    // 去哪些目录下寻找Loader 有先后之分,先去node_modules中找,找不到再去./loader/目录中寻找。
    modules: ['node_modules', './loaders/']
  }
}

写一个简易的loader

github地址

编写一个转换md文件的loader。

有一个 demo.md 文件

## 我是二级标题

### 我是三级标题

我是内容

我们在 main.js 中这样引入

// main.js
import demo from './demo.md';

console.log(demo);

由于webpack除了js以外的文件,都需要提供 loader 去处理,方便扩展。社区内也有处理 md 文件的loader,我们这里自己来写一个 非常简单的loader,以了解其流程。

// markdown-loader.js
const marked = require('marked'); // 可以将marked转换为html

module.exports = (source) => {
  console.log('source', source);
  // return source; 
  // error 接受到的source是md文件的内容,loader必须要返回一个合格js代码字符串,可以理解为能够被eval执行

  const html = marked(source)
  // return html; 
  //  <h2 id="我是二级标题">我是二级标题</h2>
  //  <h3 id="我是三级标题">我是三级标题</h3>
  //  <p>我是内容</p>
  //  也会报错,因为只能接受js代码字符串
  // 所以需要将 html 字符串转换为 js代码字符安

  // 第一种 自己拼接
  // const result = `module.exports = ${JSON.stringify(html)}`;
  // return result 

  // 第二种 使用 html-loader,在webpack配置里面,在针对.md文件的配置中
  // 在自己写的 markdown-loader 之前插入 html-loader
  // 那么 我们这个 markdown-loader处理之后返回的 html字符串,就会交给 html-loader去处理
  // 这个loader会将html处理成js
  return html;
}

需要注意的是,一个文件可以被多个loader去处理,在loader列表中从右往左进行处理,所以第一个loader(最后处理的loader)应该返回一个js代码字符串(webpack只接收这种)

关于 webpack 的配置,就要将我们自己写的 loader 配上去。

// webpack.config.js

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.md$/,
        use: ['html-loader', './src/markdown-loader']
        // 如果不使用 html-loader可以删掉,选择自己拼接一个
      }
    ]
  }
}

最后我们运行webpack,就能从入口main.js进入,开始解析文件,分析依赖树,碰到了引入的 .md 文件,开始使用我们设置匹配的 markdown-loader 去处理, 这个loader返回一个 html 字符串,再交给 html-loader 处理返回一个 js代码字符串,最终交给 webpack 将其打包进 ./dist/bound.js 中, dist 是默认的输出文件目录,也可以通过 output.path 去修改这个输出文件目录。