js怎么判断⼀个标签⾥⾯有没有⼀个标签_由浅及深实现⼀个
⾃定义loader
由浅及深实现⼀个⾃定义loader
webpack系列⽂章
1. 实现⼀个简易的模块打包器
前⾔
在之前的⽂章中,我们已经实现了⼀个[简易的webpack打包器],但是在⽂章的最后,我们也提到过我们的打包器功能并不完善,⽐如我们⽬前不⽀持内置模块的引⼊,不⽀持ES6语法的转换,不⽀持css⽂件的打包。但是这些功能都可以通过webpack提供的loader和plugin等进⾏处理。这⼀篇⽂章我们就来聊⼀聊webpack中的核⼼功能之loader。还是我之前提到过的理念,你想要真正地了解⼀个东西,最好的办法就是去实现它,哪怕功能是最简单的。因此,本⽂我们同样会基于这个思路,⼿动去实现⼀个loader。
在[简易的webpack模块打包器]中,我们还不⽀持处理css⽂件。因此,⾸先我们先实现这个功能。为了便于⼤家直接使⽤代码,这⾥直接把之前的代码贴在下⾯。
const fs = require("fs");
const path = require("path");
// 获取依赖
function getDependencies(str) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while ((result = (str))) {
dependencies.push(result[1]);
}
return dependencies;
}
let ID = 0;
// 将每个模块转成对象描述形式
function createAsset(filename) {
// readFileSync  读取⽂件最好传递绝对路径css怎么创建
let fileContent = fs.readFileSync(filename, "utf-8");
const id = ID++;
return {
id: id,
filename: filename,
dependencies: getDependencies(fileContent),
code: `function(require,exports,module){${fileContent}
}`,
};
}
// 解析所有的模块得到⼀个⼤的数组对象。
function createGraph(filename){
let asset = createAsset(filename);
let queue = [asset];
// 使⽤let of 进⾏遍历,是因为我们在遍历过程中会往数组中添加元素,⽽let of会继续遍历新添加的元素,⽽不需要像for循环那样,需要进⾏处理。
for(let asset of queue){
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname,relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
})
}
return queue;
}
function createBundle(graph){
let modules = "";
graph.forEach((mod) => {
modules += `${mod.id}:[${de},${JSON.stringify(mod.mapping)}
],`;
});
const result = `(function(modules){
function exec(id) {
let [fn, mapping] = modules[id];
let exports = {};
fn && fn(require, exports);
function require(path) {
console.log("exports:",exports);
return exec(mapping[path]);
}
console.log("exports:", exports);
return exports;
}
exec(0);
})({${modules}})`;
// 看这⾥,看这⾥,看这⾥修改打包后的地址
fs.writeFileSync("./dist/bundle.js",result);
}
/
/ 看这⾥,看这⾥,看这⾥你可以传⼊你⾃⼰的⼊⼝⽂件
let graph = createGraph("./index.js");
createBundle(graph);
⼤家拿到上⾯的代码之后,如果你想要⾃⼰进⾏测试,只需要修改⼊⼝⽂件和最终的打包⽂件地址即可。也就是下⾯这两⾏代码。
let graph = createGraph("./index.js");
fs.writeFileSync("./dist/bundle.js",result);
当然,⼤家也可以去我的github仓库中进⾏查。step-7.js就是之前最终的代码,也是我们本节课的初始代码。
⽀持打包css⽂件
我们⼀直都知道模块打包器只⽀持打包js⽂件,打包其他⽂件需要使⽤对应的loader,但是⼤家有没有想过到底是为什么不⽀持了?明明我的模块打包器也能够加载这些⽂件啊。实践出真知,最好的办法
就是去试⼀下。因此,我们⾸先编写⼀个style.css⽂件,⾥⾯只写如下的简单样式,然后在其他⽂件中进⾏引⼊。
body{
background:red;
}
然后,我们查看⼀下打包的样⼦:
3: [
function (require, exports, module) {
body {
background: red;
}
},
{}
]
我们可以发现,打包后变成这样了,也就是跟其他的js⽂件⼀样,直接讲css样式,包裹在⼀个函数中了。这些css⽂件内容肯定不能作为js 的函数的内容进⾏执⾏,原来这就是只⽀持打包js⽂件的原因(因为所有的内容最后都会放到js函数中来执⾏,⾮js内容不能执⾏,因此就不能打包)。既然我们知道了⾮js内容不能执⾏,那么我们可不可以再进⼀步把加载的内容变成js⽀持的。⽐如我们能不能⽤⼀个变量来接收这些内容,这样不就是⼀个普通的js语句了嘛。类似于如下:
3: [
function (require, exports, module) {
const str = `body {  // 看这⾥,看这⾥
background: red;
}`
},
{}
]
根据上⾯的思路,我们拿到⾮js的⽂件,就给个变量来接收⼀下它。这⾥我们就处理⼀下css后缀的⽂件。
function createAsset(filename) {
let fileContent = fs.readFileSync(filename, "utf-8");
// 处理⼀下css⽂件
if (/\.css$/.test(filename)) {
console.log("说明是css⽂件")
fileContent = `
const str = ${JSON.stringify(fileContent)};
`
}
}
如上⾯代码所⽰,我们在createAsset函数中,拿到⽂件名之后先判断是不是css⽂件,如果是就通过⼀个变量来接收它,然后导出这个变量。我们看下最终打包后的效果。
3: [
function (require, exports, module) {
// 打包后的代码
const str = "body{\r\n    background:red;\r\n}";
},
{}
],
我们可以看到,打包后就是正常的js语句了,⽽且直接通过exports导出,需要使⽤时,直接引⼊即可。**好了,到⽬前为⽌我们已经实现了能够加载.css后缀的⽂件了。但是我们拿到css之后怎么使⽤了,我们平常使⽤样式,通常是⾏内使⽤,通过link引⼊和style标签使⽤。这⾥我们没法通过⾏内和link标签进⾏使⽤。但是我们可以创建⼀个style标签,然后把标签的内容替换为刚刚导出的css⽂件内容。代码如下:
if (/\.css$/.test(filename)) {
console.log("说明是css⽂件")
fileContent = `
const str = ${JSON.stringify(fileContent)};
if (document) {
//看这⾥,看这⾥创建style标签,然后插⼊到head中
const style = ateElement('style');
style.innerHTML = str;
document.head.appendChild(style);
}
`
}
然后我们将打包后的⽂件放⼊⼀个Html⽂件中,在浏览器中打开就可以看到整个背景都变成红⾊了。也就是说我们实现了我们想要的功能。接下来我们把我们这部分代码,单独抽离出来作为⼀个函数进⾏导出:loader/css.js
const processCss = function (fileContent) {
return `
const str = ${JSON.stringify(fileContent)};
if (document) {
const style = ateElement('style');
style.innerHTML = str;
document.head.appendChild(style);
}
`
}
将上⾯的函数在之前的⽂件中引⼊使⽤:
if (/\.css$/.test(filename)) {
fileContent = process(fileContent)
}
好了,到⽬前为⽌我们已经⽀持打包css⽂件了。事实上这就是css-loader和style-loader实现的功能。可能⼤家会觉得奇怪,我们只是写了⼀个process函数,代码还不到⼗⾏,这就是⼀个loader?答案是肯定的,这就是⼀个loader,只是我们的loader不太标准,webpack定义的loader需要遵循单⼀功能原则,也就是⼀个loader只实现⼀个功能,这⾥我们的loader实现了两个功能:1. 处理css⽂件 2. 将css⽂件插⼊到style标签中。⽽且,还有更加简单的loader了,官⽅推荐的row-loader核⼼代码就⼀⾏。如下图所⽰:
虽然我们的loader不正规,但是我们把对loader的理解⼀下⼦从很⾼深的概念抽象为⼀个函数,即loader就是⼀个简单的函数。
⾃定义⼀个loader
上⾯说了这么多,最终只是为了得到⼀个结论:loader是⼀个简单的函数。所以如果你们不愿意看上⾯的代码,那么记住这个结论也⾏。接下来我们就实现⼀个⾃定义的loader。我们实现⼀个替换⽂件中姓名和年龄的loader。
分别有如下⽂件:
name.js
export const name = "⼩明";
age.js
export const age = 18;
index.js
import { name} from "./name.js";
import {age} from "./age.js";
function showInfo(){
console.log(`${name}的年龄是${age}岁`);
}
showInfo();
如果正常运⾏代码,最终的输出应该是: