AST的一次简单尝试
一、从一个需求开始
前几天接到一个新的需求,不是特别复杂如下图:
要求如下:
- 合并树结构下的所有事件与方法到同一个文本编辑器,动态生成方法名称与入参以及系统注释
- 文本编辑器内容变更,通知源数据变更
- 文本编辑器可以编辑当前入参与返参并需要解析出来
- 获取文本编辑器当前焦点,高亮显示当前编辑的树结构
对于一个有多年切图经验的cv仔,大致一设计,就有了实现思路:
- function生成可以通过字符串拼接搞定
- 变更通知在Vue下通过Watch可以监听到具体的数据变化,但是苦于前几天排查Watch造成的各种问题,尤其是通知的不确定性造成的性能问题,对Watch抱有一定的敬畏之心,顾此处没有采用,而是采用发布订阅模式,变更之后定向通知到订阅的function下
- 后面两个字符串的解析问题必须是正则匹配去搞啊
既然有了实现方案,那就说干就干,首先递归解析树结构生成文本编辑器内容
实现订阅发布
//发布变更notify(methods, data) {if (this.sub[methods]) {this.sub[methods].map(item => {item.notify(data);return item;});}}//订阅/**** @param {sub} 订阅方法* @param {notify} 通知callback*/subscribe(param) {if (typeof param.notify != "function") {return console.error("callback必须为function");}if (!this[param.sub]) {return console.error("不存在["+param.sub+"]订阅方法");}if (!this.sub[param.sub]) {this.sub[param.sub] = [];}this.sub[param.sub].push(param);// console.log(// "事件【" + param.sub + "】订阅成功,通知到【" + param.notify + "】"// );}
}
在按照Vue模板解析语法实现一套Function解析,整体齐活
解析结果如下:
到目前为止,我们大致已经实现了当初的需求,可是这个解析模板真的能实现我们所有的语法解析么?
二、思考
- JS语法过于活泛尤其是在function内部如下图,该如何去解析返回?
function test(name){()=>{()=>{(function(){return})()}}
}
- function 名称被修改之后如何判断,原则上函数名称系统生成不可变更
function test(name){return name
}
//修改成function getName(name){return name
}
基于上面两个痛点问题,如果单纯使用正则匹配的方式去实现,需要实现及其复杂且繁琐的匹配规则,而且场景覆盖可能也没有那么的完善,需要不断在实际运行中不断去完善解析规则,那么我们应该如何去处理?
答案是可以通过AST去替代我们自主完成的语法解析。
三、AST实现 JS 语法解析
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。对前端开发而言AST你可能不熟悉但是这却是你日常开发必不可少的,如babel实现高版本JS原发转化为低版本语法,在这里我们就不详细介绍AST相关知识了,感兴趣可以参考陈栋老师的文章【JavaScript AST实现原理揭秘】。接下来我们一起看看如何通过AST去解析获取方法名称以及返回值。
1.引入AST解析依赖包,这里我们采用babel/parser
`
const parser = require("@babel/parser");
let configJsTree = parser.parse(`${StrFun}`,{sourceType: "module",plugins: ["jsx","flow",],});
通过@babel/parser解析之后我们得到如下的树结构
我们先来看看每个字段都代表了什么,才能继续向下分析
字段名称 | 解释 |
---|---|
CommentBlock | 块注释 |
CommentLine | 单行注释 |
FunctionDeclaration | 块状域,存放完整的代码块 如 { return a} |
ReturnStatement | 返回域 程序中的return |
Identifier | (标志)对象,用来作为函数的唯一标志 |
ExpressionStatement | 表达式 |
VariableDeclarator | 等同于var |
BinaryExpression | 表达式 |
ArrowFunctionExpression | 箭头函数 |
本次我们分析能用到的有 Identifier,FunctionDeclaration,ReturnStatement等,接下来我们实现语法的解析 |
let body = configJsTree.program.body;let functionList = {};body.forEach(item => {//如果是函数体if (item.type == "FunctionDeclaration") {//获取方法名称let functionName = item.id.name;//获取入参let param = [];if (item.params) {item.params.forEach(jj => {param.push(jj.name);});}if (param.length > 0) {param = param.join(",");} else {param = "";}let hasReturn = false;//获取返参,存在return 并且不是空 returnif (item.body.body.some(kk => kk.type == "ReturnStatement" && kk.argument)) {hasReturn = true;}//获取程序开始位置,以及代码行数let position = `${item.loc.start.line},${item.loc.end.line}`;//获取方法体,由于AST需要逐层解析过于复杂,且业务无需该操作,顾解析当前方法体采用正则匹配let ContentString = res.split("\n");ContentString = ContentString.slice(Number(position.split(",")[0]) - 1,Number(position.split(",")[1]));ContentString = ContentString.join("\n");let content = parseStrinToFunctionContent(ContentString);functionList[functionName] = {functionName,param,hasReturn,position,function: content};}});
解析结果
函数名称变更监听
...if(this.oldFunctionList){//如果老数据列表不存在当前函数名称,启动checkif(!this.oldFunctionList[functionName]){for(let i in this.oldFunctionList){if(this.oldFunctionList[i].position==position&&this.oldFunctionList[i].functionName!=functionName){functionName=this.oldFunctionList[i].functionNameitem.id.name=this.oldFunctionList[i].functionNamenameBeChanged=true}}}}....//AST重置回JavaScript,并更新视图if(nameBeChanged){const generate = require("@babel/generator").defaultconfigJsTree.program.body=bodyconst result = generate(configJsTree, {minified: true})this.notify("updateEditor",result.code)}
到目前为止我们已经实现了上面的大部分要求,包括函数名称变更检测以及还原,参数解析,函数名称解析等。
AST的一次简单尝试相关推荐
- 桌面应用程序员简单尝试Rich JavaScript Application
虽然10年前搞过一段时间的Web应用开发,且为所在企业设计了一个基于ASP.NET WebForms(在.NET 1.1的基础上)的Web应用开发框架.但是,后来一直做的都是桌面类的应用,比如Smar ...
- Android PC投屏简单尝试—最终章2
源码地址:https://github.com/deepsadness/AppRemote 上一章中,我们简单实现了PC的投屏功能. 但是还是存在这一些缺陷. 屏幕的尺寸数据是写死的 不能通过PC来对 ...
- Android PC投屏简单尝试—最终章1
回顾之前的几遍文章,我们分别通过RMTP协议和简单的Socket 发送Bitmap图片的Base64编码来完成投屏. 回想这系列文章的想法来源-Vysor,它通过 USB来进行连接的.又看到了 scr ...
- Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)
代码地址 :https://github.com/deepsadness/MediaProjectionDemo 想法来源 上一边文章的最后说使用录制的Api进行录屏直播.本来这边文章是预计在5月份完 ...
- Android PC投屏简单尝试- 自定义协议章(Socket+Bitmap)
代码地址 :https://github.com/deepsadness/MediaProjectionDemo 效果预览 投屏效果预览 简单说明: 使用Android MediaProjection ...
- 大数据的可视化:bigvis包的简单尝试
转载自:http://site.douban.com/182577/widget/notes/10568279/note/273907035/ bigvis包是Hadley Wickham的一个新作品 ...
- 【Unity入门】软件Unity Hub和Unity的安装和简单尝试
目录 软件准备: 下载Unity Hub和需要的Unity Unity Hub配置: Unity3D配置: 开始新建项目: 简单尝试: 简易Demo: ①先认识场景的创建工具: ②创建场景对象 ...
- 对迅雷下载进行投毒的简单尝试
对迅雷下载进行投毒的简单尝试 2015-9-22 17:52:14教程43条评论 示例: http://www.163点com/typcn233.zip (把 点 换成 . ) 如果你用浏览器直接访问 ...
- 用MapReduce实现WordCount(简单尝试MapReduce)
前言 MapReduce不需要"分割",框架已经做好这一步了. 只需要进行"线程步骤"--Mapper,而且无需计算如何分割(这是Combine的工作) 和&q ...
最新文章
- SAP WM初阶之IM层面货物移动后WM层面自动完成TO创建和确认
- mysql数据库入门教程(11):视图讲解大全
- Linux查看当前http连接,51CTO博客-专业IT技术博客创作平台-技术成就梦想
- 国内最大最全的交换技术专区上线
- Spring 三种bean装配的方式
- 数值分析 有效数字计算
- 表格对角线两边打字_word单元格斜线 word表格中斜线表头制作过程及表头斜线两边打字怎么操作...
- 第22节 NAT(网络地址转换)—实现公网IP和私网IP之间的转换
- 学习Mybatis框架(一)
- 小黄鸡 php,小黄鸡simsimi接口
- 新版《鹿鼎记》史上最低分!今天用数据分析告诉你韦小宝跟哪个老婆最亲
- 常用函数式接口及Stream流
- 【软件测试】测试员vs测试工程师,你是测试员还是测试工程师?
- Excel公式与函数——每天学一个
- java cloassLoader大仙儿
- Vue项目实现web端第三方分享(qq、qq空间、微博、微信)
- js调试技巧(通过debugger调试)
- 放大电路基本性能指标
- JavaScript Function中你可能不知道的知识点
- spring-cloud-stream通道多线程并发消费
热门文章
- 鞋之语告诉你如何开好一个新式洗鞋店
- LG OnScreen Control 软件提示‘镜像显示器‘ BUG
- 木木的常用软件点评(1)------系统必备软件篇
- springboot毕设项目基于微服务的闪聚支付系统设计vwt4i(java+VUE+Mybatis+Maven+Mysql)
- [转载]我爱我妻----让男人看一遍哭一遍的文章
- c++ pair 用法详解
- python接口自动化测试框架实战从设计到开发_【B0753】[java视频教程]Python接口自动化测试框架设计到开发完整版视频教程 it教程...
- 关于我求是不是质数的一个错误,输入9判断是质数的原因
- VMware虚拟机(ubuntu)使用过程中的一些问题及解决方法
- php导出成word试卷,依据word模板批量生成试卷