前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >6-1~2 如何编写一个 loader

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

作者头像
love丁酥酥
发布2020-06-02 15:37:43
5330
发布2020-06-02 15:37:43
举报
文章被收录于专栏:coding for lovecoding for love

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

代码语言:javascript
复制
module.exports = function(content, map, meta) {
  return someSyncOperation(content);
};

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

sync-loader-with-multiple-results.js

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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 文件呢。

代码语言:javascript
复制
<!--src/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>title</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
代码语言:javascript
复制
// src/index.jsl
Log(123);

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

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

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

代码语言:javascript
复制
// 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 传入参数的。

代码语言:javascript
复制
module: {
    rules: [
      {
        test: /\.jsl$/,
        use: [
          {
            loader: path.resolve(__dirname, '../loaders/jsl-loader'),
            options: {
              tip: 'jsl-info:',
            },
          },
        ],
      },
    ],
  },

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

代码语言:javascript
复制
// 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。 如果你不提换的话,可能有时候回遇到这种情况:

代码语言:javascript
复制
{
        test: /\.jsl$/,
        use: [
          {
            loader: path.resolve(__dirname, '../loaders/jsl-loader'),
            options: 'tip=jsl-info',
          },
        ],
      },

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

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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 函数。

代码语言:javascript
复制
// 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 是否也可以这样呢?

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简介
  • 2. 编写 loader
    • 2.1 同步 loader
      • 2.2 异步 loader
      • 3. 实例
        • 3.1 装载自定义的 tsl 文件
          • 3.2 传参
          • 3.3 抛出额外信息
          • 3.4 异步 loader
          • 3.5 resolveloader
          • 参考
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档