-
Notifications
You must be signed in to change notification settings - Fork 1
Description
什么是 loader
loader 的作用是将不同类型的文件转换为 webpack 可识别的模块。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包,loader 就是这样一个代码转换器。
loader 本质就是导出一个函数的 node 模块,loader runner 会调用这个函数,然后把上一个 loader产生的结果或者资源传进去。函数的 this 上下文是由 webpack 提供的。
function Loader(source, sourceMap?, data?) {
// source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
return source;
};
module.exports = Loaderloader 链式调用
可以在处理某种文件的时候配置多个 loader。
以处理 less 为例
module.exports = {
module: {
rules: [
{
test: /\.less/,
// less 文件的处理顺序为先 less-loader 再 css-loader 再 style-loader
use: [
'style-loader',
{
loader:'css-loader',
// 给 css-loader 传入配置项
options:{
minimize:true,
}
},
'less-loader'],
},
]
},
};可以看出来这是一个链式的调用。但是他们会以相反的顺序执行,从下到上或者从右到左
loader 使用
在 Webpack 中,loader 可以被分为 4 类:pre 前置、post 后置、normal 普通和 inline 行内。其中 pre 和 post loader 可以通过 rule 对象的 enforce 属性来指定
inline loader
// 使用 ! 将资源中的 loader 分开。
import Styles from 'style-loader!css-loader?modules!./styles.css';通过为内联 import 语句添加前缀,可以覆盖配置中的所有 loader
- 使用
!前缀,将禁用所有已配置的normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css';- 使用
!!前缀,将禁用所有已配置的loader(preLoader, normal loader, postLoader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';- 使用
-!前缀,将禁用所有已配置的preLoader和loader,但是不禁用postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';可以给 inline loader 传参,?key=value&foo=bar 或者 ?{"key":"value","foo":"bar"},上面的例子中的 modules 就是参数
pre Loader, normal loader, post Loader
顾名思义,指定了 enforce 属性的 loader 在执行时不是默认的从下到上(或从右到左)的顺序。
// webpack.config.js
// 这里就是先执行 a-loader 再执行 b-loader 最后执行 c-loader
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.txt$/i,
use: ["a-loader"],
enforce: "pre", // pre loader
},
{
test: /\.txt$/i,
use: ["b-loader"], // normal loader
},
{
test: /\.txt$/i,
use: ["c-loader"],
enforce: "post", // post loader
},
],
},
};raw模式
content 可以是 string 或者 Buffer,在我们处理图片,音频,视频等这些文件的时候,我们就需要用到 Buffer,这时我们需要将 row 设为 true。
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
};
module.exports.raw = truepitch
首先我们的 loader 通常是到处一个函数
function Loader(source, sourceMap?, data?) {
// source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
return source;
};
module.exports = Loader同时我们可以在 loader 函数上添加一个 pitch 属性,它的值也是一个函数。它会比 loader 更早执行
/**
* @remainingRequest 剩余请求
* @precedingRequest 前置请求
* @data 数据对象
*/
Loader.pitch = function (remainingRequest, precedingRequest, data) {
//
};可以通过 data 参数来进行数据传递,在 Normal loader 中就可以通过 this.data 的方式读取数据。
loader 执行链的执行过程分为三个阶段,pitch、解析资源、loader执行。
// a-loader
module.exports = function(content) {
return content;
};
module.exports.pitch = function(remainingRequest) {
//
};// b-loader
module.exports = function(content) {
return content;
};
module.exports.pitch = function(remainingRequest) {
//
};// c-loader
module.exports = function(content) {
return content;
};
module.exports.pitch = function(remainingRequest) {
//
};module: {
rules: [
{
test: /\.txt$/i,
use: ["a-loader","b-loader", "c-loader"],
},
],
},而 pitch 的执行顺序是和 loader 执行顺序相反,可以看成是顺序入栈倒序出栈
同时 pitch 还有一个重要功能:阻断。
还是上面的例子,只不过现在我们 b-loader 的 pitch 函数返回一个。
// b-loader
module.exports = function(content) {
return content;
};
module.exports.pitch = function(remainingRequest) {
return '123';
};
现在的执行顺序就是
当 pitch 有返回(非 undefined),就会跳过后续 pitch 的执行,接着只执行之前执行了 pitch 相关联的 loader,而且 b-loader 的返回值也是 pitch 返回的值
可以看出 pitch 提供了一个可以阻断loader链执行的方式。style-loader 就用到了 pitch,在 style-loader 中,是没有loader内容的,只有 loader.pitch
// style-loader
const loaderAPI = () => {};
loaderAPI.pitch = function loader(request) {
//
return someCode
}这里就很奇怪了,为什么style-loader阻断了loader(pitch 有返回值)执行,但是我们写的样式依旧可以生效!
这是因为在 style-loader 的返回值里面返回了 inline loader。然后执行 inline loader返回的css字符串直接写进的style标签,然后再插入 dom 中
// style-loader 返回内容中很重要的一段
// !! 禁用所有已配置的 loader
import content, * as namedExport from "!!css-loader-path!less-loader-path!./index.less";所以我们使用 less 时,实际的loader执行顺序是:
第一次执行

第二次是inline loader执行
并且第一次执行的时候是在webpack编译阶段,但是第二次执行则是在浏览器在加载 js 的时候执行。
为什么 style-loader 要这么设计
事实上 style-loader 这么设计是有原因的:
style-loader 会去操作 dom 元素(会给dom添加 style 标签)。但是在webpack编译阶段执行 loader 的过程中,是在 nodejs 环境,没有 dom 对象。所以才通过返回 inline loader执行,执行结果在通过 style-loader 处理添加到 dom 中。
loader开发
本地开发调试
通过在 rule 对象设置 path.resolve 指向这个本地 loader
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}匹配多个 loaders, 可以使用 resolveLoader.modules 配置
resolveLoader: {
modules: [
'node_modules',
// 本地 loaders 目录
path.resolve(__dirname, 'loaders')
]
}或者通过 npm link 来关联到需要测试的项目
loader API
this.getOptions
从
webpack 5开始,getOptions可以在loader上下文中使用。替代loader-utils中的getOptions。
获取loader参数
this.getOptions()this.data
与 pitch 共享的数据
this.callback
loader 是可以通过 return 来返回处理结果,但是如果需要返回多个结果时,需要使用到 callback API。
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any // webpack 会忽略,loader 之间可以传输信息
);this.async
异步loader
async function Loader(source) {
// 1. 获取异步回调函数
const callback = this.async();
let res;
try {
// 2. 调用less 将模块内容转译为 css
res = await SomeAsync();
} catch (error) {
// ...
}
callback(null, res);
}this.cachable
默认情况下,loader的结果都是被缓存的,传递 false 会让 loader 结果不缓存。
this.cacheable(flag = true: boolean)this.addDependency
添加依赖来监听,依赖项变化时,会重新编译生成缓存
this.addDependency(file: string)实现一个 txt-loader
// txt-loader.js
module.exports = function loader(source) {
return `module.exports = '${source}'`;
}// index.js
import Data from "./data.txt"
const msgElement = document.querySelector("#message");
msgElement.innerText = Data;实现极简 babel-loader
// simple-babel-loader.js
const core = require('@babel/core');
module.exports = function loader(source) {
const options = this.getOptions() || {};
const callback = this.async();
core.transform(source, options, function (err, result) {
if (err) {
callback(err);
} else {
callback(null, result.code);
}
});
}工具
- 需要了解 loader-runner
- loader-utils
- schema-utils(options 验证库)
- loader 内部 api
注意事项
当 loader 用来解析非 js 文件到 js 文件时,最后返回的代码是需要添加module.exports 或者 export default字符串,这样外界在导入模块的时候就可以接收到这个HTML字符串。
return `module.exports = ${JSON.stringify(someString)}`
// or
// 有 callback 就不需要 return
this.callback(null, `module.exports = ${JSON.stringify(someString)}`)


