阅读 452

GoGoCode - 像用 Jquery 一样方便地处理 AST

导读

本文是写【gRPC】系列文章孵化出来的,想要实现 grpc-web 的完美封装,需要对 protoc 生成的 JS/TS 文件进行二次加工。自然想到用 AST(抽象语法树)的处理工具,在同事的推荐下,试用了一下 gogocode,的确很方便。

背景

具体背景详见【gRPC】Web 请求的 TS 封装 - 完美版,由于上下文较多,所以本文可以做到即使不爬前文也能看懂 gogocode 怎么用,各位看官可以安心阅读。

GoGoCode 官网的介绍:

GoGoCode 是一个基于 AST 的 JavaScript/Typescript/HTML 代码转换工具,但相较于同类,它提供了更符合直觉的 API:一套类 JQuery 的 API 用来查找和处理 AST、一套和正则表达式接近的语法用来匹配和替换代码

另有与 babeljscodeshift 的对比,可见它们是同一类东西。本文会比较偏实践,直接提出问题,并展示 gogocode 如何解决,不会有太多理论层面的论述。

目标

没有阅读上下文的同学可以简单理解本文需要作如下操作:

  1. 修改下列 JS 代码,增加一个函数;

  // 原内容   proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {     return /** @type{?proto.helloworld.Student} */ (       jspb.Message.getWrapperField(this, proto.helloworld.Student, 2));   };   // 待增加内容   proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {     return proto.helloworld.Student;   } 复制代码

  1. 修改下列 TS 代码,为 Class 增加一个方法。

  export class HelloRequest extends jspb.Message {     // 原内容     getStudentInfo(): Student | undefined;     // 待增加内容     getStudentInfoClass(): typeof Student;   } 复制代码

正文

处理 helloworld_pb.js 代码

分析

:没有阅读上下文的同学可以忽略分析过程,直接看分析结论。

阅读以下 protoc 生成的 helloworld_pb.js 代码

/**  * optional Student student_info = 2;  * @return {?proto.helloworld.Student}  */ proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {   return /** @type{?proto.helloworld.Student} */ (     jspb.Message.getWrapperField(this, proto.helloworld.Student, 2)); }; /**  * @param {?proto.helloworld.Student|undefined} value  * @return {!proto.helloworld.HelloRequest} returns this */ proto.helloworld.HelloRequest.prototype.setStudentInfo = function(value) {   return jspb.Message.setWrapperField(this, 2, value); }; 复制代码

我们可以发现:

  1. 所有复杂类型的 get 方法,都会调用 getWrapperField,set 也一样;

  2. getWrapperField 的参数中有我们需要的 Student 类,就是上文中的 proto.helloworld.Student

有了以上两个前提,理论上我们就可以用 AST 工具做以下操作,来达到最终目的:

  1. 获取到所有 getWrapperField 调用,并取出其第二个参数 A

  2. 同时获取到外层的函数名称 getXXX,上例中为 getStudentInfo

  3. 生成 getXXXClass 函数返回 A,上例为

proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {    return proto.helloworld.Student }; 复制代码

实现

直接上核心代码:

const $ = require("gogocode"); const classMethodList = []; // 收集被命中的 method,为后续 ts 处理提供筛选范围 const newCode = $(code).find(     `$_$0 = function() {       return jspb.Message.getWrapperField(this, $_$1, $_$2);     };`   )   .each((item) => {     const getMethod = item.match[0][0].value;     const paramsClass = item.match[1][0].value;     classMethodList.push(getMethod);     item.after(` ${getMethod}Class = function() {   return ${paramsClass}; }   `);   })   .root()   .generate(); 复制代码

简要说明一下:

  1. code 即为 protoc 生成的 helloworld_pb.js 的代码;

  2. gogocode 的代码风格很像 jQuery,可读性比较强,findeachafter 等语法,即使没用过 jQuery,大概也能猜到是什么意思;

  3. 代码中的 $_$x 是正则占位符,下面的 item.match[x][x].value 就是在读取它们的值。

处理后生成的 newCode 代码片段为:

proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {   return /** @type{?proto.helloworld.Student} */ (     jspb.Message.getWrapperField(this, proto.helloworld.Student, 2)); }; proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {   return proto.helloworld.Student; } 复制代码

  1. 新生成的代码(after 方法里的内容)可以根据自己的需求定义,理论上不用非得是一个函数,直接定义一个属性反而更简单,本文为了易读性使用了函数。

处理 helloworld_pb.d.ts 文件

分析

阅读以下 protoc 生成的 helloworld_pb.d.ts 代码

export class HelloRequest extends jspb.Message {   // other code   getStudentInfo(): Student | undefined;   setStudentInfo(value?: Student): HelloRequest;   hasStudentInfo(): boolean;   clearStudentInfo(): HelloRequest;   // other code } 复制代码

我们希望插入一条

    getStudentInfoClass(): typeof Student; 复制代码

利用 gogocode 提取出 getStudentInfoStudent 后拼出需要的代码就行了。但是,别看修改量不大,想实现也不太容易,需要注意几点:

  1. 理论上这部分修改的代码需要依赖修改 js 文件时,遍历的 getMethod 信息进行筛选,所以需要用一个 classMethodList 数组来收集被命中的方法名,详见上文的代码备注;

  2. typeof 这个 ts 的关键字目前 gogocode 没有提供方便的生成 AST 的方法(也可能是用的不熟),所以不得不引入 @babel/types 来生成需要的 AST 对象;

  3. 基于上一点,生成代码有两种思路:一是 clone 一份 getStudentInfo 的 AST,修改其属性;二是直接用 @babel/types 来生成。前者的易读性更好,但是代码量较多;后者代码量较少,但是需要具备一定的 babel 知识,各位视情况而定。不过即使是第一种方法,也需要稍微用一下 @babel/types 来解决 typeof 的问题。

实现

const $ = require("gogocode"); const t = require("@babel/types"); const tsNewCode = $(tsCode)   .find("$_$1(): $_$2 | undefined")   .each((item) => {     const callee = item.match[1][0].value; // getStudentInfo     const returnNode = item.match[2][0]; // returnType AST     if (returnNode.type === "TSTypeReference" && classMethodList.includes(callee)) {       // 方法一:clone item       const newNode = item.clone();       // t.tsTypeQuery 就是生成 typeof AST 的方法       newNode.attr({         "[0].nodePath.node.key.name": `${callee}Class`,         "[0].nodePath.node.returnType.typeAnnotation": t.tsTypeQuery(           returnNode.typeName // Student 的 AST         ),       });       item.after(newNode);       /* 方法二: @babel/types       item.after(         t.tsDeclareMethod(           null,           t.identifier(`${callee}Class`),           null,           [],           t.tsTypeAnnotation(t.tsTypeQuery(returnNode.typeName))         )       );       */     }   })   .root()   .generate(); 复制代码

最终生成的代码片段:

    // other code     getStudentInfo(): Student | undefined;     getStudentInfoClass(): typeof Student;     // other code 复制代码

处理 helloworld_grpc_web_pb.js

在《完美版》中还提到,需要对 sayHello 所在的文件进行处理,让其能够返回 sayHello 等 Method 的入参类。思路与方法与上文大同小异,就不赘述了。

值得一提的是本来它内部是有一个对象能够获取到这个信息的,但是因为有 bug,没有办法获取到。见如下代码:

const methodDescriptor_Greeter_SayHello = new grpc.web.MethodDescriptor(   '/helloworld.Greeter/SayHello',   grpc.web.MethodType.UNARY,   proto.helloworld.HelloRequest,   proto.helloworld.HelloReply,   /**    * @param {!proto.helloworld.HelloRequest} request    * @return {!Uint8Array}    */   function(request) {     return request.serializeBinary();   },   proto.helloworld.HelloReply.deserializeBinary ); 复制代码

实际上调用 methodDescriptor_Greeter_XXX.getRequestMessageCtor() 就能够获取到 HelloRequest 了,但是打包后的代码中,该对象并没有这个方法,所以根本调不到。

这个 bug 是同事发现的(没错,就是《完美版》中提到的那个同事),而且据说已经合了相关 PR,估计不久后会发布新版本吧。如果新版本自带了这个能力,我们也就不用再加工它了。

结语

本文的内容都是基于最简单的 demo,所以在用在实践当中时有可能会有需要调整的地方。所以各位重要的还是理解思想,知道这个思路可行即可,具体问题具体分析。

不过 gogocode 的确是一个好用的操作 AST 的工具,而且它还提供了 Vue2 转 Vue3 的功能,正好符合团队诉求,后续如果还有什么好用的工具会尽量分享出来。另外,简单了解下 Babel 尤其是 @babel/types 的用法其实性价比挺高的。

谨记,你是在寻找最好的答案,而不是你自己能得出的最好答案。——Ray Dalio


作者:DylanlZhao
链接:https://juejin.cn/post/7034082080451624974


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐