编写一个 webpack 的 bundle
手写 bundle 能够帮助我们对 webpack 的打包流程有个比较清晰的认识,对于 webpack 的学习有着很大的帮助,接下来跟着文章来手写一个 bundle 吧
一、初始化项目
在根目录下创建初始文件夹 src,创建三个文件index.js,message.js,word.js,三个文件有着简单的调用关系。这时候我们执行index.js肯定是不成的,因为它无法识别 import
//index.js
import { message } from "./message.js";
console.log(message);
//message.js
import { word } from "./word.js";
export const message = `hello ${word}`;
//word.js
export const word = "world";
执行 index.js ,import不能如期执行:
二、获取模块内容
要想这段代码正常运行,我们需要在根目录下编写一个 bundler,来帮助我们进行打包分析 首先要从入口文件开始分析
// 帮我们做文件分析
const fs = require("fs");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8");
console.log(content);
};
// 我们要分析哪个入口文件
moduleAnalyser("./src/index.js");
在控制台执行查看输出,可以看到已经拿到这个入口文件了
觉得黑色不太好看的话,我们可以执行npm install cli-highlight -g,这是一高亮显示代码的工具
再执行一次我们的代码
三、获取模块依赖
在拿到模块内容后,我们需要对模块依赖进行处理,这里我们可以用字符串分割来获取 import 到的模块依赖,但这样就太不方便了
我们可以使用 babel 提供的工具@babel/parser帮助我们进行分析
npm install @babel/parser --save
// 帮我们做文件分析
const fs = require("fs");
const parser = require("@babel/parser");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8");
console.log(
parser.parse(content, {
sourceType: "module",
})
);
};
// 我们要分析哪个入口文件
moduleAnalyser("./src/index.js");
观察打印结果
实际上这个打印结果就是我们常说的抽象语法树 AST
对bundler进行修改
const ast = parser.parse(content, {
sourceType: "module",
});
console.log(ast.program.body);
观察打印结果,我们的入口文件有两个节点,第一个是ImportDeclaration引入语法,第二个节点是ExpressionStatement,也就是一个表达式
抽象语法树很好的帮助我们把代码转化为了一个对象,拿到对象后应该干嘛呢?对了,应该对对象进行分析了
四、分析 AST,收集依赖
我们使用traverse帮助我们进行分析
npm install @babel/traverse --save
修改我们的代码,拿到导入文件的绝对路径和相对路径
// 帮我们做文件分析
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const path = require("path");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8");
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
traverse(ast, {
// 对import引入的节点执行下面的方法
ImportDeclaration({ node }) {
// 这里是要拿到绝对路径
const dirname = path.dirname(filename);
// node.source.value是一个相对路径
const newFile = "./" + path.join(dirname, node.source.value);
// 为了方便我们后续处理,相对路径和绝对路径我们都需要
dependencies[node.source.value] = newFile;
},
});
console.log(dependencies);
};
// 我们要分析哪个入口文件
moduleAnalyser("./src/index.js");
执行结果:
至此我们就分析出了依赖的路径,将它和入口文件名一起 return 出去
...
console.log(dependencies);
return {
filename,
dependencies
}
...
五、将导入模块转为 ES5 语法
npm install @babel/core --save
npm install @babel/preset-env --save
使用 babel 的工具对我们入口文件的代码进行处理转为 ES5 语法
// 帮我们做文件分析
...
const babel = require("@babel/core");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8");
const ast = parser.parse(content, {
sourceType: "module"
});
...
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return {
filename,
dependencies,
code
}
}
// 我们要分析哪个入口文件
const moduleInfo = moduleAnalyser("./src/index.js");
console.log(moduleInfo);
执行结果:
对比index.js,到这为止我们就成功分析了入口文件的代码
六、递归分析所有依赖
上一步我们分析了入口文件,将入口文件的代码转为ES5语法,并拿到了他的依赖,但还没对导入的依赖进行分析,那要怎么获取一个模块里面所有依赖模块的信息呢?
在这里,我们可以使用递归来分析所有依赖
// 递归获取所有依赖模块信息
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
// 依赖数组
const graphArray = [entryModule];
// 对依赖数组进行遍历
for (let i = 0; i < graphArray.length; i++) {
// 拿到当前遍历到的依赖的所有依赖
const { dependencies } = graphArray[i];
// 对拿到的依赖做分析,将结果加入依赖数组
if (dependencies) {
for (const key in dependencies) {
graphArray.push(moduleAnalyser(dependencies[key]));
}
}
}
// 对结果进行处理
const graph = {};
graphArray.forEach((item) => {
// 文件名
graph[item.filename] = {
// 此文件的依赖
dependencies: item.dependencies,
// 此文件的代码
code: item.code,
};
});
return graph;
};
添加递归后我们的 bundler:
// 帮我们做文件分析
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const path = require("path");
const babel = require("@babel/core");
// 分析一个模块
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8");
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
traverse(ast, {
// 对引入语法的节点执行下面的方法
ImportDeclaration({ node }) {
// 这里是要拿到绝对路径
const dirname = path.dirname(filename);
// node.source.value是一个相对路径
const newFile = "./" + path.join(dirname, node.source.value);
// 为了方便我们后续处理,相对路径和绝对路径我们都需要
dependencies[node.source.value] = newFile;
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return {
filename,
dependencies,
code,
};
};
// 递归获取所有依赖模块信息
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {
const { dependencies } = graphArray[i];
if (dependencies) {
for (const key in dependencies) {
graphArray.push(moduleAnalyser(dependencies[key]));
}
}
}
// 对数组结果进行处理,方便我们之后的操作
const graph = {};
graphArray.forEach((item) => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code,
};
});
return graph;
};
// 我们要分析哪个入口文件
const graghInfo = makeDependenciesGraph("./src/index.js");
console.log(graghInfo);
这样我们就通过一个入口文件,拿到了所有依赖模块的信息:
七、生成能在浏览器真正运行的代码
注意看我们拿到的依赖对应的执行代码
我们浏览器是不能直接执行这些代码的,因为它识别不出require和exports,我们要对这些代码做处理,这也是一个迭代的过程
const generateCode = (entry) => {
// 将递归得到的依赖转为JSON
const graph = JSON.stringify(makeDependenciesGraph(entry));
// 返回一个立即执行函数,传入依赖,分析依赖并执行对应的代码片段
return `
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}');
})(${graph})
`;
};
// 我们要分析哪个入口文件
const code = generateCode("./src/index.js");
console.log(code);
运行代码,得到执行结果code,将执行结果复制到浏览器控制台中,查看输出结果
能看到我们 bundler 生成的代码可以被浏览器正确执行~,至此我们就成功手写了一个bundle帮助我们进行打包。但webpack要做的事肯定是比这多得多,了解了这些核心步骤后,再去深入学习webpack会容易一些。