hello2dj / blog

一些总结文章
27 stars 1 forks source link

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

Open hello2dj opened 5 years ago

hello2dj commented 5 years ago

抽象语法树

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

美团的这篇文章对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;
    }
  3. 当然我们可以做正则替换,替换好说,但是增加呢,这个比较简单那复杂的呢?但一百多个文件也够受了。

  4. 使用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' } ]

2. 语法分析:得到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](https://github.com/estools/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语法之外的一些插件。

问:那接下该怎么做呢?
答:先看两张图
![](https://user-gold-cdn.xitu.io/2018/11/11/167019b7916707b1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
![](https://cloud.githubusercontent.com/assets/853712/7672655/048d435a-fcf2-11e4-872f-ae47a6d9f143.png)
从这两个图我们可以看出 babel的工作原理就是 parse 源文件 到AST 再transform一下到 AST再从AST生成代码

接下来再看一篇关于babel plugin(注意是babel 的plugin不是babel/parser的plugin)的文章[从零开始编写一个babel插件](https://juejin.im/post/5a17d51851882531b15b2dfc) 
这个文章实现了一个很简单的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](https://github.com/babel/babel/issues/1351) 被关闭了,但在很久以前的babel版本中我们是可以的详见[adding-custom-syntax-to-babel](https://medium.com/@jacobp100/adding-custom-syntax-to-babel-e1a1315c6a90)

问:真的没办法了?
答:babel的parser是从acorn 来的,其实acorn是支持plugin的,炫酷,也就是说只要我们想实现总是可以的。关于他的扩展方式没看到文档有时间在继续吧,[acorn](https://github.com/acornjs/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...