blog.cover

Complier&AST-前端工具链开发基础

我不知道啊我写

浏览483
评论0
2025年12月05日

1.介绍

在现代前端开发中,工程化已经成为不可或缺的一部分,而 Bundle(打包工具) 则是前端工程化的基石。通过 Bundle 工具,我们可以将复杂的开发环境中的代码转化为浏览器可以直接运行的 JavaScript 文件。这一过程的核心在于 编译,而编译的本质则是对代码进行解析、转换和生成。

本文将讲述现代前端编译的核心原理,重点聚焦于 抽象语法树(Abstract Syntax Tree) 的构建与应用。无论是 Webpack、Rollup 还是 Babel,这些现代化工具都依赖 AST 来完成代码的分析、优化和转换。

1.1.什么是 AST?

AST(Abstract Syntax Tree,抽象语法树)是一种将源代码结构化表示的数据结构。它把一段文本形式的代码(如 JavaScript)解析成一棵树形对象,其中每个节点代表一个语法结构(如变量声明、函数调用、表达式等)。

比如,这段 JavaScript 代码:

Text
      const sum = (a, b) => a + b;
    

会被解析为如下 AST(遵循 ESTree 规范):

Text
      {
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "kind": "const",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": { "type": "Identifier", "name": "sum" },
          "init": {
            "type": "ArrowFunctionExpression",
            "params": [
              { "type": "Identifier", "name": "a" },
              { "type": "Identifier", "name": "b" }
            ],
            "body": {
              "type": "BinaryExpression",
              "operator": "+",
              "left": { "type": "Identifier", "name": "a" },
              "right": { "type": "Identifier", "name": "b" }
            }
          }
        }
      ]
    }
  ]
}
    

通过遍历这棵树,我们可以精确控制每个语法节点的处理逻辑

  • 把 ArrowFunctionExpression 转换成普通函数(Babel 的 ES6 → ES5 转译);

  • 检查是否存在未声明的变量(各种Lint工具的变量检查);

  • 自动插入埋点代码(监控 SDK 的静态注入);

正是这种“可分析、可修改、可生成”的能力,让 AST 成为现代前端工程化的基石。

编译器

编译器的工作流程通常分为以下几个主要阶段,每个阶段都有其特定的任务和目标:

  • 词法分析:首先,词法不是语法。这步是为了原代码拆解为词元(tokens),例如关键字、操作符、标识符等,还要分析 [] {}这些的层级关系。

  • 语法分析:根据语言的语法规则解析这些标记,并构建抽象语法树(AST)。这是整个编译过程中最重要的一步,因为 AST 是后续所有操作的基础。

  • 语义分析:检查程序的意义是否合法,例如类型检查。这一步确保代码符合语言规范,避免运行时错误。前端常使用ESLint,Biome(我觉得Biome必秒ESLint)等工具来实现静态代码检查,其原理也就是语义分析。

  • 中间代码生成:将 AST 转换为一种更接近目标机器语言的形式。这一步的目的是为后续优化和生成目标代码做准备。

  • 代码优化:对中间代码进行优化以提高执行效率或减少资源消耗。常见的优化手段包括常量折叠、死代码消除、循环展开等。

  • 代码生成:优化后的中间代码转换为目标机器码或目标语言代码。对于前端ts编译器来说,这通常是生成浏览器可执行的 JS 代码。

  • 链接:将多个文件的目标代码组合成一个可执行文件。在前端工程中,这一步通常由打包工具(如 Webpack 或 Rollup)完成。

2. 用JS实现一个简单的编译器

  1. 词法分析

    整一个分词器,根据词法把每个词转化为一个节点。

    Javascript
          // 词性类型
    const WHITESPACE = /\s/ // 空格
    const NUMBER = /[0-9]/  // 数字
    const LETTERS = /[a-zA-Z_]/ // 字母与下划线
    
    module.export = function tokenizer(sourceCode){
        let index = 0
        const tokens =  []
        while(index < sourceCode.length){
            let char = sourceCode[cur_index]
            if (char === "("){
                token.push({
                    type: "paren",
                    value: "("
                })
                index++
                continue  // 进入下一次循环 前进一个 token/词
            }
            if (char === ")"){
                // ...省略
            }
            if (WHITESPACE.test(char)){  // 正则匹配空格
                index++
                continue
            } 
            if (NUMBER.test(char)){  // 正则匹配数字
                let value = ""
                while(NUMBER.test(sourceCode[index])){
                    value += sourceCode[index]
                    index++
                }
                token.push({
                    type: "number",
                    value: value
                })
                continue
            } 
            if (LETTERS.test(char)){  // 正则匹配字符 这也是为什么变量开头不能是数字
                let value = ""
                // 收集字母、数字和下划线
                while (index < sourceCode.length && (LETTERS.test(char) || NUMBER.test(char))) {
                    value += char;
                    char = sourceCode[++index];
                }
                tokens.push({ type: "name", value });
                continue;
                token.push({
                    type: "name",
                    value: value
                })
                continue
            } 
            if ("+-*/=".includes(char)) {  // 处理其他符号(如操作符、括号等)
                tokens.push({ type: "operator", value: char });
                index++;
                continue;
            }
            throw new Error(`Unexpected character: ${char}`);  // 遇到未知字符
        }
        return tokens
    }
        
  2. 语法分析

    这一步是使用Parser进行语法分析,构建 AST (抽象语法树)

    Javascript
          // 假设有这么一个token 其原字符串是 "abc123 + 456 _var = 789"
    const tokens = [
      { type: "name", value: "abc123" },
      { type: "operator", value: "+" },
      { type: "number", value: 456 },
      { type: "name", value: "_var" },
      { type: "operator", value: "=" },
      { type: "number", value: 789 }
    ]
    // AST 示例
    const AST = {
      "type": "Program",
      "body": [
        {
          "type": "AssignmentExpression",
          "operator": "=",
          "left": {
            "type": "Identifier",
            "name": "_var"
          },
          "right": {
            "type": "BinaryExpression",
            "operator": "+",
            "left": {
              "type": "Identifier",
              "name": "abc123"
            },
            "right": {
              "type": "NumberLiteral",
              "value": 456
            }
          }
        }
      ]
    }
        

    下面是Parser的实现

Javascript
         function parser(tokens){
       const AST = {
           type: 'Program', // 这是AST的根
           body: []
       }
       let index = 0
       // 处理函数 递归
       function walk(){
       	let token = tokens[index] 
           if (token.type === 'number'){
               index++
               return{
                   type: 'NumberLiteral',
                   value: token.value
               }
           }
           if (token.type === 'paren' && token.value === '('){
               token = tokens[++index]
               // 创造节点 层级 + 1
               let node = {
                   type:'CallExpression',
                   name: token.value
                   params: []
               }
           	token = tokens[++index]
           	// 搜索并创造子节点 直到发现右括号 则根据函数入栈的顺序 返回到最开始建立node的层级
            	while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')){
                   node.params.push(walk())  // 递归生成子节点
                   token = tokens[index]
               }
           	index++
               return node
           }
       	throw TypeError('Unexpected token: ', token.type, token.value)
       } 
       // 主循环
       while(index < tokens.length){
           AST.body.push(walk())
       }
   	return AST // 所有执行完返回 AST
   }
   
    
  1. 代码转换

    这一步是将Parser返回的 AST 作进一步处理,生成一个新的 AST。 导入一个工具——estraverse,它是一个 traverse,有了它我们可以方便快捷的实现遍历 AST节点,并在其提供的两个钩子函数(enter & leave)中实现修改节点或动态插入节点。

Javascript
      const esprima = require('esprima');  // 一个生成AST的工具
const estraverse = require('estraverse');
const code = ` let a = 1; let b = 2; console.log(a + b); `; // 源代码
const ast = esprima.parseScript(code);  // 生成ast
// 转换 AST,深度优先遍历,且只会遍历节点的 type 属性
estraverse.traverse(ast, {
  // 进入-hook
  enter(node) {
    console.log('enter -> node.type', node.type);
  },
  // 离开-hook
  leave(node) {
    console.log('leave -> node.type', node.type);
  },
});
    

输出结果如下

Shell
      enter -> node.type Program
enter -> node.type VariableDeclaration
enter -> node.type VariableDeclarator 
enter -> node.type Identifier 
leave -> node.type Identifier 
enter -> node.type Literal 
leave -> node.type Literal leave -> node.type 
.......
    

如果我们想修改某个树节点的某个值,则可以在enter钩子中实现如下代码

Javascript
      // 假设有个函数名字叫做 privateFunc 我们要修改其名字为 _Func
enter(node) {
	(()=>{ // 立即执行函数
		if(node.type === "FunctionDeclaration" && node.id.name === 'privateFunc'){
			node.id.name = '_Func'
		}
	})()
}
    
  1. 代码生成 根据 AST 以及预设的代码规范,生成出目标代码。这也是 Babel 实现代码降级的核心部分。

3.体验AST

  1. 在线分析AST网站: https://astexplorer.net/ (可能需要魔法

Complier&AST-前端工具链开发基础
作者 NoelOrin
发布时间2025年12月05日
许可协议 CC BY-NC-SA 4.0