乐趣区

关于ast:AST真香

豆皮粉儿们,又见面了,明天这一期,由字节跳动数据平台的太郎酱,带大家走进 AST 的世界。

作者:太郎酱

什么是 AST

形象语法树(Abstract Syntax Tree, AST),是源代码的形象语法结构的树状示意,与之对应的是具体语法树;之所以是形象的,是因为形象语法树并不会示意出实在语法中呈现的每一个细节,而且是文法无关、不依赖于语言的细节;能够把 AST 设想成一套标准化的编程语言接口定义,只不过这一套标准,是针对编程语言自身的,小到变量申明,大到简单模块,都能够用这一套标准形容,有趣味的同学能够深刻理解 AST 的概念和原理,本文的重点聚焦在 JavaScript AST 的利用。

为什么要谈 AST

对于前端同学来说,日常开发中,和 AST 无关的场景无处不在;比方:webpack、babel、各种 lint、prettier、codemod 等,都是基于 AST 解决的;把握了 AST,相当于把握了控制代码的代码能力,能够帮忙咱们拓宽思路和视线,不论是写框架,还是写工具和逻辑,AST 都会成为你的得力助手。

AST 解析流程

先举荐一个 AST 在线转换网站:astexplorer.net,珍藏它,很重要;除了 js,还有很多其余语言的 AST 库;不必做任何配置,就能够作为一个 playground;

在解说 case 之前,先理解下解析流程,分为三步:

  1. source code –> ast (源代码解析为 ast)
  2. traverse ast (遍历 ast,拜访树中的各个节点,对节点做各种操作)
  3. ast –> code (把 ast 转换为源码,打完出工)

源码解析成为 AST 的引擎有很多,转换进去的 AST 大同小异;

Use Cases

从一个变量申明说起,如下:

const dpf = 'DouPiFan';

把代码复制到 astexplorer 中,失去如下后果 (后果已简化),这张图解释了从源码到 AST 的过程;

抉择不同的第三方库来生成 AST,后果会有所差别,这里以 babel/parse 为例;前端同学对 babel 再相熟不过了,通过它的解决,能够在浏览器中反对 ES2015+ 的代码,这仅仅是 babel 的其中一个利用场景,官网对本人的定位是:Babel is a javascript compiler。

回到 babel-parser,它应用 Babylon 作为解析引擎,它是 AST 到 AST 的操作,babel 在 Babylon 的根底上,封装了解析(babel-parser)和生成(babel-generator)这两步,因为每次操作都会做这两步;对于利用而言,操作的重点就是 AST 节点的遍历和更新了;

第一个 babel 插件

咱们以一个最简略的 babel 插件为例,来理解它的处理过程;

当咱们开发 babel-plugin 的时候,咱们只须要在 visitor 中形容如何进行 AST 的转换即可。把它退出你的 babel 插件列表,就能够工作了,咱们的第一个 babel 插件开发实现;

babel-plugin-import 是如何实现的?

应用过 antd 的同学,都晓得 babel-plugin-import 插件,它是用来做 antd 组件的按需加载,配置之后的成果如下:

import {Button} from 'antd'

    ↓ ↓ ↓ ↓ ↓ ↓

import Button from 'antd/lib/button'

本文旨在抛砖引玉,对于插件的实现细节以及各种边界条件,可参考插件源码;

以 AST 的思维来思考,实现步骤如下:

  1. 查找代码中的 import 语句,且必须是 import {xxx} from ‘antd’
  2. 把步骤一找到的节点,转换为 import Button from ‘antd/lib/button’

实现步骤

  1. 关上神器:AST Explorer,把第一行代码复制到神器中
  2. 点击代码中的 import 关键字,会主动定位到对应的节点,构造如下:
ImportDeclaration {

    type: "ImportDeclaration",

    specifiers: [{// 对应 {} 括号中的组件

        ImportSpecifier: {

            type: "ImportSpecifier",

            imported: {

                type: "Identifier",

                name: "Button"

            }

        }

    }] 

    source: {

        type: "StringLiteral",

        value: "antd"

    },

    ...

}

源码被转换成带有类型和属性的对象,不论是关键字、变量申明,还是字面量值,都有对应类型;

  1. import 语句对应的类型是:ImportDeclaration
  2. {Button} 对应的是 specifiers 数组,示例中只引入了 “Button”,所以 specifiers 数组中的元素只有一个
  3. specifiers 中的元素,也就是 Button,类型是 ImportSpecifier;
  4. ‘antd’ 在 source 节点中,类型是:StringLiteral,value 为 antd

再次阐明:示例并非残缺逻辑实现,细节和边界条件,可参考源码或本人欠缺;

针对 AST 的操作,和浏览器自带 DOM API 相似;先确定要查找节点的类型,而后依据具体的条件,放大搜寻范畴,最初对查找到的节点,进行增删改查;

// babel 插件模板

export default function({types: t}) {

    return {

        // Visitor 中的每个函数接管 2 个参数:path 和 state

        visitor: {ImportDeclaration(path, state) {const { node} = path;

                // source 的值为 antd

                if(node.source.value === 'antd'){

                    const specifiers = node.specifiers

                    // 遍历 specifiers 数组

                    const result = specifiers.map((specifier) => {

                        const local = specifier.local

                        // 结构 source

                        const source = t.stringLiteral(`${node.source.value}/lib/${local.name}`)

                        // 结构 import 语句

                        return t.importDeclaration([t.importDefaultSpecifier(local)], source)

                    })

                    console.log(result)

                    path.replaceWithMultiple(result)

                }

            }

        }

    }

}

验证办法也很简略,把这段代码复制到 AST Explorer 中,查看输入后果即可;到这里,这个“繁难”插件实现实现;

再来回顾一下实现思路:

  1. 比照源码在语法树中的差别,明确要做哪些转换和批改
  2. 剖析类型,能够在 babel 官网,找到类型阐明
  3. 在插件模板中,通过 visitor 拜访对应的类型节点,进行增删改查
Codemod

下面解说了 ast 在 babel 中的基本操作办法,再来看看 codemod。

应用 antd3 的同学,都接触过 antd3 到 antd4 的 codemod,这是一个帮忙咱们自动化的,把 antd3 的代码转换到 antd4 的一个工具库;因为它的实质是进行代码转换,所以基于 babel 实现 codemod,是齐全 ok 的。但除了代码转换,还须要有命令行操作,源代码读取,批量执行转换,日志输入等性能,他更是一个性能汇合,代码转换是其中很重要的一部分;所以,举荐另外一个工具 jscodeshift。他的定位是一个 transform runner,所以,咱们的外围工作是,定义一系列的 transform,也就是转换规则,剩下的命令行、源码读取、批量执行、日志输入都能够交给 jscodeshift。

筹备工作

先定义一个 transform,和 babel 插件很像

import {Transform} from "jscodeshift";



const transform: Transform = (file, api, options) => {return null;};



export default transform;
入手实际

咱们尝试把 Button 组件的 ”type” 属性替换为 ”status”,并把 width 属性,增加到 style 中:

// 输出

const Component = () => {

  return (

      <Button 

          type="dange"

          width="20"

      />

  )

}



// 输入

const Component = () => {

  return (

      <Button 

          statue="dange"

          style={{width: 20}}

      />

  )

}
差别比照
  1. react 组件的属性类型为:JSXIdentifier,属性 ”type” 批改为 ”status”
  2. 如果组件有 ”width” 属性,把该属性挪动到 ”style” 属性中

查找 Button 组件的代码如下:

import {Transform} from "jscodeshift";



const transform = (file, api, options) => {

    const j = api.jscodeshift;

    // 查找 jsx 节点, 通过 find 办法的第二个参数进行过滤

    return j(file.source).find(j.JSXOpeningElement, {

        name: {

            type: 'JSXIdentifier',

            name: 'Button'

        }

    })

};



export default transform;
属性替换

接下来,增加属性替换逻辑,把 type 替换为 status

export default function transformer(file, api) {

  const j = api.jscodeshift;

  return j(file.source)

    .find(j.JSXOpeningElement, {

      name: {

        type: 'JSXIdentifier',

        name: 'Button'

      }

    }).forEach(function(path){

        var attributes = path.value.attributes;

        attributes.forEach(function(node, index){

          const attr = node.name.name;

          if(attr === 'type'){

            // attr 为 type 时,把属性名替换为 status

            node.name.name = 'status'

          }

        })

     })

    .toSource();}

在查找 JSX 元素时,jscodeshift 能够间接获取:j(file.source).findJSXElements(),这里应用 find 代替,find 的第二个参数,能够形容过滤条件;

jscodeshift 反对链式调用,查找到节点后,应用 forEach 遍历,当组件的属性名为 type 时,把属性名替换为 ”status”,这里只思考了一种状况,还存在 JSXNamespaceName 的场景,比方:<Button n:a />;

解决 width

存在 width 时,获取 width 的值,而后删除该节点;

接下来是创立 style 节点,类型是 jsxAttribute,把 width 的值设置回 style

...

attributes.forEach(function(node, index){

    const attr = node.name.name;

    if(attr === 'width'){

        // 获取 width 的值

        width = node.value.value;

        // 删除 width 属性

        attributes.splice(index, 1)

     }

     let width;

     if(width){

        // 结构 style 节点

        var node = j.jsxAttribute(

          // 设置 attr 的名称为: style

          j.jsxIdentifier('style'), 

          // 结构 jsxExpressionContainer { }

          // 结构 objectExpression 

          j.jsxExpressionContainer(j.objectExpression([

            j.objectProperty(j.identifier('width'),

              j.stringLiteral(width),

            ),

          ])),

        )

        // 插入 style 节点

        attributes.splice(index, 0, node)

    }

}



...

总结

下面别离介绍了基于 babel 的实现和 jscodeshift 的实现,思路一样,比较简单,然而要花费额定的工夫和精力能力达到比拟完满的状态,尤其是面对大规模的代码解决时,边界条件较多,须要思考的十分全面;但这个投入是值得的,能够把大部分工作自动化的解决;

另外,babel 的专长是在 ast 的解决,jscodeshift 更像是性能齐备的工具汇合,能够把精力聚焦在转换器的实现,请依据理论场景抉择适合的工具。

The End

退出移动版