为什么要写这篇文章
当G和百度的时候,看到一些不错文章,但是总是给我一种印象,看了就忘记了,但是这次我打算看完之后,从算法的角度写一篇自己理解的文章,来看看webpack这一类打包工具它们做了什么?最困难的点在哪里?
思路
先看看这个图
上图就是webpack的实现流程
其中值得我们疑问的地方在哪?
问题: 如何通过遍历AST收集依赖?其中通过了什么算法?
因为当有很多复杂的依赖的时候,如何解决这些依赖的问题,应该是其中最困难的点
实例
看看源文件
第一个文件
//word.js
export const word = 'hello'
第二个文件
//message.js
import { word } from './word.js';
const message = `say ${word}`
export default message;
第三个文件
//index.js
import message from './message.js'
console.log(message)
看到了 index --> message --> word
转化问题
看到这三个文件互相依赖的关系;你脑海里有一些解题思路:
- 利用babel完成代码转换,并生成单个文件的依赖: @bable/parse 可以生成 AST; @babel/traverse 进行AST遍历,记录依赖关系;最后用@babel/core和@babel/preset-env进行代码的转换
- 生成依赖图谱
- 生成最后打包代码
这就是我们的问题,编写一个函数,将上面的ES6 代码转成 ES5 并将这些文件代码,生成一段浏览器能运行起来的代码
代码实现
按照上面的思路实现代码
第一步:
//先安装好相应的包
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
//导入包
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
//处理函数
function stepOne(filename){
// 读入文件
const ast = readFile(filename)
// 遍历AST抽象语法🌲
const dependencies = traverseAST(ast)
//通过@babel/core和@babel/preset-env进行代码的转换
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
//返回文件名称,和依赖关系
return {
filename,
dependencies,
code
}
}
// 最近可能是看了代码整洁之道之后,就会特别注意命名合理性和代码块不在同一层尽力不放在一起,即使这只是一个demo
function readFile(filename){
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'//babel官方规定必须加这个参数,不然无法识别ES Module
})
return ast
}
function traverseAST(ast){
const dependencies = {}
traverse(ast, {
//获取通过import引入的模块
ImportDeclaration({node}){
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
//保存所依赖的模块
dependencies[node.source.value] = newFile
}
})
return dependencies
}
第二步:生成依赖图谱。
function stepTwo(entry){
// 先拿到带有依赖的AST对象
const entryModule = stepOne(entry)
// 下面就是深度算法运用部分
const graphArray = getGraphArray(entryModule)
// 接下来就是生成图谱
const graph = getGraph(graphArray)
// 返回图谱
return graph
}
function getGraphArray(entryModule) {
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
const {dependencies} = item;//拿到文件所依赖的模块集合(键值对存储)
for(let j in dependencies){
graphArray.push(
one(dependencies[j])
)//敲黑板!关键代码,目的是将入口模块及其所有相关的模块放入数组
}
}
}
function getGraph(graphArray) {
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
// 可以测试下:console.log(stepTwo('./src/index.js'))
看到这里就应该大概知道代码是如何处理这种AST树的了,就和算法中处理二叉树是一样,找到规律,不断的循环
第三步:生成代码字符串
function stepThree(entry){
// //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
const graph = JSON.stringify(stepTwo(entry))
return `
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
//localRequire的本质是拿到依赖包的exports变量
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
}
require('${entry}')
})(${graph})`
}
// 可以测试下:console.log(stepThree('./src/index.js'))
总结:
其实整个下来你会发现,假设你找到了那个最为复杂的问题,并且通过算法或者其他的方式解决,那么你就基本上可以搞定webpack了;当然真实的webpack还做了很多其他的事情,这不是我们这次题目中的重点;阅读各种经典的工具,你都会发现核心,最为复杂的还是算法!