前言身边有朋友问我如何在错误监控中给函数自动添加错误捕获。今天我们就来说说技术是如何实现的。先说原理:在编译代码的时候,利用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是一个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包开发项目:$yobabel-plugin此时项目结构为:babel-plugin-function-try-catch├─.babelrc├─.gitignore├─.npmignore├─.travis.yml├─README.md├─package-lock.json├─package.json├─test|├─index.js|├─fixtures||├─example|||├─.babelrc|||├─actual.js|||└expected.js├─src|└index.js├─lib|└index.js这是我们的Babel插件,命名为babel-loader-function-try-catch(为了阅读文章方便,以下简称插件!)。至此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)}`;//分析astletast=parser.parse(source,{sourceType:"module",plugins:["dynamicImport"]});//打印看看是否正常console.log(ast);终端执行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",插件:["dynamicImport"]});//2.Traverse+traverse(ast,{+FunctionExpression(path,state){//函数节点+//dosomestuff+},+});所有的函数表达式都会转到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;//mock待转换源代码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;const=require("babel类型");consttemplate=require("@babel/template");//0.定义一个挂起的函数(mock)letsource=`varfn=function(){console.log(111)}`;//1。parseletast=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节点创建代码]//catchBody+)+);+//创建try/catch的ast+varttryStatement=t.tryStatement(blockStatement,catchClause);}});新建一个函数节点,将上面定义的try/catch插入到函数体中:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;constt=require("babel-types");consttemplate=require("@babel/template");//0,定义一个pendingfunction(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,//函数函数内部代码,将函数内部代码块放入try节点isGenerator=node.generator,isAsync=node.async;//在catch节点创建代码varcatchStatement=template.statement(`ErrorCapture(error)`)();varcatchClause=t.catchClause(t.identifier('error'),t.blockStatement([catchStatement]//catchBody));//创建try/catchastvarttryStatement=t.tryStatement(blockStatement,catchClause);+//创建新节点+varfunc=t.functionExpression(node.id,params,t.BlockStatement([tryStatement]),isGenerator,isAsync);+//打印看是否成功+console.log('当前节点为:',func);+console.log('当前节点下的自身节点为:',func.body);}});这个时候在终端节点src/index.js中执行上面的代码:可以看到这个时候我们在函数表达式体中创建一个尝试函数(TryStatement)最后,我们需要替换原来的函数节点:constparser=require("@babel/parser");consttraverse=require("babel-traverse").default;const=require("babel类型");consttemplate=require("@babel/template");//0.定义一个挂起的函数(mock)letsource=`varfn=function(){...//1.parseletast=parser.parse(source,{...//2.遍历traverse(ast,{FunctionExpression(path,state){//函数节点varnode=path.node,params=node.params,blockStatement=node.body,//函数函数内部代码,函数内部代码将block放入try节点isGenerator=node.generator,isAsync=node.async;//在catch节点中创建代码varcatchStatement=template.statement(`ErrorCapture(error)`)();varcatchClause=t.catchClause(t.identifier('error'),...//创建try/catchastvarttryStatement=t.tryStatement(blockStatement,catchClause);//创建一个新的节点varfunc=t.functionExpression(node.id,params,t.BlockStatement([tryStatement]),isGenerator,isAsync);+//替换原来的节点+path.replaceWith(func);}});+//ConvertthenewgeneratedASTtoSourcesourcecode:+returncore.transformFromAstSync(ast,null,{+configFile:false//屏蔽babel.config.js,否则会提示添加polyfill会使调试变得困难+}).code;“加载器是一个导出函数的节点模块”,这意味着加载器是一个暴露的节点模块。由于是node模块,所以基本上可以这样写看起来像:module.exports=function(){//...};编辑src/index.js如下截图:我们不需要为所有函数都添加try/catch做边界条件,所以我们要处理一些边界条件1.如果有trycatch包2.防止circleloops3、唯一需要trycatch的就是语句,比如()=>0body4、如果函数内容少于满足上述条件的行数,就返回吧!代码如下:if(blockStatement.body&&t.isTryStatement(blockStatement.body[0])||!t.isBlockStatement(blockStatement)&&!t.isExpressionStatement(blockStatement)||blockStatement.body&&blockStatement.body.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",]}]如下图所示:
