Loader

Loader 基本使用

loader 的本质是一个 node 模块,这个模块导出一个函数,用于将不同类型的文件转换为 webpack 可识别的模块。

loader 执行顺序

  1. 分类:pre 前置,normal 普通,inline 内联,post 后置。
  2. 执行优先级: pre > normal > inline > post
  3. 同类型执行顺序:从右向左,从下到上

使用前置、后置 loader:

1
2
3
4
5
6
7
{
test: /\.js$/,
use: {
loader: 'loader1'
},
enforce: 'pre' // (post)
}

使用内联 loader:

  • -! 不会让文件再通过 pre + normal loader 处理
  • ! 不再用 normal loader
  • !! 不用其他所有,只用行内

let str = require('!!inline-loader!./a.js')

Loader 开发

loader 组成

  1. loader 默认由两部分组成: pitchnormal

pitch 是 loader 上的一个方法,它的作用是阻断 loader 链。

1
2
3
4
5
// loader上的pitch方法,非必须
module.exports.pitch = function () {
console.log("pitching graph");
// todo
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
     //pitch 无返回值
pitch loader3 → loader2 → loader1

资源

normal loader3 ← loader2 ← loader1

// pitch loader - 有返回值
use: [loader3, loader2, loader1]
pitch loader3 → loader2 loader1

有返回值 资源

normal loader3 loader2 loader1

loader 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1 直接寻址
use: {
loader: path.resolve(__dirname, "loaders/loader1.js");
}

//2 设置别名
resolveLoader: {
alias: {
loader1: path.resolve(__dirname, "loaders/loader1.js");
}
}
use: {
("loader1");
}

//3 设置loaders所在文件夹
resolveLoader: {
modules: ["node_modules", path.resolve(__dirname, "loaders")];
}

开发 loader 必备工具

  1. loader-utils

常用的几个方法:

  • getOptions 获取 loader 的配置项。
    • loader 的配置就是这个:
  • interpolateName 处理生成文件的名字。
  • stringifyRequest 把绝对路径处理成相对根目录的相对路径。
  1. schema-utils

可以验证 loader option 配置的合法性:就像TS一样去约束其option的类型

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const loaderUtils = require("loader-utils");
const validate = require("schema-utils");
module.exports = function (source) {
// 获取 loader 配置项
let options = loaderUtils.getOptions(this) || {};
// 定义配置项结构和类型
let schema = {
type: "object",
properties: {
name: {
type: "string",
},
},
};
// 验证配置项是否符合要求
validate(schema, options);
return source;
};

loader 开发准则

官方说明

  • 简单易用。
  • 使用链式传递。
  • 模块化的输出。
  • 确保无状态。
  • 使用 loader utilities。
  • 记录 loader 的依赖。
  • 解析模块依赖关系。
  • 提取通用代码。
  • 避免绝对路径。
  • 使用 peer dependencies。

一些常用 loader 简易实现例子

loader 编写例子源码

babel-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let babel = require("@babel/core");
let loaderUtils = require("loader-utils");

function loader(source) {
// this: loaderContext
// loader上下文 默认有async这个方法 异步执行
let callback = this.async();

let options = loaderUtils.getOptions(this);
babel.transform(
source,
{
...options,
sourceMap: true,
// 从绝对路径取出文件名,否则在开发者工具source中为 unknown
filename: this.resourcePath.split("/").pop(),
},
function (err, result) {
callback(err, result.code, result.map);
// 返回:错误、代码、sourceMap
}
);
// return source; 使用异步回调时此处不再起作用
}

module.exports = loader;

file-loader & url-loader

  1. file-loader 作用:根据图片生成 md5,发射到 dist 目录下,file-loader 还会返回当前图片路径
1
2
3
4
5
6
7
8
9
10
11
12
let loaderUtils = require("loader-utils");
function loader(source) {
// 根据当前文件生成路径
let filename = loaderUtils.interpolateName(this, "[hash].[ext]", {
content: source,
});
// 发射文件
this.emitFile(filename, source);
return `module.exports="${filename}"`;
}
loader.raw = true; //二进制
module.exports = loader;
  1. url-loader 作用:根据设定的大小限制,选择将图片转换为 base64 或调用 file-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let loaderUtils = require("loader-utils");
let mime = require("mime");

function loader(source) {
let { limit } = loaderUtils.getOptions(this);
if (limit && limit > source.length) {
// 文件大小小于设定限制,转换为base64
return `module.exports="data:${mime.getType(
this.resourcePath
)};base64,${source.toString("base64")}"`;
} else {
return require("./file-loader").call(this, source);
}
}
loader.raw = true; //二进制
module.exports = loader;

less-loader & css-loader & style-loader

  1. less-loader
1
2
3
4
5
6
7
8
9
10
11
let less = require("less");

function loader(source) {
let css;
less.render(source, function (err, result) {
css = result.css;
});
return css;
}

module.exports = loader;
  1. css-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { match } = require("micromatch");

function loader(source) {
let reg = /url\((.+?)\)/g;
let pos = 0;
let current;
let arr = ["let list = []"];
while ((current = reg.exec(source))) {
let [matchUrl, g] = current;
// matchUrl:url('./avatar.jpg') g:'./avatar.jpg'
let last = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
pos = reg.lastIndex;
// g 替换成 require 写法
arr.push(`list.push('url('+require(${g})+')')`);
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`);
arr.push(`module.exports = list.join('')`);
return arr.join("\r\n");
}
return source;
}

module.exports = loader;
  1. style-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let loaderUtils = require("loader-utils");

// 作用:导出脚本
function loader(source) {
let str = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
return str;
}
// 在 style-loader 上写了pitch
// css-loader!less-loader!./index.less
loader.pitch = function (remainingRequest) {
// 剩余请求
// stringifyRequest将请求转换为可在require()或import中使用的字符串,同时避免使用绝对路径。
// require 路径,返回的是css-loader处理后的结果 require('!!css-loader!less-loader!index.less)
let str = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(
this,
"!!" + remainingRequest
)});
document.head.appendChild(style);
`;
return str;
};
module.exports = loader;

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!