Skip to content

AST递归解析

装一些babel的包

parse方法主要AST来解析语法树,替换相应的代码,然后再转换回来。

需要用到babel的一些库。

  1. babylon,把源码转换为ast语法树
  2. @babel/traverse,遍历ast语法树,遍历的过程中可以替换节点
  3. @babel/types,生成ast节点,可以生成各种类型
  4. @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

总结

本节主要做了下面几件事

  1. 将源代码解析成AST语法树,使用babylon 插件

    js
    let babylon = require('babylon') // 把源码转换为ast
    
    let ast = babylon.parse(source) // 源码先解析成ast语法树
    // https://astexplorer.net/
  2. 使用@babel/traverse插件,遍历ast语法树

    let a = require('./b'),将类似的语法,替换成let a = __webpack_require__('./src/b.js')

    path.extname(moduleName)可以获取路径后缀

    path.join(parentPath, moduleName)可以拼接路径

    使用@babel/types生成各种类型的节点

    将遍历出的依赖关系存到依赖数组中

    js
    let 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/

    image-20220203162417306

  3. 使用 @babel/generator可以将ast语法树,重新转换为源码

    js
    let generator = require('@babel/generator').default // ast语法树,生成源代码
    // 生成新的代码
    let sourceCode = generator(ast).code
  4. 递归解析依赖的模块,存入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 表示不是主入口文件
    })
  5. 最终转换的结果,console.log(this.modules, this.entryId);

image-20220203165214242

参考

https://www.bilibili.com/video/BV1a4411e7Bz?p=36&spm_id_from=pageDriver

AST 语法树