AST递归解析
装一些babel的包
parse方法主要AST来解析语法树,替换相应的代码,然后再转换回来。
需要用到babel的一些库。
babylon
,把源码转换为ast语法树@babel/traverse
,遍历ast语法树,遍历的过程中可以替换节点@babel/types
,生成ast节点,可以生成各种类型@babel/generator
,根据ast语法树,生成源代码
安装一下
shell
yarn add babylon @babel/traverse @babel/types @babel/generator
看一下此时的package.json
json
{
"name": "cheny-pack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"cheny-pack": "./bin/cheny-pack.js"
},
"dependencies": {
"@babel/generator": "^7.17.0",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"babylon": "^6.18.0"
}
}
本节全部代码
js
// webpack\cheny-pack\lib\Complier.js
let path = require('path')
let fs = require('fs')
let babylon = require('babylon') // 把源码转换为ast
let t = require('@babel/types') // 遍历节点
let traverse = require('@babel/traverse').default // 替换节点
let generator = require('@babel/generator').default // 生成源代码
class Complier {
constructor(config) {
// 将配置文件挂载到实例上,所有实例都能拿到了
// webpack.config.js
this.config = config
// 1. 保存入口文件的路径
this.entryId; // './src/index.js'
// 2. 需要保存所有模块的依赖
// 解析文件的依赖,变成webpack打包传递的参数,key value的形式
// key 是路径名,value就是模块代码
this.modules = {}
this.entry = config.entry // 入口路径
this.root = process.cwd() // 工作路径 运行 npx cheny-pack 时候的路径
}
run() {
// 执行,并且创建模块的依赖关系
// 从入口开始执行
this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块
console.log(this.modules, this.entryId);
// 发射一个文件 打包后的文件
this.emitFile()
}
// 创建模块的依赖关系
buildModule(modulePath, isEntry) {
// 模块的key是相对路径 value是模块中的代码,不过需要做一些替换
// require都变成了webpack_require等
let source = this.getSource(modulePath) // 模块内容
// 模块id是一个相对路径 总路径 减去 rootPath
// 总路径modulePath F:\code\note_code\webpack\webpack-dev-3\src\index.js
// 工作路径this.root F:\code\note_code\webpack\webpack-dev-3
let moduleName = './' + path.relative(this.root, modulePath) // ./src/index.js 模块id
if (isEntry) { // 如果是主入口的话,记录一下主入口的id
this.entryId = moduleName // 保存入口的名字
}
// path.dirname(moduleName)获取父路径 ./src/index.js -> ./src
// 解析模块代码,替换相应的内容
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
// 在这里需要递归继续解析依赖项
dependencies.forEach(dep => { // 加载附模块
// 参数是一个绝对路径
this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件
})
}
// 解析文件
// 需要把source源码进行改造 返回一个依赖列表
// 因为每一个模块里面还可能会引另外的模块,所以需要一个依赖列表
parse(source, parentPath) { // AST 解析语法树
let ast = babylon.parse(source) // 源码先解析成ast语法树
// https://astexplorer.net/
let dependencies = [] // 存放当源代码依赖的数组,即当前文件引入的第三方模块
// 遍历ast语法树
traverse(ast, {
CallExpression(p) { // 调用表达式,比如 a() require() ...
let node = p.node // 拿到对应的节点
// 如果是require 就改为 __webpack_require__
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a
// 有时候我们引用的时候,没有带后缀,我们先把后缀加上
// path.extname('./a') === '' path.extname('./a.js') === '.js'
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
// webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js
moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js
// 把依赖的第三方模块 塞进依赖数组中
dependencies.push(moduleName)
// 替换原来的ast节点
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 生成新的代码
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 发射文件
emitFile() { }
// 获取模块代码
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf8')
}
}
module.exports = Complier
总结
本节主要做了下面几件事
将源代码解析成AST语法树,使用
babylon
插件jslet babylon = require('babylon') // 把源码转换为ast let ast = babylon.parse(source) // 源码先解析成ast语法树 // https://astexplorer.net/
使用
@babel/traverse
插件,遍历ast语法树let a = require('./b')
,将类似的语法,替换成let a = __webpack_require__('./src/b.js')
path.extname(moduleName)
可以获取路径后缀path.join(parentPath, moduleName)
可以拼接路径使用
@babel/types
生成各种类型的节点将遍历出的依赖关系存到依赖数组中
jslet traverse = require('@babel/traverse').default // 遍历ast语法树,替换节点 let t = require('@babel/types') // 生成ast节点,可以生成各种类型 // 遍历ast语法树 traverse(ast, { CallExpression(p) { // 调用表达式,比如 a() require() ... let node = p.node // 拿到对应的节点 // 如果是require 就改为 __webpack_require__ if (node.callee.name === 'require') { node.callee.name = '__webpack_require__' let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a // 有时候我们引用的时候,没有带后缀,我们先把后缀加上 // path.extname('./a') === '' path.extname('./a.js') === '.js' moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js // webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js // 把依赖的第三方模块 塞进依赖数组中 dependencies.push(moduleName) // 替换原来的ast节点 node.arguments = [t.stringLiteral(moduleName)] } } })
语法树样例图,
require('./a.js')
,转换成ast是,参考 https://astexplorer.net/使用
@babel/generator
可以将ast语法树,重新转换为源码jslet generator = require('@babel/generator').default // ast语法树,生成源代码 // 生成新的代码 let sourceCode = generator(ast).code
递归解析依赖的模块,存入modules中
js// path.dirname(moduleName)获取父路径 ./src/index.js -> ./src // 解析模块代码,替换相应的内容 let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) // 把相对路径和模块中的内容对应起来 this.modules[moduleName] = sourceCode // 在这里需要递归继续解析依赖项 dependencies.forEach(dep => { // 加载附模块 // 参数是一个绝对路径 this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件 })
最终转换的结果,
console.log(this.modules, this.entryId);
参考
https://www.bilibili.com/video/BV1a4411e7Bz?p=36&spm_id_from=pageDriver