6-1~2 如何编写一个 loader

1. 简介

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

2. 编写 loader

关于如何编写 loader,在文档 api/loaders中其实讲解非常详细。

所谓 loader 只是一个导出为函数的 JavaScript 模块。loader runner 会调用这个函数,然后把上一个 loader 产生的结果或者资源文件(resource file)传入进去。函数的 this 上下文将由 webpack 填充,并且 loader runner 具有一些有用方法,可以使 loader 改变为异步调用方式,或者获取 query 参数。

第一个 loader 的传入参数只有一个:资源文件(resource file)的内容。compiler 需要得到最后一个 loader 产生的处理结果。这个处理结果应该是 String 或者 Buffer(被转换为一个 string),代表了模块的 JavaScript 源码。另外还可以传递一个可选的 SourceMap 结果(格式为 JSON 对象)。

如果是单个处理结果,可以在同步模式中直接返回。如果有多个处理结果,则必须调用 this.callback()。在异步模式中,必须调用 this.async(),来指示 loader runner 等待异步结果,它会返回 this.callback() 回调函数,随后 loader 必须返回 undefined 并且调用该回调函数。

2.1 同步 loader

无论是 return 还是 this.callback 都可以同步地返回转换后的 content 内容:
sync-loader.js

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

this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是content。

sync-loader-with-multiple-results.js

module.exports = function(content, map, meta) {
  this.callback(null, someSyncOperation(content), map, meta);
  return; // 当调用 callback() 时总是返回 undefined
};

2.2 异步 loader

对于异步 loader,使用 this.async 来获取 callback 函数:
async-loader.js

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);
  });
};

async-loader-with-multiple-results.js

module.exports = function(content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function(err, result, sourceMaps, meta) {
    if (err) return callback(err);
    callback(null, result, sourceMaps, meta);
  });
};

3. 实例

3.1 装载自定义的 tsl 文件

下面我们来看一个简单的例子,假设我们定义了一个新的文件类型,jsl 文件,其中的 Log(xxx) 表示的是 console.log('jsl-log:', xxx),其余都语法和 js 一致,我们如何去装载 tsl 文件呢。

<!--src/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>title</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
// src/index.jsl
Log(123);

首先我们要实现一个 tsl-loader,这里面去麻烦,我们就不单独发一个 npm 包了,直接在项目中建立一个 loaders 目录,放我们的 loader。

// loaders/jsl-loader
module.exports = function (content) { // attention: 这里不可以用箭头函数
  return content.replace(/Log\((.*)\)/g, "console.log('tsl-log:', $1)");
};

因为只是做演示,我们就简单做了一个正则匹配进行替换,事实上做语法转换一般都需要 ast。这又是一块很复杂的内容,最近也做了一些词法解析和 ast 方面的学习研究,后续会写一个专题来记录。
然后我们配置一下:

// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './src/index.jsl',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.js', '.jsl'],
  },
  module: {
    rules: [
      {
        test: /\.jsl$/,
        use: [
          path.resolve(__dirname, '../loaders/jsl-loader'),
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin(),
  ],
};

npm run dev,打开生成的网页看一下:


image.png

可以看到,打包成功了。

3.2 传参

现在假设我们 Log 时,希望将 Log 翻译的 console 中,第一个参数是我们指定的参数,比如是 jsl-info,该如何做呢。
loader 本身是支持使用 options 传入参数的。

module: {
    rules: [
      {
        test: /\.jsl$/,
        use: [
          {
            loader: path.resolve(__dirname, '../loaders/jsl-loader'),
            options: {
              tip: 'jsl-info:',
            },
          },
        ],
      },
    ],
  },

在 jsl-loader 这边是如何接收的呢,我们上边讲了,这里 loader 是一个 function,而且不能使用箭头函数,就是因为这个 this 上下文是由 webpack 填充多个,其中包含了许多特定的属性和方法。这里,要去到对应的参数,我们可以使用 this-query

// loaders/jsl-loader
module.exports = function (content) { // attention: 这里不可以用箭头函数
  return content.replace(/Log\((.*)\)/g, `console.log("${this.query.tip}", $1)`);
};

这里官方还提示了一句,options 已取代 query,所以此属性废弃。使用 loader-utils 中的 getOptions 方法来提取给定 loader 的 options。
如果你不提换的话,可能有时候回遇到这种情况:

{
        test: /\.jsl$/,
        use: [
          {
            loader: path.resolve(__dirname, '../loaders/jsl-loader'),
            options: 'tip=jsl-info',
          },
        ],
      },

用户使用自出传的方式传递 options,这个时候我们的 loader 就拿不到对应参数了。如果改为 loader-utils,就不会有问题:

// loaders/jsl-loader
const loaderUtils = require('loader-utils');

module.exports = function (content) { // attention: 这里不可以用箭头函数
  const options = loaderUtils.getOptions(this);
  return content.replace(/Log\((.*)\)/g, `console.log("${options.tip}", $1)`);
};

3.3 抛出额外信息

前面我们的 laoder 都是返回源码处理后的结果而已,但有时候我们可能还会抛出一些其他的信息,比如报错信息,sourcemap 以及 ast。就必须要用到 this-callback

// loaders/jsl-loader
const loaderUtils = require('loader-utils');

module.exports = function (content) { // attention: 这里不可以用箭头函数
  const options = loaderUtils.getOptions(this);
  const res = content.replace(/Log\((.*)\)/g, `console.log("${options.tip}", $1)`);
  this.callback(null, res);
};

3.4 异步 loader

如果我们的 laoder 内部有异步逻辑,需要等待异步逻辑执行完才能抛出正确信息该如何做呢?对于 异步 loader,需要使用 this.async 来获取 callback 函数。

// loaders/jsl-loader
const loaderUtils = require('loader-utils');

module.exports = function (content) { // attention: 这里不可以用箭头函数
  const options = loaderUtils.getOptions(this);
  const callback = this.async();
  setTimeout(() => {
    const res = content.replace(/Log\((.*)\)/g, `console.log("${options.tip}", $1)`);
    callback(null, res);
  }, 2000);
};

3.5 resolveloader

可以看到,我们上面在指定 loader 时,写了一大串的路径,如果我们在 loaders 目录下定义了一堆 loader,配置文件看上去就会很冗长,我们写的也会很繁琐。
node_modules 里的 loader 我们在配置的时候都只用写名称即可,那么 loaders 下面的 laoder 是否也可以这样呢?

resolveLoader: {
    modules: ['loaders', 'node_modules'], // 谁在前,谁优先。可以使用 path.resolve 指定目录绝对地址,还可以使用 alias 选项指定别名
  },
  module: {
    rules: [
      {
        test: /\.jsl$/,
        use: [
          {
            loader: 'jsl-loader',
            options: 'tip=jsl-info',
          },
        ],
      },
    ],
  },

参考

writing-a-loader
concepts/#loader
loaders
api/loaders

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271