一、分析打包生成的文件
打包生成的文件内容分为两大部分:固定模板和依赖图谱(模块路径和相应的chunk)
**固定模板(打补丁)**是定义一些webpack中用到的方法,基本所有打包文件都会有,内容也一样。
重点在于依赖图谱,依赖图谱包括模块路径和相应的chunk。
我们现在要模拟webpack根据文件和配置,生成一个打包文件(bundle文件),所以我们首先要解决如何获取模块路径和生成相应的chunk这两个问题。
我们可以根据配置中的entry配置项知道从哪个模块开始执行打包任务,打包任务最核心的工作就是编译模块。
获取模块后要做的事情就是查看是否有依赖?获取依赖的路径和模块的编译,输出chunk。
二、创建基础模板
整体项目目录:
- 新建项目,项目中新建src文件夹。添加index.js和other.js。
index.js:
import { str } from "./other.js";
console.log(str);
other.js:
export const str = "bundle";
import { a } from "./a.js";
export const str = "bundle" + a;
a.js:
export const a = "a";
- 项目根目录创建webpack.config.js:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist")
},
mode: "development",
plugins: [new HtmlWebpackPlugin()]
};
- 项目根目录创建lib文件夹,新建webpack.js:
module.exports = class Webapck {
constructor(options) {
// 读取配置文件信息
console.log(options);
this.entry = options.entry;
this.output = options.output;
}
// 入口函数:实现编译
run() {
this.parser(this.entry);
}
/** 编译函数:实现具体编译
* 分析是否有依赖,获取依赖路径
* 编译模块生成chunk
*/
parser() {}
};
- 项目根目录创建bundle.js:
const webpack = require("./lib/webpack.js");
const config = require("./webpack.config.js");
// 创建webpack实例并将配置传入实例,执行编译
new webpack(config).run();
三、实现具体编译
- 使用编译函数来实现具体编译。
- 编译函数的实现:
- 分析是否有依赖,获取依赖路径。
- 编译模块生成chunk
- 返回:模块路径、模块依赖、chunk
- 思路:
- 分析是否有依赖,要找到文件中的引入依赖的语句,用
@babel/parser将文件内容解析成AST树,当语句的节点类型为“ImportDeclaration”时说明是import依赖,依赖路径就存在于节点的source.value字段中。使用@babel/traverse对 节点类型为“ImportDeclaration”的语句 进行操作,获取到依赖路径并存储起来。 - 编译模块生成chunk:使用
@babel/core插件中的transformFromAst将ast树转化为js。
- 整体代码:
const fs = require("fs");
const path = require("path");
const BabelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
module.exports = class Webapck {
constructor(options) {
// 读取配置文件信息
// console.log(options);
this.entry = options.entry;
this.output = options.output;
}
// 入口函数:实现编译
run() {
this.parser(this.entry);
}
/** 编译函数:实现具体编译
* 分析是否有依赖,获取依赖路径
* 编译模块生成chunk
*/
parser(modulePath) {
// 获取文件内容
const content = fs.readFileSync(modulePath, "utf-8");
// 将内容转化成AST树,生成module形式的内容类型
const ast = BabelParser.parse(content, { sourceType: "module" });
// 保存路径依赖
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const newPath =
"." +
path.sep +
path.join(path.dirname(modulePath), node.source.value);
console.log(newPath);
dependencies[node.source.value] = newPath;
}
});
// 将ast编译成js
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"] //插件
});
console.log(code);
// 返回模块路径、模块依赖、chunk
return {
modulePath,
dependencies,
code
};
}
};
四、收集所有依赖,整理成关系图谱
上面我们由parser方法获取传入的文件的路径、模块依赖和chunk,但我们还需要获取文件的依赖文件的信息,
-
在run方法中收集所有依赖,并转化成需要的关系图谱:
路径:{依赖, 代码}。 -
实现:
- 循环获取所有入口的依赖,对依赖进行编译,获取其路径、依赖、chunk,将其加到依赖数组中;
- 获取到所有依赖后,进行遍历修改成需要的关系图谱。
3.整体代码:
// 入口函数:实现编译
run() {
const moduleParserInfo = this.parser(this.entry);
console.log(moduleParserInfo);
// 收集所有依赖
this.modulesInfo.push(moduleParserInfo);
// 循环获取所有入口的依赖,对依赖进行编译,获取其路径、依赖、chunk,将其加到modulesInfo数组中
for (let i = 0; i < this.modulesInfo.length; i++) {
const dependencies = this.modulesInfo[i].dependencies;
if (dependencies) {
for (let j in dependencies) {
this.modulesInfo.push(this.parser(dependencies[j]));
}
}
}
// console.log(this.modulesInfo);
// 数据类型转换:转换成 路径:{依赖, 代码}
const obj = {};
this.modulesInfo.forEach(item => {
obj[item.modulePath] = {
dependencies: item.dependencies,
code: item.code
};
});
// 生成bundle文件
this.bundleFile(obj);
}
- 关系图谱的内容
五、生成bundle文件
-
在bundleFile方法中生成bundle文件。
-
实现:
- 生成文件是使用node.js的
fs.writeFileSync(bundlePath, content, "utf-8")方法实现的,需要提供打包路径和打包内容。 - 打包路径是根据配置文件
webpack.config.js中的output配置项获得的。 - 打包内容包括打补丁内容和run方法中生成的关系图谱。关系图谱中的代码无法正常运行,因为缺失了
require和exports方法,所以打补丁这两个方法。 parser方法中分析出的code中require引入的依赖路径是相对入口模块的路径,此时我们的代码在dist文件夹下,所以依赖路径要改成相对项目根节点下的路径。所以在检测到代码中有require时写一个newRequire方法来替换它,再执行代码。代码执行过程中会有exports模块,所以我们先设定一个exports对象,执行代码过程中会逐渐将exports模块添加到exports对象中,最后返回exports对象。
3.整体代码:
// 生成bundle文件
bundleFile(obj) {
// 根据配置中的output配置项合成打包文件的地址
const bundlePath = path.join(this.output.path, this.output.filename);
// 序列化关系图谱
const dependenciesInfo = JSON.stringify(obj);
// 打包的内容
const content = `(function(modulesInfo) {
// 打补丁,补上缺失的require和exports方法
function require(modulePath) {
// 定义一个新的require:将相对入口模块的路径转换成相对项目根节点下的路径
function newRequire(relativePath) {
return require(modulesInfo[modulePath].dependencies[relativePath])
}
const exports = {};
(function(require, code){
eval(code)
})(newRequire, modulesInfo[modulePath].code)
console.log(exports)
return exports;
}
require('${this.entry}')
})(${dependenciesInfo})`;
// 生成文件
fs.writeFileSync(bundlePath, content, "utf-8");
}