打包工具之Webpack篇(五) | 青训营笔记

139 阅读4分钟

这是我参与第四届青训营笔记创作活动的第十二天

今天这篇我们开始对loader"下手",了解loader原理以及用法,最后试着简单实现一些简易的loader。话不多说,我们开始吧!

loader

由于webpack只认识js,所以我们经常需要loader,loader指的是为了处理非标准js资源所设计出的资源翻译模块,帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块,也即是将资源翻译成标准js。

使用loader

要使用loader,首先要先安装对应的loader。比如要处理的是less等资源,我们需要安装以下三个 image.png 然后添加module处理less文件

image.png 这样就可以处理less了,不过为什么需要三个loader呢?而且能不能调换顺序呢?这就得扯到loader的链式调用了。上面三个loader其实是从后往前执行的,也就是先是less-loader,接着css-loader,最后是style-loader。如图所示

image.png 为什么要以这种顺序呢?我们得先搞清楚他们各自是干嘛的?less-loader实现的是less到css的转换,而css-loader是将css包装成类似module.export="${css}"的内容,包装后的被人符合js语法,最后的style-loader将css模块包进require语句,并在执行时调用injectStyle等函数将内容注入到页面的style标签。

loader 执行顺序

平时如果没有其他配置的话,loader都是从下往上执行的,但是我们也可以通过一些属性配置修改顺序。

1、分类

  • pre: 前置 loader
  • normal: 普通 loader
  • inline: 内联 loader
  • post: 后置 loader

2、 执行顺序

4 类 loader 的执行优级为:pre > normal > inline > post

相同优先级的 loader 执行顺序为:从右到左,从下到上

举个例子:此时loader执行顺序:loader3 - loader2 - loader1

module: {
    rules: [
        {
            test: /.js$/,
            loader: "loader1",
        },
        {
            test: /.js$/,
            loader: "loader2",
        },
        {
            test: /.js$/,
            loader: "loader3",
        },
    ],
},

再举个例子:此时loader执行顺序:loader1- loader2 - loader3

module: {
    rules: [
        {
            enforce:"pre",
            test: /.js$/,
            loader: "loader1",
        },
        {
            test: /.js$/,
            loader: "loader2",
        },
        {    
            enforce:"post",
            test: /.js$/,
            loader: "loader3",
        },
    ],
},

3、 使用loader的方式

配置方式:在webpack.config.js文件中指定loader。(pre、normal、post loader)

内联方式:在每个import语句中显式指定loader(inline loader)

4、 inline loader

用法:

 import Styles from 'style-loader!css-loader?modules!./stykes.css';

含义: 使用css-loaderstyle-loader处理style.css文件 通过!将资源中的loader分开

扩展

inline loader 可以通过添加不同前缀,跳过其他类型 loader。

! 跳过 normal loader。

import Styles from '!style-loader!css-loader?modules!./stykes.css';

-! 跳过 pre 和 normal loader。

import Styles from '-!style-loader!css-loader?modules!./stykes.css';

!! 跳过 pre、 normal 和 post loader。

import Styles from '!!style-loader!css-loader?modules!./stykes.css';

开发一个 loader

我们先做一个最简单的loader,它接受要处理的源码作为参数,输出转换后的 js 代码。

// loaders/loader1.js
module.exports = function loader1(content) {
  console.log("hello loader");
  return content;
};

我们会看到最后return的是一个content,那loader 接受的参数有哪些呢?可以是以下三个!

  • content 源文件的内容
  • map SourceMap 数据
  • meta 数据,可以是任何内容

我们试着用这三个写一个同步loader,最直接的可能是这样

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

但是这不是很灵活,我们试试用一下this.callback ,它相对更灵活,因为它允许传递多个参数,而不仅仅是 content。那么上面的代码我们可以改成这样。

module.exports = function (content, map, meta) {
    // 传递map,让source-map不中断
    // 传递meta,让下一个loader接收到其他参数
    this.callback(null, content, map, meta);
    return; 
    // 当调用 callback() 函数时,总是返回 undefined
};

但是事实上我们平时不会写很多同步loader,由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,所以我们尽可能地使 loader 异步化。但如果计算量很小,同步 loader 也是可以的。我们看一下异步loader可以怎么写?

module.exports = function (content, map, meta) {
    const callback = this.async();
    // 进行异步操作
    setTimeout(() => {
        callback(null, result, map, meta);
    },
    1000);
};

这个案例是通过定时器setTimeout来实现异步的,感兴趣的同学可以再试一试其他方式。

有时候我们需要处理一些图片的资源文件。而默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。那我们应该怎么处理呢?或许我们只需要通过设置 raw 为 true,loader 就可以接收原始的 Buffer。

module.exports = function (content) {
    // content是一个Buffer数据
    return content;
};
module.exports.raw = true; // 开启 Raw Loader

最后我们还要认识一下Pitching Loader

module.exports = function (content) {
    return content;
};
module.exports.patch = function (remainingRequest, precedingRequest, data) {
  console.log("do somethings");
};

webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。

在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。

loader API

方法名含义用法
this.async异步回调 loader。返回 this.callbackconst callback = this.async()
this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err, content, sourceMap?, meta?)
this.getOptions(schema)获取 loader 的 optionsthis.getOptions(schema)
this.emitFile产生一个文件this.emitFile(name, content, sourceMap)
this.utils.contextify返回一个相对路径this.utils.contextify(context, request)
this.utils.absolutify返回一个绝对路径this.utils.absolutify(context, request)

更多文档,请查阅 webpack 官方 loader api 文档

loader就先了解到这里啦,不过大家如果感兴趣可以手写一些平时会用到的loader,比如clean-log-loader, babel-loader, file-loader或者style-loader等等。 给一个参考文章:Webpack 原理系列七:如何编写loader

常见loader

站在使用角度,建议大家掌握这些常用Loader的功能与配置方法

image.png 图源范文杰老师

课后思考

1、Loader 输入是什么?要求的输出是什么?(转自Webpack 原理系列七:如何编写loader)

答:代码层面,Loader 通常是一个函数,结构如下:

module.exports = function(source, sourceMap?, data?) {
  // source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
  return source;
};

(1)Loader 函数接收三个参数,分别为:

  • source:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果
  • sourceMap: 可选参数,代码的 sourcemap 结构
  • data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象

其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的 output ,比如 webpack-contrib/raw-loader 的核心源码:

//... 
export default function rawLoader(source) {
  // ...

  const json = JSON.stringify(source)
    .replace(/\u2028/g'\u2028')
    .replace(/\u2029/g'\u2029');

  const esModule =
    typeof options.esModule !== 'undefined' ? options.esModule : true;

  return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

这段代码的作用是将文本内容包裹成 JavaScript 模块,例如:

// source
I am Tecvan

// output
module.exports = "I am Tecvan"

经过模块化包装之后,这段文本内容转身变成 Webpack 可以处理的资源模块,其它 module 也就能引用、使用它了。

(2)返回多个结果

上例通过 return 语句返回处理结果,除此之外 Loader 还可以以 callback 方式返回更多信息,供下游 Loader 或者 Webpack 本身使用,例如在 webpack-contrib/eslint-loader 中:

export default function loader(content, map) {
  // ...
  linter.printOutput(linter.lint(content));
  this.callback(null, content, map);
}

通过 this.callback(null, content, map) 语句同时返回转译后的内容与 sourcemap 内容。callback 的完整签名如下:

this.callback(
    // 异常信息,Loader 正常运行时传递 null 值即可
    errError | null,
    // 转译结果
    contentstring | Buffer,
    // 源码的 sourcemap 信息
    sourceMap?: SourceMap,
    // 任意需要在 Loader 间传递的值
    // 经常用来传递 ast 对象,避免重复解析
    data?: any
);

2、Loader 的链式调用是什么意思?如何串联多个 Loader?

答:loader 支持链式调用,即链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。

串联多个 Loader可以是下面这种方式 image.png

但由于链式调用的一部分有两个问题:

(1) Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常

(2)某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行

为了解决这两个问题,Webpack 在 loader 基础上叠加了 pitch 的概念,大家可以去了解一下。

3、Loader 中如何处理异步场景?(转自转自Webpack 原理系列七:如何编写loader

答:涉及到异步或 CPU 密集操作时,Loader 中还可以以异步形式返回处理结果,例如 webpack-contrib/less-loader 的核心逻辑:

import less from "less";

async function lessLoader(source) {
  // 1. 获取异步回调函数
  const callback = this.async();
  // ...

  let result;

  try {
    // 2. 调用less 将模块内容转译为 css
    result = await (options.implementation || less).render(data, lessOptions);
  } catch (error) {
    // ...
  }

  const { css, imports } = result;

  // ...

  // 3. 转译结束,返回结果
  callback(null, css, map);
}

export default lessLoader;

在 less-loader 中,逻辑分三步:

  • 调用 this.async 获取异步回调函数,此时 Webpack 会将该 Loader 标记为异步加载器,会挂起当前执行队列直到 callback 被触发
  • 调用 less 库将 less 资源转译为标准 css
  • 调用异步回调 callback 返回处理结果

this.async 返回的异步回调函数签名 this.callback 相同。

至此,关于loader的就介绍到这里。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!