webpack 在执⾏npx webpack进⾏打包后,都⼲了什么事情?
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = (installedModules[moduleId] = {
i: moduleId, l: false,
exports: {}
});
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
return __webpack_require__((__webpack_require__.s = "./index.js"));
})({
"./index.js": function(module, exports) {
eval(
'// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
sourceURL=webpack:///./index.js?'
),
"./a.js": function(module, exports) {
eval(
'// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
sourceURL=webpack:///./index.js?'
),
"./b.js": function(module, exports) {
eval(
'// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
sourceURL=webpack:///./index.js?'
);
}
});
⼤概的意思就是,我们实现了⼀个webpack_require 来实现⾃⼰的模块化,把代码都缓存在installedModules⾥,代码⽂件以对象传递进来,key是路径,value是包裹的代码字符串,并且代码内部的require,都被替换成了webpack_require
⾃⼰实现⼀个bundle.js
模块分析:读取⼊⼝⽂件,分析代码
1、创建一个bundle.js
const webpack = require("./lib/webpack.js");
const options = require("./webpack.config.js");
new webpack(options).run();
2、在src目录下创建3个例子文件。
index.js
import { a } from "./a.js";
console.log(`hello ${a} webpack bundle!`);
a.js
import { b } from "./b.js";
export const a = "yht" + b;
b.js
export const b = "!!!!";
3、新建lib文件夹,在该文件夹下创建webpack.js。
新建一个webpack类并实现导出。
class webpack {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = []; // 用于保存所有模块的信息
}
}
options的参数是从webpack.congfig.js中进行获得。
4、定义个run()函数作为入口函数。
主要分为三步:
1、分析入口。2、分析入口是否有依赖。3、编译入口模块的内容。
(1)模块分析:读取⼊⼝⽂件,分析代码。在run()函数中定义一个parse()进行模块的解析。
run()方法
const info = this.parse(this.entry);
parse()方法
(1)解析模块
安装@babel/parser
npm isntall @babel/parser -D
// 解析模块
const content = fs.readFileSync(entryFile, "utf-8");
const ast = parser.parse(content, { // 获取ast抽象语法树
sourceType: "module",
});
获得的ast抽象语法树,本质就是一个对象,里面有多个node节点。打印的ast如下:
(2)根据ast语法树解析出引入的路径,并进行依赖的保存。
安装@babel/traverse
npm install @babel/traverse -D
该插件是专门用来处理语法树的增删改查。需要的是,引入时需要加上default才能生效。
const travaerse = require("@babel/traverse").default;
const yilai = {};
travaerse(ast, {
ImportDeclaration({ node }) { //type:ImportDeclaration类型,进行操作
const newPathName =
"./" + path.join(
path.dirname(entryFile), // 分析根路径
node.source.value
);
//把依赖的新路径和旧路径都保存下来
yilai[node.source.value] = newPathName;
},
});
由此,我们保存好了依赖,yilai的对象数据内容如下:
(3)从ast中解析出代码。
使用@babel/core插件中transformFromAst接触处依赖中代码
npm install @babel/core -D
const { code } = transformFromAst(ast, null, { // 从ast中解析出代码
presets: ["@babel/preset-env"],
});
解析出来的code如下:
最后parse函数返回entryFile,yilai,code,三个值。
parse函数的完整代码:
parse(entryFile) {
// 解析模块
const content = fs.readFileSync(entryFile, "utf-8");
const ast = parser.parse(content, { // 获取ast抽象语法树
sourceType: "module",
});
const yilai = {};
travaerse(ast, {
ImportDeclaration({ node }) { //type:ImportDeclaration类型,进行操作
const newPathName =
"./" + path.join(
path.dirname(entryFile), // 分析根路径
node.source.value
);
//把依赖的新路径和旧路径都保存下来
yilai[node.source.value] = newPathName;
},
});
const { code } = transformFromAst(ast, null, { // 从ast中解析出代码
presets: ["@babel/preset-env"],
});
return {
entryFile,
yilai,
code,
};
}
3、接下来run()函数把得到的info信息保存再module数组中,并且对数组进行双层for循环实现伪递归。得到整体的module信息。
this.modules.push(info);
// 递归
// 用双层for循坏 遍历modules,达到递归的效果
// 方便理解
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const yilai = item.yilai;
if (yilai) { // 如果有依赖,对依赖进行遍历,递归调用parse进行code解析
for (let j in yilai) {
this.modules.push(this.parse(yilai[j]));
}
}
}
打印module得到的信息如下:
4、进行数组的转换得到依赖图谱,方便最后转变时更好的使用。
// 数据格式转换 arr to obj
const obj = {};
this.modules.forEach((item) => {
obj[item.entryFile] = {
yilai: item.yilai,
code: item.code,
};
});
最后的到obj对象如下图的格式:
5、输出bandle文件。
this.file(obj);
file函数如下:
file(obj) {
// 1. 生成bundle文件(需要从output配置字段拿到文件的存储位置和文件的名称)
const bundlePath = path.join(this.output.path, this.output.filename);
const newObj = JSON.stringify(obj);
const content = `(function(modules){
function require(module){
// ./a.js ---> 是否可以拿到这个模块的code?
function newRequire(relativePath){
// 就是把相对于入口模块的路径替换成相对根目录的路径
return require(modules[module].yilai[relativePath])
}
const exports = {};
(function(exports,require,code){
eval(code)
})(exports,newRequire,modules[module].code)
return exports;
}
require('${this.entry}')
})(${newObj})`;
fs.writeFileSync(bundlePath, content, "utf-8");
}
bundlePath:文件输出的位置。
content:是一个自执行函数。传入的参数需要用JSON.stringify进行序列化。否则获无法传入。自执行里面的reuire函数里输入了入口模块的路径。然后再、通过一个自执行函数,出入对应的code,而code里面的路径是相对于入口模块的路径。我们需要通过newRquire函数转换成相对于根目录的路径。eval执行完成以后会把exports返回到exports对象上面,并进行返回。