Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

js 抽象语法树的实战以及babel plugin 和 babel/parser plugin的区别 #5

Open
hello2dj opened this issue Oct 29, 2018 · 0 comments

Comments

@hello2dj
Copy link
Owner

hello2dj commented Oct 29, 2018

抽象语法树

说到抽象语法树就得说到具体语法树,具体差异,这个答案就很棒。

美团的这篇文章对AST的讲解也很棒

我的项目再用他做什么?js代码重构

  1. 替换变量
  2. 增加代码
    我们的项目使用的是eggjs, 也用了egg-sequelize,但是有些老旧的代码,游离在外,有100多张表,他们都如下
module.exports = (sequelize, DataTypes) =>
  sequelize.define(
    'answer',
    {...}
 )

而我想要的是

module.exports = app => {
    const { DataTypes } = app.Sequelize;
    const Answer = app.model.define(
      'answer', 
      {...}
    );
    return Answer;
}
  1. 当然我们可以做正则替换,替换好说,但是增加呢,这个比较简单那复杂的呢?但一百多个文件也够受了。
  2. 使用AST parser, 替换加增加统统搞定,顺道在挪到新的目录下面。

Esprima

js的AST的parser有很多,但他们基本都遵循MDN给出的parser API

  1. Acorn(babel依赖的插件)
  2. UglifyJS 2
  3. Shift
  4. Esprima
    我这里选择了Esprima,初次使用没有太多考量使用Esprima, 他的语法规范列的很详细, 这篇文章翻译了大部分语法

Esprima api

  1. 词法分析: 得到tokens
> var program = 'const answer = 42';

> esprima.tokenize(program);
[ 
  { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'answer' },
  { type: 'Punctuator', value: '=' },
  { type: 'Numeric', value: '42' } 
]
  1. 语法分析:得到AST
> var program = 'const answer = 42';

> esprima.parse(program);
{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "answer"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 42,
                        "raw": "42"
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "script"
}

如上图,我们要是想替换answer这个名字怎么办呢?或者就像是替换为parseInt(2,10), 1: 想美团的那个根据position替换,还有就是直接替换 AST

> var program = 'const answer = 42';
> const ast = esprima.parse(program);
> ast.body[0].declarations[0].id.name = '替换掉了';
> var addon = 'const stentence = '你个坏人';
> ast.body.unshift(esprima.parse(addon).body[0])
> {
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "sentence"
                    },
                    "init": {
                        "type": "Literal",
                        "value": "你个坏人",
                        "raw": "'你个坏人'"
                    }
                }
            ],
            "kind": "const"
        },
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "answer"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 42,
                        "raw": "42"
                    }
                }
            ],
            "kind": "const"
        }
    ]
    "sourceType": "script"
}

问题来了我们生成新的AST,那怎么在转换为代码呢?escodegen

escodegen.generate(AST) // string

到此我们可以发现,我可以找到任何语句,进行任何合法的修改,uglify2还提供了一些便利的方法,比如TreeWalker 遍历语法树,很方便,但还未实操过,有待使用。

利用语法树我们可以做什么?

  1. ugliy
  2. 编辑器语法高亮,自动补全,等等
  3. eslint等语法校验
  4. babel的功能,以及写babel的插件
  5. 利用AST进行元编程就好比JSX,那样写出自己的业务DSL, 说白了,就是个DSL版本的babel,为什么是DSL呢,因为不通用,但可以针对我们自己的业务进行AST级别的改造以及魔改,生成对应函数库(元编程的函数库)
    ...还有什么其他功能呢?

---------------------------------------------- 华丽分割线-------------------------------------------
我的好友也有一篇关于js ast的文章里面还介绍了babel插件的写法推荐一下(他可是高质量博主)

在上次写完后我就一直在思考一个问题,如何使用AST来编写DSL,js的表现力来说我觉得和那些有macro的语言还是差很多,不是不能做而是不优雅,比如:

crystal-lang 用宏定义方法

macro define_method(name, content)
  def {{name}}
    {{content}}
  end
end

define_method dj, { puts 2 }

但用js的话就是

function define_method(body) {
    new Function('a', 'b', body);  // 此处body是字符串, 'a + b; return a + b'
}
const a = define_method('a + b; return a + b')

可以看出来,js的元编程其实就是字符串拼接,那么macro 和AST又有什么关系呢?关系就是:macro匹配的参数会转化为AST,然后进行操作。向上面的crystal-lang的define_method的name和content,在宏内部就是AST节点。可以看出来使用macro编写DSL是很方便的。即使像C/C++那样简陋的macro都很有用 就不用说rust,crystal中那么强大的宏了。 js目前不支持macro。

我想写个DSL语法 就叫 '||='

 a ||= b;  若a为空 赋值为b

我看了js AST以后就在想esprima可以么?Babel可以么?
答: 目前不可以
原因:这些都只支持js的语法or Next JS的语法比如class, ArrowFuction等等, 你写个 '||=' babel也是识别不了的,肯定会报语法错误(不行你试试,要是真试了的话就别回来了。。。),也就是说你写的babel可以转换的那都是babel支持的语法,也可以说是js的语法或者是即将支持的语法。

问:此时就有人要问了,那JSX呢这可不是js的语法
答:这个是babel/parser内部就支持JSX
扩展: 那是不是就是说只要babel/parser能支持就好了
答:是的
举例:https://github.com/babel/babel/tree/master/packages/babel-parser/src/plugins 在这个文件夹下我们可以看到babel/parser支持的一些js语法之外的一些插件。

问:那接下该怎么做呢?
答:先看两张图


从这两个图我们可以看出 babel的工作原理就是 parse 源文件 到AST 再transform一下到 AST再从AST生成代码

接下来再看一篇关于babel plugin(注意是babel 的plugin不是babel/parser的plugin)的文章从零开始编写一个babel插件
这个文章实现了一个很简单的babel plugin插件干的事儿就是

import {uniq, extend, flatten, cloneDeep } from "lodash" 

// convert to 

import uniq   from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";

那么babel plugin和上面的图是什么关系呢? babel plugin就是就是图中AST -> AST的transform过程。也就是说我们想要使用babel plugin,第一步我们写的源文件得能parse到AST。其实我们可以看出来我们使用balbel/plugin能做的也就是 babel支持的语法的替换,删减或者增加。上例就是替换使用一大坨来替换。

我们可以从这里学到一些东西,那就是旧项目的改造,怎么样?或者不合理语法的改造,不想一个一个手动改,那就AST来改造

回到我们的DSL 'a ||= b' 上来,怎么办?就问怎么办?显然在transform 这个阶段是不行的

问:又问了,那JSX是咋弄的?
答:可以看上面的回答,是babel/parser就支持,也就是说如果我们可以写个babel/parser的plugin就好了

问: 怎么给babel/parser写个plugin呢?
答:你去babel的主库里提pr(233333), 是的目前babel不支持给babel/parser写plugin, 但相信未来不会太遥远的。 #1351 被关闭了,但在很久以前的babel版本中我们是可以的详见adding-custom-syntax-to-babel

问:真的没办法了?
答:babel的parser是从acorn 来的,其实acorn是支持plugin的,炫酷,也就是说只要我们想实现总是可以的。关于他的扩展方式没看到文档有时间在继续吧,acorn。但我们找到了出路,我们也可以随心所欲的写一个新的DSL language了,然后转到JS。

Code -> (1)Token -> AST ->(2) AST -> CODE

再总结一下,babel plugin的作用域是在(2),他做的是把合法的babel语法AST(都不敢说是js语法了...)转换为合法的JS的AST。 babel/parser plugin的作用域是在(1),他做的是把不合法的源码转换为合法的babel AST。分清babel plugin 和 babel/parser plugin我们就更能理解babel plugin到底是在做啥,他又能做啥。

总归我们是可以用优雅的方式在js中来编写DSL, 但不使用acorn等parser是不行的,其实我们在做前端基础工具时是可以做这些的,采用js的语法,加入合理的DSL 非js 语法使用 acorn转换。(使用decorator和proxy也是可以大大增强js的表现力的)

用大白话来说其实我们就是想要一个其他语言到js的transformer。Typescirpt, PureScirpt, CoffeScript...

@hello2dj hello2dj changed the title js 抽象语法树的实战(是否可以利用AST来进行DSL的元编程呢?) js 抽象语法树的实战以及是否可以利用AST来进行DSL的元编程呢? Dec 4, 2018
@hello2dj hello2dj changed the title js 抽象语法树的实战以及是否可以利用AST来进行DSL的元编程呢? js 抽象语法树的实战以及babel plugin 和 babel/parser plugin的区别 Dec 4, 2018
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant