webpack
作为一款打包工具,在学习它之前,对它感到特别陌生,最近花了一些时间,学习了下。
学习的最大收获是手写一个简易的打包工具webpack-demo
。
webpack-demo
分为主要分为三个部分:
- 生成抽象语法树
- 获取各模块依赖
- 生成浏览器能够执行的代码
依赖准备
src
目录下有三个文件:index.js
、message.js
、word.js
。他们的依赖关系是:index.js
是入口文件,其中index.js
依赖message.js
,message.js
依赖word.js
。
index.js
:
import message from "./message.js";
console.log(message);
message.js
:
import { word } from "./word.js";
const message = `say ${word}`;
export default message;
word.js
:
var word = "uccs";
export { word };
现在要要编写一个bundle.js
将这三个文件打包成浏览器能够运行的文件。
打包的相关配置项写在webpack.config.js
中。配置比较简易只有entry
和output
。
const path = require("path");
module.exports = {
entry: path.join(__dirname, "./src/index.js"),
output: {
path: path.join(__dirname, "dist"),
filename: "main.js",
},
};
代码分析
获取入口文件的代码
通过node
提供的fs.readFileSync
获取入口文件的内容
const fs = require("fs");
const content = fs.readFileSync("./src/index.html", "utf-8");
拿到入口文件的内容后,就需要获取到它的依赖./message
。因为它是string
类型。自然就想到用字符串截取的方式获取,但是这种方式太过麻烦,假如依赖项有很多的话,这个表达式就会特别复杂。
那有什么更好的方式可以获取到它的依赖呢?
生成抽象语法树
babel
提供了一个解析代码的工具@babel/parser
,这个工具有个方法parse
,接收两个参数:
code
:源代码options
:源代码使用ESModule
,需要传入sourceType: module
function getAST(entry) {
const source = fs.readFileSync(entry, "utf-8");
return parser.parse(source, {
sourceType: "module",
});
}
这个ast
是个对象,叫做抽象语法树,它可以表示当前的这段代码。
ast.program.body
存放着我们的程序。通过抽象语法树可以找到声明的语句,声明语句放置就是相关的依赖关系。
通过下图可以看到第一个是import
声明,第二个是表达式语句。
接下来就是拿到这段代码中的所有依赖关系。
一种方式是自己写遍历,去遍历body
中的type: ImportDeclaration
,这种方式呢有点麻烦。
有没有更好的方式去获取呢?
获取相关依赖
babel
就提供一个工具@babel/traverse
,可以快速找到ImportDeclaration
。
traverse
接收两个参数:
ast
:抽象语法树options
:遍历,需要找出什么样的元素,比如ImportDeclaration
,只要抽象语法树中有ImportDeclaration
就会进入这个函数。
function getDependencies(ast, filename) {
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newFile = path.join(dirname, node.source.value);
dependencies[node.source.value] = newFile;
},
});
return dependencies;
}
ImportDeclaration
:会接收到一个节点node
,会分析出所有的ImportDeclaration
。
通过上图可以看到node.source.value
就是依赖。将依赖保存到dependencies
对象中就行了,这里面的依赖路径是相对于bundle.js
或者是绝对路径,否则打包会出错。
代码转换
依赖分析完了之后,源代码是需要转换的,因为import
语法在浏览器中是不能直接运行的。
babel
提供了一个工具@babel/core
,它是babel
的核心模块,提供了一个transformFromAst
方法,可以将ast
转换成浏览器可以运行的代码。
它接收三个参数:
ast
:抽象语法树code
:不需要,可传入null
options
:在转换的过程中需要用的presents: ["@babel/preset-env"]
function transform(ast) {
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return code;
}
获取所有依赖
入口文件分析好之后,它的相关依赖放在dependencies
中。下一步将要去依赖中的模块,一层一层的分析最终把所有模块的信息都分析出来,如何实现这个功能?
先定义一个buildModule
函数,用来获取entryModule
。entryModule
包括filename
、code
、dependencies
function buildModule(filename) {
let ast = getAST(filename);
return {
filename,
code: transform(ast),
dependencies: getDependencies(ast, filename),
};
}
通过遍历modules
获取所有的模块信息,当第一次走完for
循环后,message.js
的模块分析被推到modules
中,这时候modules
的长度变成了2
,所以它会继续执行for
循环去分析message.js
,发现message.js
的依赖有word.js
,将会调用buildModule
分析依赖,并推到modules
中。modules
的长度变成了3
,在去分析word.js
的依赖,发现没有依赖了,结束循环。
通过不断的循环,最终就可以把入口文件和它的依赖,以及它依赖的依赖都推到modules
中。
const entryModule = this.buildModule(this.entry);
this.modules.push(entryModule);
for (let i = 0; i < this.modules.length; i++) {
const { dependencies } = this.modules[i];
if (dependencies) {
for (let j in dependencies) {
// 有依赖调用 buildmodule 再次分析,保存到 modules
this.modules.push(this.buildModule(dependencies[j]));
}
}
}
modules
是个的数组,在最终生成浏览器可执行代码上有点困难,所以这里做一个转换
const graphArray = {};
this.modules.forEach((module) => {
graphArray[module.filename] = {
code: module.code,
dependencies: module.dependencies,
};
});
生成浏览器可执行的代码
所有的依赖计算完之后,就需要生成浏览器能执行的代码。
这段代码是一个自执行函数,将graph
传入。
graph
传入时需要用JSON.stringify
转换一下,因为在字符串中直接传入对象,会变成[object Object]
。
在打包后的代码中,有个require
方法,这个方法浏览器是不支持的,所有我们需要定义这个方法。
require
在导入路径时需要做一个路径转换,否在将找不到依赖,所以定义了localRequire
。
require
内部还是一个自执行函数,接收三个参数:localRequire
、exports
、code
。
const graph = JSON.stringify(graphArray);
const outputPath = path.join(this.output.path, this.output.filename);
const bundle = `
(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("${this.entry}")
})(${graph})
`;
fs.writeFileSync(outputPath, bundle, "utf-8");
总结
通过手写一个简单的打包工具后,对webpack
内部依赖分析、代码转换有了更深的理解,不在是一个可以使用的黑盒了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。