怎么理解AST,并实现手写babel插件

简介: 怎么理解AST,并实现手写babel插件

怎么理解AST,并实现手写babel插件


程序表面看就是文本文件里的字符,计算机首先对其进行词法、语法分析,然后用某种计算机能理解的低级语言来重新表达程序,而这其中AST就是一个重点。

水平有限,粗浅的说下AST的理解,并通过手写babel插件来加深理解。

先看看, JS 是怎么编译执行的

  • 词法分析,将原始代码生成Tokens
  • 语法分析,将Tokens生成抽象语法树(Abstract Syntax Tree,AST)
  • 预编译,当 JavaScript 引擎解析脚本时,它会在预编译期对所有声明的变量和函数进行处理!并且是先预声明变量,再预定义函数!
  • 解释执行,在执行过程中,JavaScript 引擎是严格按着作用域机制(scope)来执行的,并且 JavaScript 的变量和普通函数作用域是在定义时决定的,而不是执行时决定的。

网络异常,图片无法展示
|
后两者暂不介绍,先简单理解下词法分析和语法分析:

词法分析

词法分析:将原始代码转化成最小单元的词语数组,最小单元的词语数组的专业名词是Tokens,这里注意,词语会加上相应的类型。

比如:var a = 'hello'

词法分析之后输出Tokens如下:

[
  {
    type: "Keyword",
    value: "var",
  },
  {
    type: "Identifier",
    value: "a",
  },
  {
    type: "Punctuator",
    value: "=",
  },
  {
    type: "String",
    value: "'hello'",
  },
];

可借助网站esprima在线生成。

语法分析

语法分析:将Tokens按照语法规则,输出抽象语法树,抽象语法树其实就是JSON对象

将 JS 进行生成抽象语法树的网站:astexplorer

比如:var a = 'hello'

语法分析之后生成抽象语法树如下:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": "hello",
            "raw": "'hello'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

借助网站esprimaastexplorer生成。

也可以借助esprima库生成:

const esprima = require("esprima");
const createAst = (code) => esprima.parseModule(code, { tokens: true });
const code = `var a="hello"`;
const ast = createAst(code);
console.log(ast);

注意到这里,是代码生成AST然后编译,但也可以用AST转化成代码

看着好像AST跟日常代码有点远,但其实一直在接触:

  • webpacklint 核心都是通过 AST 对代码进行检查、分析
  • UglifyJS 通过 AST 实现代码压缩混淆
  • babel核心通过AST而实现代码转换功能

以下要一步步实践~

修改 AST 生成新的代码

AST 怎么转成 代码

借助esprima将代码转化成AST,同理可以可借助escodegen将 AST 转化为代码.

const esprima = require("esprima");
// code变成AST的函数
const createAst = (code) => esprima.parseModule(code, { tokens: true });
const escodegen = require("escodegen");
// AST变成code的函数
let astToCode = (ast) => escodegen.generate(ast);
const code = `var a="hello"`;
const ast = createAst(code);
const newCode = astToCode(ast);
// var a = 'hello';
console.log(newCode);

怎么修改 AST

AST 其实就是 JSON 对象,当然怎么修改对象,就怎么修改 AST 啦。

比如想将var a='hello' 变成 const a='hello',先看下两个代码的 AST,然后将前者修改成和后者一样就好啦!

网络异常,图片无法展示
|

细看看只有,kind 那里不一样,这样就简单啦!

// ...同上面
const code = `var a="hello"`;
const ast = createAst(code);
// 直接修改kind
ast.body[0].kind = "const";
const newCode = astToCode(ast);
// const a = 'hello';
console.log(newCode);

但简单的这样直接修改很容易,一旦代码变得复杂,嵌套层次变多,或者修改的代码变多,上面的方式就难过了,这里借助另外一个工具库estraverse,遍历找到需要的地方,然后修改

怎么遍历 AST

AST 虽然是一个 JSON 对象,但是可以以树的结构去理解,而estraverse是以深度优先的方式遍历 AST 的。

凡是带所有属性 type 的都是一个节点。

网络异常,图片无法展示
|

也可以用代码直观的理解,estraverse怎么遍历AST的:

const esprima = require("esprima");
// code变成AST的函数
const createAst = (code) => esprima.parseModule(code, { tokens: true });
const code = `var a="hello"`;
const ast = createAst(code);
// 遍历
const estraverse = require("estraverse");
let depth = 0;
// 层次越深,缩进就越多
const createIndent = (depth) => " ".repeat(depth);
estraverse.traverse(ast, {
  enter(node) {
    console.log(`${createIndent(depth)} ${node.type} 进入`);
    depth++;
  },
  leave(node) {
    depth--;
    console.log(`${createIndent(depth)} ${node.type} 离开`);
  },
});

对照着生成的 AST 看,很明显就是深度遍历的过程:

Program 进入
  VariableDeclaration 进入
   VariableDeclarator 进入
    Identifier 进入
    Identifier 离开
    Literal 进入
    Literal 离开
   VariableDeclarator 离开
  VariableDeclaration 离开
 Program 离开

借助 estraverse 修改 AST

比如:var a='hello'; var b='world'var修改成const的话

// ..createAst astToCode函数同上面
const estraverse = require("estraverse");
const code = `var a='hello'; var b='world'`;
const ast = createAst(code);
estraverse.traverse(ast, {
  enter(node) {
    // 凡是var的都改成const
    if (node.kind === "var") {
      node.kind = "const";
    }
  },
});
const newCode = astToCode(ast);
// const a = 'hello'; const b = 'world';
console.log(newCode);

通过estraverse,很方便的,将旧的 AST增删改其中的结点,从而生成想要的新的 AST!

用 babel-types 快速生成一个 AST

上面的生成一个 AST 总是先有 code 才行,怎么能直接生成 AST 呢?

babel-types!!!!

AST 是由节点构成的,只要生成相应描述的节点就可以哒。

借助babel-types可以生成任意的节点!

比如const a='hello',b='world':

{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "a"
      },
      "init": {
        "type": "Literal",
        "value": "hello",
        "raw": "'hello'"
      }
    },
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "b"
      },
      "init": {
        "type": "Literal",
        "value": "world",
        "raw": "'world'"
      }
    }
  ],
  "kind": "const"
}

其实就是 init + id => VariableDeclarator + kind => VariableDeclaration

const kind = "const";
const id = t.identifier("a");
const init = t.stringLiteral("hello");
const id2 = t.identifier("b");
const init2 = t.stringLiteral("world");
// a=hello
const variableDeclarator = t.variableDeclarator(id, init);
const variableDeclarator2 = t.variableDeclarator(id2, init2);
const declarations = [variableDeclarator, variableDeclarator2];
const ast = t.variableDeclaration(kind, declarations);
// 这里的ast就是上面展开的
console.log(ast);

type 名一般就是相应的 API 名,同级的其他属性就是 API 的参数。

怎么写一个 babel 插件

babel 最重要的功能就是将ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法。

因为转化的地方非常多,babel一般以插件的形式,通过babel-core连接插件,从而转换代码。

假设将const转化为var的话:

const t = require("babel-types");
// 插件
const ConstPlugin = {
  visitor: {
    // path是对应的type的路径,其属性node也可以理解为一个描述const表达式的对象
    // 遍历到VariableDeclaration的时候,就把kind换成var
    VariableDeclaration(path) {
      let node = path.node;
      console.log(node);
      if (node.kind === "const") {
        node.kind = "var";
      }
    },
  },
};
// 试下写的插件
const code = `const a='hello'`;
const babel = require("babel-core");
let newCode = babel.transform(code, { plugins: [ConstPlugin] });
// var a = 'hello';
console.log(newCode.code);

手写实现插件 babel-plugin-arrow-functions

babel-plugin-arrow-functions的功能:将箭头函数转化为普通函数

var a = (s) => {
  return s;
};
var b = (s) => s;
// 转化成
var a = function (s) {
  return s;
};
var b = function (s) {
  return s;
};

先看下箭头函数和转化成普通函数之后的 AST 区别:

网络异常,图片无法展示
|

  • 注意,body 的类型如果不是BlockStatement的话,就换成BlockStatement;是的话,不用管
  • type 是ArrowFunctionExpression的节点,可以换成FunctionExpression节点

因为写babel插件,所以必须借助babel-core转换成新代码:

// npm i babel-core babel-types
const t = require("babel-types");
// 插件
const ArrowPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      let node = path.node;
      let { params, body } = node;
      // 如果不是代码块的话,生成一个代码块
      if (!t.isBlockStatement(body)) {
        // 生成return语句
        let returnStatement = t.returnStatement(body);
        // 创建代码块语句
        body = t.blockStatement([returnStatement]);
      }
      // 利用t生成一个等价的普通函数的表达式对象
      const functionJSON = t.functionExpression( null, params, body, false, false );
      // 替换掉当前path
      path.replaceWith(functionJSON);
    },
  },
};
// 试下写的插件
const arrowJsCode = `var a = (s) => { return s; }; var b = (s) => s;`;
const babel = require("babel-core");
let functionJSCode = babel.transform(arrowJsCode, { plugins: [ArrowPlugin] });
// var a = function (s) { return s; };var b = function (s) { return s; };
console.log(functionJSCode.code);

引用

目录
相关文章
|
6月前
|
JavaScript 前端开发 编译器
js开发: 请解释什么是Babel,以及它在项目中的作用。
**Babel是JavaScript编译器,将ES6+代码转为旧版JS以保证兼容性。它用于前端项目,功能包括语法转换、插件扩展、灵活配置和丰富的生态系统。Babel确保新特性的使用而不牺牲浏览器支持。** ```markdown - Babel: JavaScript编译器,转化ES6+到兼容旧环境的JS - 保障新语法在不同浏览器的运行 - 支持插件,扩展编译功能 - 灵活配置,适应项目需求 - 富强的生态系统,多样化开发需求 ```
50 4
|
1天前
|
开发框架 自然语言处理 JavaScript
babel 原理,怎么写 babel 插件
【10月更文挑战第23天】要深入理解和掌握如何编写 Babel 插件,需要不断实践和探索,结合具体的项目需求和代码结构,灵活运用相关知识和技巧。你还可以进一步扩展和深入探讨各个方面的内容,提供更多的实例和细节,以使文章更加丰富和全面。同时,关注 Babel 插件开发的最新动态和研究成果,以便及时了解其发展和变化。
|
3月前
|
JSON JavaScript 前端开发
JS逆向 AST 抽象语法树解析与实践
JS逆向 AST 抽象语法树解析与实践
47 2
|
自然语言处理 前端开发 JavaScript
Babel 的工作原理以及怎么写一个 Babel 插件
Babel 的工作原理以及怎么写一个 Babel 插件
200 0
|
6月前
|
JavaScript 前端开发 编译器
什么是TypeScript模块?为啥那么重要?
什么是TypeScript模块?为啥那么重要?
87 0
|
JavaScript
TypeScript 支持模块化编程,具体应用案例解析
TypeScript 支持模块化编程,具体应用案例解析
62 0
|
JavaScript 前端开发 编译器
🎖️使用 esbuild 简化 TypeScript 构建并跳过 tsc/tsx
JavaScript 生态系统一直在不断创新,最近的一位游戏规则改变者是 esbuild,这是一个极速的 JavaScript 和 TypeScript 打包器。
1003 0
|
缓存 JSON JavaScript
30分钟搞懂Rollup+Typescript工程构建(一)
最近在研究一个ngptcommit命令行工具,然后想通过Rollup+Typescript去编译的时候,发现对Rollup和Typescript的编译配置有点陌生,所以希望通过本文能够对其有个系统的认知。
222 0
|
JSON JavaScript 前端开发
30分钟搞懂Rollup+Typescript工程构建(二)
在本文中不讨论Typescript的具体用法,我们将学习如何将Typescript代码转为JavaScript。
491 0
|
JavaScript
js手写new操作符
手写new核心代码
104 0