Webpack:手写自定义loader
1.loader执行顺序
1.1 loader的分类
- pre: 前置 loader
- normal: 普通 loader
- inline: 内联 loader
- post: 后置 loader
1.2 loader的执行顺序
- 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",
},
{
// 没有enforce就是normal
test: /\.js$/,
loader: "loader2",
},
{
enforce: "post",
test: /\.js$/,
loader: "loader3",
},
],
},
1.3 使用loader的方式
- 配置方式:在
webpack.config.js
文件中指定 loader。(pre、normal、post loader) - 内联方式:在每个
import
语句中显式指定 loader。(inline loader)
1.4 inline loader
用法:import Styles from 'style-loader!css-loader?modules!./styles.css';
含义:
- 使用
css-loader
和style-loader
处理styles.css
文件 - 通过
!
将资源中的 loader 分开
inline loader
可以通过添加不同前缀,跳过其他类型 loader。 !
跳过 normal loader。
import Styles from '!style-loader!css-loader?modules!./styles.css';
-!
跳过 pre 和 normal loader。
import Styles from '-!style-loader!css-loader?modules!./styles.css';
!!
跳过 pre、 normal 和 post loader。
import Styles from '!!style-loader!css-loader?modules!./styles.css';
2.开发一个最简单的loader
2.1 项目准备
项目目录结构如下:
webpack.config.js
内容如下:
const path=require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry:"./src/main.js",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].js',
clean:true
},
module:{
rules:[
]
},
plugins:[
new HtmlWebpackPlugin({
template: path.resolve(__dirname,"public/index.html")
}),
],
//暂时指定为开发模式,因为开发模式不会压缩便于观察打包后的代码信息
mode:"development",
}
src/main.js
内容如下:
console.log("hello main!!!")
安装一下依赖:
npm install webpack webpack-cli html-webpack-plugin -D
安装好依赖后多出两个包管理文件:
通过npx webpack
打包:
2.2 最简单的loader
新建一个loader
文件:
//`loaders/test-loader.js`:
/*
loader就是一个函数
当webpack解析资源时,会调用相应的loader去处理
loader接受到文件内容作为参数,返回内容出去
content 文件内容
map SourceMap
meta 别的loader传递的数据
*/
module.exports = function loader1(content) {
console.log("hello loader");
return content.toUpperCase();
};
配置webpack.config.js
:
module:{
rules:[
{
test:/\.js$/,
loader: "./loaders/test-loader.js"
}
]
},
然后打包npx webpack
:
查看dist/js
下的js
文件:
2.3 loader接受的参数
content
源文件的内容map
SourceMap 数据meta
数据,可以是任何内容
3.loader分类
3.1 同步loader
//如果当前资源只配置了这一个loader,可以用这种方式
module.exports = function (content, map, meta) {
return content;
};
this.callback
方法则更灵活,因为它允许传递多个参数,而不仅仅是 content
。
module.exports = function (content, map, meta) {
/*
第一个参数:err 代表是否有错误
第二个参数:content 处理后的内容
第三个参数:source-map 继续传递source-map
第四个参数:meta 给下一个loader传递参数
*/
this.callback(null, content, map, meta);
// 同步loader中不能进行异步操作
// setTimeout(() => {
// console.log("test1");
// this.callback(null, content, map, meta);
// }, 1000);
};
3.2 异步loader
module.exports = function (content, map, meta) {
const callback = this.async();
// 进行异步操作
setTimeout(() => {
callback(null, result, map, meta);
}, 1000);
};
由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。
3.3 Raw Loader
默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。
loaders/raw-loader.js
module.exports = function (content) {
// content是一个Buffer数据
console.log(content)
return content;
};
module.exports.raw = true; // 开启 Raw Loader
配置一下webpack
配置文件:
{
test:/\.js$/,
loader: "./loaders/raw-loader.js"
}
打包项目:
3.4 Pitching Loader
module.exports = function (content) {
return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("do somethings");
};
webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。
下面来举个例子:
test-loaders/test1.js
:
module.exports = function (content) {
console.log('normal loader 1');
return content;
};
module.exports.pitch = function () {
console.log("pitch loader 1");
};
test-loaders/test2.js
:
module.exports = function (content) {
console.log('normal loader 2');
return content;
};
module.exports.pitch = function () {
console.log("pitch loader 2");
};
test-loaders/test3.js
:
module.exports = function (content) {
console.log('normal loader 3');
return content;
};
module.exports.pitch = function () {
console.log("pitch loader 3");
};
配置webpack
文件:
{
test:/\.js$/,
use: ["./test-loaders/test1.js","./test-loaders/test2.js","./test-loaders/test3.js"]
}
打包项目:
在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。
假如我们在test2.js
中的pitch
加一个返回值:
module.exports = function (content) {
console.log('normal loader 2');
return content;
};
module.exports.pitch = function () {
console.log("pitch loader 2");
return "result";
};
打包项目:
4.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) |
其它方法详细讲官方文档。
5.开发自定义loader
5.1 手写clean-log-loader
作用:用来清理 js 代码中的console.log
//clean-log-loader/clean-log-loader.js
module.exports = function cleanLogLoader(content) {
// 将console.log替换为空
//`\`为转义符
//`?`为可选
//g表示全局
return content.replace(/console\.log\(.*\);?/g, "");
};
配置webpack
文件:
{
test:/\.js$/,
loader: "./clean-log-loader/clean-log-loader.js"
}
打包项目:
5.2 手写banner-loader
作用:给 js 代码添加文本注释
//banner-loader/index.js
const schema = require("./schema.json");
module.exports = function (content) {
// 获取loader的options,同时对options内容进行校验
// schema是options的校验规则(符合 JSON schema 规则)
const options = this.getOptions(schema);
const prefix = `
/*
* Author: ${options.author}
*/
`;
return `${prefix} \n ${content}`;
};
//banner-loader/schema.json
{
"type": "object",
"properties": {
"author": {
"type": "string"
}
},
//是否允许追加属性
"additionalProperties": false
}
配置webpack
文件:
{
test: /\.js$/,
loader: "./banner-loader/index.js",
options: {
author: "老王",
//age: 18, // 不能新增字段,不然会报错
},
},
打包项目:
5.3 手写babel-loader
作用:编译js代码,将 ES6+语法编译成 ES5-语法。
在此之前我们需要了解babel是什么,可以看我写的文章https://blog.csdn.net/fageaaa/article/details/146023275。另外还可以点击这儿查看中文说明文档。在这里面可以查看相关工具的用法。如下:
先要下载依赖:
npm i @babel/core @babel/preset-env -D
//babel-loader/index.js
const schema = require("./schema.json");
const babel = require("@babel/core");
module.exports = function (content) {
const options = this.getOptions(schema);
// 使用异步loader
const callback = this.async();
// 使用babel对js代码进行编译
babel.transform(content, options, function (err, result) {
callback(err, result.code);
});
};
//banner-loader/schema.json
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
配置webpack
:
{
test: /\.js$/,
loader: "./babel-loader/index.js",
options: {
presets: ["@babel/preset-env"],
},
},
修改一下src/main.js
文件:
console.log("hello main!!!")
let a=11
打包项目:
5.4 手写file-loader
作用:将文件(如图片、字体)原封不动输出出去。在webpack中通过type:asset/resource
实现。
file-loader
可以让打包文件如下:
总结:
file-loader
可以根据文件内容生成一个hash
值的文件名称- 把该文件名输出到地址目录
return
一个module.exports=“路径/hash值”
安装一个工具:
npm i loader-utils -D
//file-loader/index.js
const loaderUtils = require("loader-utils");
module.exports = function (content) {
// 1. 根据文件内容生成带hash值文件名
let interpolatedName = loaderUtils.interpolateName(this, "[hash].[ext][query]", {
content,
});
interpolatedName = `images/${interpolatedName}`
// console.log(interpolatedName);
// 2. 将文件输出出去
this.emitFile(interpolatedName, content);
// 3. 返回:module.exports = "文件路径(文件名)"
return `module.exports = "${interpolatedName}"`;
};
// 需要处理图片、字体等文件。它们都是buffer数据
// 需要使用raw loader才能处理
module.exports.raw = true;
配置webpack
文件:
{
test: /\.(png|jpe?g|gif)$/,
loader: "./file-loader/index.js",
//这个地方如果不设置,webpack默认会用type:asset/resource来处理非js的资源
//也就是会多生成3张图片
type: "javascript/auto", // 阻止webpack默认处理图片资源,只使用file-loader处理
},
然后我们需要在项目中用到图片,准备一下图片:
新建index.css
:
//src/css/index.css
.box1 {
width: 100px;
height: 100px;
background-image: url("../images/1.jpeg");
}
.box2 {
width: 100px;
height: 100px;
background-image: url("../images/2.png");
}
.box3 {
width: 100px;
height: 100px;
background-image: url("../images/3.gif");
}
在主文件引入样式:
import "./css/index.css";
而css
的打包加载需要用到css-loader
、style-loader
:
npm i css-loader style-loader -D
给css
提交规则:
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
然后给项目添加一些html
结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack</title>
</head>
<body>
<div class="box1"></div>
<div class="box2"></div>
<div class="box3"></div>
</body>
</html>
打包项目:
通过vscode
打开项目:
5.5 手写style-loader
作用:动态创建style标签,插入js中的样式代码,使样式生效。
关于style-loader
的详细解释参见我写的文章。
//style-loader/index.js
module.exports = function (content) {
/*
1. 直接使用style-loader,只能处理样式
不能处理样式中引入的其他资源
use: ["./loaders/style-loader"],
*/
const script = `
const styleEl = document.createElement('style');
styleEl.innerHTML = ${JSON.stringify(content)};
document.head.appendChild(styleEl);
`;
return script;
};
然后只添加style-loader
规则:
{
test: /\.css$/,
// use: ["style-loader", "css-loader"],
use: ["./style-loader/index.js"],
},
打包项目运行打包文件index.html
发现界面空白:
原因是直接使用style-loader,只能处理样式不能处理样式中引入的其他资源。
把css-loader
添加进去:
{
test: /\.css$/,
// use: ["style-loader", "css-loader"],
// use: ["./style-loader/index.js"],
use: ["./style-loader/index.js","css-loader"],
},
再次打包项目运行打包文件index.html
发现界面还是空白:
出现的问题是css-loader暴露了一段js代码,style-loader需要执行js代码,得到返回值,再动态创建style标签,插入到页面上
我们需要使用pitch loader用法:
//style-loader/index.js
module.exports = function (content) {
};
module.exports.pitch = function (remainingRequest) {
// remainingRequest 剩下还需要处理的loader
// console.log(remainingRequest);
//C:\前端\webpack5\手写loader\my-loader\node_modules\css-loader\dist\cjs.js!C:\前端\webpack5\手写loader\my-loader\src\css\index.css
// 1. 将 remainingRequest 中绝对路径改成相对路径(因为后面只能使用相对路径操作)
const relativePath = remainingRequest
.split("!")
.map((absolutePath) => {
// 返回相对路径
return this.utils.contextify(this.context, absolutePath);
})
.join("!");
//console.log(relativePath); // ../../node_modules/css-loader/dist/cjs.js!./index.css
// 2. 引入css-loader处理后的资源
// 3. 创建style,将内容插入页面中生效
//这里有两个`!`,`!!`会跳过 pre、 normal 和 post loader。
const script = `
import style from "!!${relativePath}";
const styleEl = document.createElement('style');
styleEl.innerHTML = style;
document.head.appendChild(styleEl);
`;
// 中止后面loader执行
return script;
};
我们打包运行,这次是正常运行。