Skip to content
New issue

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

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

Already on GitHub? Sign in 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant