之前了解到webpack的打包和编译原理,今天又看到了webpack中loader的部分原理,借此机会分享给大家
loader是做什么的?
webpack中loader是用来转换代码的,即将源代码从一种代码转换为另一种
所以loader的写法也就呼之欲出,即如下代码:
module.exports = function loader(code) {
// some transfrom operation... and return resultCode
return resultCode;
}
loader基本配置
下面展示的是webpack中loader的基本配置之一,若不了解配置则需要去看webpack中loader的配置规则
module.exports = {
...,
module: {
rules: [
{
test: /\.css/, // 正则表达式作为loader的匹配规则
use: [
{
loader: "", // loader路径,和require的规则类似
options: { // loader参数
...,
}
}
]
}
]
}
}
loader打包执行顺序
我们都知道了loader是干嘛的,所以loader对于源代码的操作是有序的,具体怎么个有序法呢?我们可以来测试一下 如下代码
// webpack.config.js
const path = require("path");
const outputPath = path.join(__dirname, "dist");
module.exports = {
mode: "development",
entry: {
index: path.join(__dirname, "src", "index.js")
},
output: {
path: outputPath,
filename: "[name].js",
clean: true
},
module: {
rules: [
{
test: /index.js$/,
use: [
"./loaders/loader1",
"./loaders/loader2",
]
},
{
test: /.js$/,
use: [
"./loaders/loader3",
"./loaders/loader4",
]
}
]
}
}
// loaders/loader1.js
module.exports = function (code) {
console.log("loader1");
return code;
}
// loaders/loader2.js
module.exports = function (code) {
console.log("loader2");
return code;
}
// loaders/loader3.js
module.exports = function (code) {
console.log("loader3");
return code;
}
// loaders/loader4.js
module.exports = function (code) {
console.log("loader4");
return code;
}
这段代码执行命令npx webpack
之后,执行结果如下:
由此可见loader执行的时候是从后往前执行的
这个时候我们在src/index,js
增加如下内容,并在src目录下新增a.js,路径为src/a.js
,不用填入内容
// src/index.js
require("./a");
此时我们再执行npx webpack
,webpack的loader输出结果是什么呢?
所以webpack的loader执行时,首先对入口文件进行loader方法的依次执行,然后再对其依赖文件进行loader方法的依次执行,而且对于其依赖也会执行loader的匹配规则进行匹配。
loader打包和AST语法树建立先后顺序
修改src/index.js
为如下代码
// src/index.js
require("./a");
变量 a = 1;
再次执行npx webpack
,发现webpack不能打包,且报错。
接下来我们修改loader/loader4.js
代码,如下:
// loaders/loader4.js
module.exports = function (code) {
console.log("loader4");
const { changeVar } = this.getOptions();
const reg = new RegExp(changeVar, "g");
return code.replace(reg, "var");
}
修改webpack.config.js
代码中对于loader4
的loader配置如下:
module.exports = {
...,
module: {
rules: [
...,
{
test: /.js$/,
use: [
"./loaders/loader3",
{
loader: "./loaders/loader4", // loader:将源代码转换成另外的源代码,运行于构建AST树之前
options: {
changeVar: "变量"
}
},
]
}
]
}
}
接下来再执行npx webpack
,执行结果如下:
如上我们得到了输出,并且不报错。说明loader打包的执行顺序比AST语法树的建立要早,所以有如下图: webpack打包流程:
一些例子
简易的css-loader
新增loader/style-loader.js
// loaders/style-loader
const path = require("path");
const loaderUtil = require("loader-utils");
module.exports = function (buffer) {
const {embed} = this.getOptions();
if (embed) {
return exportEmbedStyle(this, buffer);
}
return exportFileStyle(this, buffer);
}
function exportFileStyle(context, buffer) {
const {filePath, publicPath} = context.getOptions();
const filename = getFileName(context, buffer);
const fullPath = path.join(filePath, filename);
const relativePath = path.relative(publicPath, fullPath).replace(/\\/g, "/");
return `const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "${relativePath}";
document.head.appendChild(link);
module.exports = "${relativePath}"`;
}
function getFileName(context, buffer) {
const ext = path.extname(context.resourcePath);
const _name = path.basename(context.resourcePath);
const filename = _name.slice(0, _name.length - ext.length);
const hash = loaderUtil.interpolateName(context, "[contenthash]", {
content: buffer
});
const fullName = filename + "." + hash + ext;
context.emitFile(fullName, buffer);
return fullName;
}
function exportEmbedStyle(context, buffer) {
const code = buffer.toString();
return `const style = document.createElement("style");
style.innerHTML = `${code}`;
document.head.appendChild(style);
module.exports = `${code}``;
}
module.exports.raw = true; // 让源文件以buffer的形式传入
修改webpack配置:
const outputPath = path.join(__dirname, "dist");
const publicPath = path.join(__dirname, "public");
module.exports = {
...,
module: {
rules: [
...,
{
test: /.css$/,
use: [
{
loader: "./loaders/style-loader",
options: {
filePath: outputPath,
publicPath: publicPath,
embed: false
}
}
]
}
]
}
}
简易的img-loader
新增loaders/img-loader.js
// loaders/img-loader.js
const path = require("path");
const loaderUtil = require("loader-utils");
function loader(buffer) {
console.log(this.context);
const { limit } = this.getOptions();
if (buffer.byteLength <= limit) {
return exportBase64(this, buffer);
}
return exportFile(this, buffer);
}
function exportFile(context, buffer) {
const filename = getFilePath(context, buffer);
const { filePath, publicPath } = context.getOptions();
const fullPath = path.join(filePath, filename);
const relativePath = path.relative(publicPath, fullPath).replace(/\\/g, "/");
return `module.exports = "${relativePath}"`;
}
function exportBase64(context, buffer) {
const ext = path.extname(context.resourcePath).slice(1);
const content = base64Image(buffer, ext);
return `module.exports = `${content}``;
}
function getFilePath(context, buffer) {
const ext = path.extname(context.resourcePath);
const _name = path.basename(context.resourcePath);
const filename = _name.slice(0, _name.length - ext.length);
const hash = loaderUtil.interpolateName(context, "[contenthash]", {
content: buffer
});
const fullName = filename + "." + hash + ext;
context.emitFile(fullName, buffer);
return fullName;
}
function base64Image(buffer, ext) {
let base64String = buffer.toString("base64");
return `data:image/${ext};base64,${base64String}`;
}
loader.raw = true;
module.exports = loader;
修改wenpack配置:
const outputPath = path.join(__dirname, "dist");
const publicPath = path.join(__dirname, "public");
module.exports = {
...,
module: {
rules: [
...,
{
test: /.(png|jpg|gif)$/gi,
use: [
{
loader: "./loaders/img-loader",
options: {
limit: 1024 * 100, // 10KB以上使用路径,100KB一下使用base64
filePath: outputPath,
publicPath: publicPath
}
}
]
}
]
}
}
更多loader上下文见: Loader Interface | webpack
综上就是我对于laoder打包修改代码时的原理理解和部分应用,欢迎大佬指正