美文网首页
6-1~2 如何编写一个 loader

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

作者: love丁酥酥 | 来源:发表于2020-05-30 23:38 被阅读0次

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

相关文章

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

    1. 简介 loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只...

  • 如何编写一个Loader

    创建一个demo(make_loader) 新建一个文件,名字为make_loader 在make_loader目...

  • 编写一个webpack的loader(2)

    目录 介绍一下raw-loader 编写一个raw-loader的demo,watermark-loader 个人...

  • 编写一个 loader

    loader 是一个导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用。给定的函数将调用...

  • 编写 webpack loader

    loader loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加...

  • 编写 webpack Loader

    loader 类型 pre post normal inline 行内 loader 1、2、3 通过修改 loa...

  • 怎么编写一个 webpack loader

    编写 loader 前需要知道 webpack loader 的执行循序?从右到左执行。 为什么 webpack ...

  • 第六章 js-web-api 下

    6-1 事件 题目 1.编写一个通用的事件监听函数 2.描述事件冒泡流程dom树形结构事件冒泡阻止冒泡冒泡应用 3...

  • Webpack4.0原理

    1. 编写一个Loader 1.1 初始化项目 创建文件夹make-loader 初始化npm包➜ make-lo...

  • 如何写一个 webpack loader

    本文主要讲如何快速写一个 webpack loader 和如何调试 webpack loader. 01 背景 w...

网友评论

      本文标题:6-1~2 如何编写一个 loader

      本文链接:https://www.haomeiwen.com/subject/zlqkzhtx.html