当前位置: 首页 > 后端技术 > Node.js

【编译】AST实现函数错误自动上报

时间:2023-04-03 12:29:05 Node.js

前言身边有朋友问我如何在错误监控中给函数自动添加错误捕获。今天我们就来说说技术是如何实现的。先说原理:在编译代码的时候,利用babel的loader来劫持所有的函数表达式。然后使用AST(抽象语法树)修改函数节点,将try/catch包裹在函数的外层。然后在catch中使用sdk捕获并上报运行时的错误信息。如果您对编译和打包感兴趣,那么本文适合您。本文涉及以下知识点:[x]AST[x]npmpackagedevelopment[x]Babel[x]Babelplugin[x]Webpackloader实现效果开发环境前:varfn=function(){console.log('hello');}上线环境后:varfn=function(){+try{console.log('hello');+}catch(error){+//sdk报错+ErrorCapture(error);+}}Babel是什么Babel是一个JS编译器,主要用于将ECMAScript2015+版本的代码转换为向后兼容的JavaScript语法,使其可以在当前和旧版本的浏览器或其他环境中运行。简单的说,就是一个源码到另一个源码的编辑器!以下列表是Babel可以为你做的:语法转换通过Polyfill(通过@babel/polyfill模块)添加目标环境中缺失的特性源代码转换(codemods)其他Babel运行分为三个主要阶段,请记住:解析->转换-??>生成,后面会用到。在本文中,我们将编写一个Babel插件npm包,用于在编译期间转换代码。babel-plugin环境搭建这里我们使用yeoman和generator-babel-plugin搭建插件的脚手架代码。安装:$npmi-gyo$npmi-ggenerator-babel-plugin然后新建文件夹:$mkdirbabel-plugin-function-try-actch$cdbabel-plugin-function-try-actch生成npm包development项目:$yobabel-plugin此时项目结构为:babel-plugin-function-try-catch├─.babelrc├─.gitignore├─.npmignore├─.travis.yml├─README.md├─包锁.json├─package.json├─测试|├─index.js|├─固定装置||├─范例|||├─.babelrc|||├─actual.js|||└expected.js├─src|└index.js├─lib|└index.js这是我们的Babel插件,命名为babel-loader-function-try-catch(为了阅读方便,下面简称为plugin!)。至此npm包环境搭建完成,代码地址。调试插件的ast开发工具本文前面提到,Babel的运行主要分为三个阶段:解析->转换-??>生成。在每个阶段,babel官方都提供了核心库:babel-core。Babel的核心库提供了编译和转换代码的能力。通天塔类型。提供AST树节点的类型。巴贝尔模板。可以将普通字符串转成AST,提供更方便的使用需要安装在插件根目录下的工具包:npmi@babel/core@babel/parserbabel-traverse@babel/templatebabel-types-S打开插件src/index.js的编辑:constparser=require("@babel/parser");//首先定义一个简单的函数letsource=`varfn=function(n){console.log(111)}`;//parsedastletast=parser.parse(source,{sourceType:"module",plugins:["dynamicImport"]});//打印出来看是否正常console.log(ast);terminal执行nodesrc/index.js后会打印如下结果:这是fn函数对应的ast,第一步解析完成!获取当前节点的AST,然后我们使用babel-traverse遍历对应的AST节点。我们想找到所有可以写成FunctionExpression的函数表达式:打开插件的src/index.js编辑器:constparser=require("@babel/parser");consttraverse=require("babel-traverse")。default;//模拟要转换的源代码letsource=`varfn=function(){console.log(111)}`;//1.解析letast=parser.parse(source,{sourceType:"module",plugins:["dynamicImport"]});//2.Traverse+traverse(ast,{+FunctionExpression(path,state){//函数节点+//做一些事情+},+});所有函数表达式都将转到FunctionExpression,然后我们就可以在里面修改了。参数path用于访问当前节点信息path.node,父节点的方法path.parent也可以像DOM树一样访问。修改当前节点的AST。接下来要做的就是劫持FunctionExpression中的函数内部代码,然后放到try函数中,在catch中加入报错sdk代码段。获取函数体内部代码上面定义的函数是varfn=function(){console.log(111)}那么函数内部的代码块就是console.log(111),可以使用path获取AST这段代码的信息,如下:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;//模拟要转换的源代码letsource=`varfn=function(n){console.log(111)}`;//1.解析letast=parser.parse(source,{sourceType:"module",plugins:["dynamicImport"]});//2.遍历traverse(ast,{FunctionExpression(path,state){//函数表达式会进入当前方法+//获取函数当前节点信息+varnode=path.node,+params=node.params,+blockStatement=node.body,+isGenerator=node.generator,+isAsync=node.async;+//你可以尝试打印并查看结果+console.log(node,params,blockStatement);},});终端执行nodesrc/index.js,可以打印查看当前函数AST节点信息。创建try/catch节点(两步)和创建新节点可能有点陌生(fu)陌生(za),但是我总结了我个人的经验给大家(仅供参考)。首先需要知道当前新增代码段的声明是什么,然后使用@babel-types创建。第1步:那么我们如何知道它的表达式声明类型是什么?这里我们可以使用astexplorer在AST中找到它的类型表达式。如上截图所示,AST中try/catch的类型是TryStatement!第二步:然后去@babel-types官方文档找到对应的方法,根据API文档创建即可。如文档所示,创建try/catch的方法是使用t.tryStatement(block,handler,finalizer)。一句话新建一个ast节点总结:使用astexplorer找到你要生成的代码的类型,然后根据类型在@babel-types文档中找到对应的使用方法并使用!然后创建一个try/catch,只需要使用t.tryStatement(trycodeblock,catchcodeblock)。try代码块表示try中的函数代码块,即原函数体中的代码console.log(111),可以直接通过path.node.body获取;catchcodeblock表示catch代码块,也就是我们要转换错误收集上报的sdk的代码ErrorCapture(error)可以使用@babel/template生成。代码如下所示:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;constt=require("babel类型");consttemplate=require("@babel/template");//0.定义一个挂起函数(mock)letsource=`varfn=function(){console.log(111)}`;//1.解析letast=解析器。parse(source,{sourceType:"module",plugins:["dynamicImport"]});//2.遍历traverse(ast,{FunctionExpression(path,state){//函数节点varnode=path.node,params=node.params,blockStatement=node.body,//function函数的内部代码,将函数内部代码块放入trynodeisGenerator=node.generator,isAsync=node.async;+//创建catch节点中的代码+varcatchStatement=template.statement(`ErrorCapture(error)`)();+varcatchClause=t.catchClause(t.identifier('error'),+t.blockStatement(+[catchStatement]//catchBody+)+);+//创建ast+vartryStatementfortry/catch=t.tryStatement(blockStatement,catchClause);}});创建新的功能部分点,把上面定义的try/catch放到函数体中:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;constt=require("babel类型");consttemplate=require("@babel/template");//0.定义一个挂起函数(mock)letsource=`varfn=function(){console.log(111)}`;//1.解析letast=parser.parse(source,{sourceType:"module",plugins:["dynamicImport"]});//2.遍历traverse(ast,{FunctionExpression(path,state){//函数节点varnode=path.node,params=node.params,blockStatement=node.body,//函数函数内部代码,将函数内部代码块放入trynodeisGenerator=node.generator,isAsync=node.async;//在catch节点中创建代码varcatchStatement=template.statement(`ErrorCapture(error)`)();varcatchClause=t.catchClause(t.identifier('error'),t.blockStatement([catchStatement]//catchBody));//创建try/catchastvartryStatement=t.tryStatement(blockStatement,catchClause);+//创建一个新节点+varfunc=t.functionExpression(node.id,params,t.BlockStatement([tryStatement]),isGenerator,isAsync);+//打印以查看是否成功+co??nsole.log('当前节点为:',func);+console.log('当前节点下的自身节点为:',func.body);}});这时候在终端节点src/index中执行上面的代码。js:可以看到此时我们在一个函数表达式体中创建了一个try函数(TryStatement)最后,我们需要替换原来的函数节点:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;constt=require("babel-types");consttemplate=require("@babel/template");//0.定义一个挂起的函数(mock)letsource=`varfn=function(){...//1.解析letast=parser.parse(source,{...//2.遍历traverse(ast,{FunctionExpression(path,state){//函数节点varnode=path.node,params=node.params,blockStatement=node.body,//函数函数内部代码,将函数内部代码块放入try节点isGenerator=node.generator,isAsync=node.async;//在catch节点创建代码varcatchStatement=template.statement(`ErrorCapture(error)`)();varcatchClause=t.catchClause(t.identifier('error'),...//创建try/catchastvartryStatement=t.tryStatement(blockStatement,catchClause);//创建新节点varfunc=t.functionExpression(node.id,params,t.BlockStatement([tryStatement]),isGenerator,isAsync);+//替换原来的节点+path.replaceWith(函数);}});+//将新生成的AST转换为Source源代码:+returncore.transformFromAstSync(ast,null,{+configFile:false//屏蔽babel.config.js,否则会注入Polyfill,调试困难+}).代码;“loader是一个导出函数的node模块”,也就是说loader是一个暴露出来的node模块。由于是node模块,所以基本上可以写成如下样子:module.exports=function(){//...};编辑src/index.js如下截图:边界条件处理我们不需要为所有函数都加上try/catch,所以我们还有一些边界条件要处理1.如果有trycatch包2.防止循环3.唯一需要trycatch的就是语句,比如()=>0body4.如果函数内容少于满足上述条件的行数,则returnoff!代码如下:length<=LIMIT_LINE){return;}最后我们发布到npm平台使用。由于篇幅和阅读难度,本文特意省略本地调试过程,如需调试请移步【【使用AST自动为函数添加报错-sequel】关于npm包本地开发调试]()。如何使用npminstallbabel-plugin-function-try-catchwebpack配置规则:[{test:/\.js$/,exclude:/node_modules/,use:[+"babel-plugin-function-try-catch","babel-loader",]}]效果如下图所示:最后看下一篇关于npm包本地调试的文章:更多npm包本地开发调试,请付费以后注意分享,谢谢。参考:完整代码地址请点击BabelPluginManual点击