这是我参与第四届青训营笔记创作活动的第十二天
今天这篇我们开始对loader"下手",了解loader原理以及用法,最后试着简单实现一些简易的loader。话不多说,我们开始吧!
loader
由于webpack只认识js,所以我们经常需要loader,loader指的是为了处理非标准js资源所设计出的资源翻译模块,帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块,也即是将资源翻译成标准js。
使用loader
要使用loader,首先要先安装对应的loader。比如要处理的是less等资源,我们需要安装以下三个
然后添加
module处理less文件
这样就可以处理less了,不过为什么需要三个loader呢?而且能不能调换顺序呢?这就得扯到loader的
链式调用了。上面三个loader其实是从后往前执行的,也就是先是less-loader,接着css-loader,最后是style-loader。如图所示
为什么要以这种顺序呢?我们得先搞清楚他们各自是干嘛的?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-loader和style-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源文件的内容mapSourceMap 数据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.callback | const callback = this.async() |
| this.callback | 可以同步或者异步调用的并返回多个结果的函数 | this.callback(err, content, sourceMap?, meta?) |
| this.getOptions(schema) | 获取 loader 的 options | this.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的功能与配置方法
图源范文杰老师
课后思考
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 值即可
err: Error | null,
// 转译结果
content: string | Buffer,
// 源码的 sourcemap 信息
sourceMap?: SourceMap,
// 任意需要在 Loader 间传递的值
// 经常用来传递 ast 对象,避免重复解析
data?: any
);
2、Loader 的链式调用是什么意思?如何串联多个 Loader?
答:loader 支持链式调用,即链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
串联多个 Loader可以是下面这种方式
但由于链式调用的一部分有两个问题:
(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的就介绍到这里。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!