如何阅读“深度嵌套”&“复杂引用关系”的react+ts项目?不如自己写个loader来缓解一下吧~
如何读懂'嵌套很深'&引用关系复杂'的react+ts项目?不如我们写一个loader来缓解吧~简介?这篇文章描述了我做这个功能的整个过程,不断的掉坑里爬出来,比结果的过程更有趣,所以我想放它分享它。1.项目太“复杂”,要为找个组件发愁随着项目越来越大(cha),深层代码模块会越来越多。例如,你可以看到页面上显示了一个'名片盒',但是你可能需要搜索几分钟才能找到这个'名片盒'的代码写在哪个文件中。如果你刚收到这个项目而你前几年没有维护,那么找代码的过程会很痛苦。ReactDeveloperTools也没有很好的解决这个问题。需要明确的是,所谓的“复杂”可能只是“糟糕”的代码编写。代码结构设计不合理,如过于抽象。很多人认为只要不断地提取组件代码,注释越少越好,这就是写的好代码,其实这只是'比较初级',代码是写给人看的,代码逻辑清晰,易读易发现核心功能节点才是好代码,往往离群太远的组件会降低性能。毕竟,产生新的作用域是不可避免的。很多人写react比写vue更容易过度抽象。这里我想到的一种解决方案是这样的,给每个元素添加一个'address'属性:(这次以react+Ts项目为例)比如一个导出的按钮组件,代码位置是'object/src/page/home/index.tsx'然后我们可以写button我们可以悬停显示路径,或者通过查看consoleimg,input等标签的Path信息不能使用伪元素需要打开console查看解决方法二。选择谷歌浏览器插件虽然很容易为标签插入属性,但无法读取插件所在的开发路径。这个解决方案可以排除。vscode插件可以很好的读取开发文件所在的文件夹,但是添加路径属性会破坏整体的代码结构,而且不好应对用户主动删掉一些属性和区分开发环境和生产环境。毕竟生产环境我们不会去处理的。loader对于特定类型的文件,它控制只在'开发环境'的元素标签中注入'路径属性',非常方便的获取当前文件本身的路径。本文只是一个小功能插件。虽然没有解决大问题,但是思考的过程还是挺有意思的。效果图当鼠标放在元素上时,会显示元素的文件夹路径。3.样式方案标签属性赋值后,我们需要考虑如何获取。显然,我们这次需要使用属性选择器,将所有标签属性设置为带tipx的所有标签都检索出来,然后我们通过伪元素before或after显示文件地址。你还记得attr吗?这个属性被css代码用来获取dom标签的属性,我们可以这样写:[tipx]:hover[tipx]::after{content:attr(tipx);白颜色;显示:弹性;位置:相对;对齐项目:居中;背景色:红色;证明内容:居中;不透明度:.6;字体大小:16px;填充:3px7px;border-radius:4px;}4.解决方案一:loader自带正则表达式简单粗暴的方式必须是正则表达式,匹配所有的开始标签,比如把是古代程序员经常使用的标签。"上述情况会被正则化误判为真正的标签,但是这个字符串不应该被修改。2:重名
自定义标签名这种标签出现的几率很小,但是有重名的几率3:单引号双引号conststr="标签的外层已经有双引号
"//替换后出错conststr="标签外层已经有双引号 "不好判断外层是单引号还是双引号4:styled-components的写法我们不能把它分开,比如下面这样:importstyledfrom"styled-components";exportdefaultfunctionHome(){constMyDiv=styled.div`border:1pxsolidred;`;return123}6.方案二:AST树&获取当前文件路径终于到了主要任务,将代码解析成树结构可以让解析更舒服,更好的插件用于转换AST树的包括esprima和recast。我们可以把步差分为三个部分,代码到树结构,循环遍历到树结构,树结构到代码。当前文件路径webpack已经注入到loader中,可以获取到this.resourcePath,但是会是一个全局路径,即电脑从根目录到当前目录的完整路径,如果需要的话,我们可以拆分它进行显示。我们写了loader.js的代码,在执行“第一步”分析的时候报错,因为不理解jsx语法。constesprima=require("esprima");module.exports=function(context,map,meta){让astTree=esprima.parseModule(context);控制台日志(astTree);this.callback(null,context,map,meta);};七。如何生成和解析react代码这时候我们可以传入一个参数jsx:true:letastTree=esprima.parseModule(context,{jsx:true});遍历这棵树由于树结构可能很深,我们可以使用效用函数estraverse进行遍历:estraverse.traverse(astTree,{enter(node){console.log(node);},});这时候报错了,大家一起来欣赏一下:解决遍历问题在网上找到了解决办法,就是用一个专门处理jsxElement的循环插件yarnaddestraverse-fb://beforereplacementconstestraverse=require("estraverse");//替换后constestraverse=require("etraverse-fb");可以正常循环:生成代码我平时用来解析纯js代码的工具函数在这里escodegen");module.exports=function(context,map,meta){letastTree=esprima.parseModule(context,{jsx:true});estraverse.traverse(astTree,{enter(node){}});//这里是AST树转换成js代码context=escodegen.generate(astTree);this.callback(null,context,map,meta);};然后报错:但是这时候的问题肯定是在将AST树恢复到jscode这一步的,我搜索了escodegen的各种配置,没有找到可以解决当前问题的配置。当时只好再找其他插件八。recastrecast也是一个很好用的AST转换库,recast官网地址,但是没有自带好用的遍历方法,用法如下:constrecast=require("recast");module.exports=function(context,map,meta){//1:生成树constast=recast.parse(context);//2:转换树constout=recast.print(ast).code;上下文=出;this.callback(null,context,map,meta);};然后我们无奈放弃,只取它的树到代码的函数//替换前context=escodegen.generate(astTree);//替换后context=recast.print(astTree).code;9.找到目标&分配属性前后端进程打通了。现在有必要为标签分配属性。这是我的总结:constpath=this.resourcePath;estraverse.traverse(astTree,{enter(node){if(node.type==="JSXOpeningElement"){node.attributes.push({type:"JSXAttribute",name:{type:"JSXIdentifier",name:"tipx",},value:{type:"Literal",value:path,},});}},});过滤掉JSXOpeningElement类型的元素node.attributes.push将要添加的属性放入元素JSXIdentifier属性的属性队列中属性名类型Literal属性值类型结合recast确实可以很好的还原代码,但是真的结束了吗?十。ts有话要说!当我把开发好的loader放到实际项目中的时候,真的是傻眼了。假设开发的代码如下:importReactfrom"react";导出默认函数Home(){接口C{名称:字符串;}constc:C={name:"金毛猎犬",};returnhomepage
;}会产生如下错误信息:很容易理解,接口不能随意使用,因为这是ts的语法,我们不会懂js的,第一时间想到的是ts-loader,试过让ts-loader先编译,然后我们解析它编译出来的代码,但是不行Esprima不能直接理解ts语法,ts-loader不能很好的解析jsx,解析出来的代码也不能匹配我们写的解析AST树的各种代码。我曾经陷入泥潭。全能babel-loader勇敢的站了出来!十一。Babel改变一切我们把它放在最上面执行:{test:/\.(tsx)$/,use:[require.resolve("./loaders/loader.js"),{loader:require.resolve("babel-loader"),options:{presets:[[require.resolve("babel-preset-react-app")]],},},],},当时我为自己鼓掌4.6s,终于通过了,但是不能就这样结束了。由于文件已经经过babel处理,理论上可以去掉我们之前对jsx的特殊处理//之前的constestraverse=require("etraverse-fb");//现在的constestraverse=require("etraverse");//之前的le??tastTree=esprima.parseModule(context,{jsx:true});//当前的le??tasttree=esprima.parseModule(context);循环不再是jsx,循环体需要大改//之前的estraverse.traverse(astTree,{enter(node){if(node.type==="JSXOpeningElement"){node.attributes.push({类型:“JSXAttribute”,名称:{类型:“JSXIdentifier”,名称:“tipx”,},值:{类型:"文字",值:路径,},});}},});//当前etraverse.traverse(astTree,{enter(node){if(node.type==="ObjectExpression"){node.properties.push({type:"Property",key:{type:"Identifier",name:"tipx"},computed:false,value:{type:"Literal",value:path,raw:'""',},kind:"init",方法:false,速记:错误的,});}},});现在我们启动项目的时候可以解析ts语言了,但是。。。我放到实际项目中,又出问题了!12、实际开发时报错按照我上面配置的方法,原封不动的放到官方项目中,却报错了。简单说一下,错误的原因是package.json需要给babel指定type:"babel":{"presets":["react-app"]},这里是我的babel版本:"@babel/core":"7.12.3","babel-loader":"8.1.0","babel-plugin-named-asset-import":"^0.3.7","babel-preset-react-app":"^10.0.0",你觉得这不是Bug?十三。真的需要尝试一下!根据每个项目的特点进行独特的配置,但是几百页的代码中只有3页报了奇怪的错误,最后选择了用trycatch来包裹整个过程,这也是最严谨的做法,毕竟,只是一个辅助插件,应该不会影响主进程的进度十四。完整代码constesprima=require('esprima');constestraverse=require('estraverse');constrecast=require('recast');module.exports=function(context,map,meta){constpath=this.资源路径;让astTree='';尝试{astTree=esprima.parseModule(context);estraverse.traverse(astTree,{enter(node){if(node.type==='ObjectExpression'){node.properties.push({type:'Property',key:{type:'Identifier',name:'tipx'},computed:false,value:{type:'Literal',value:path,raw:'""',},kind:'init',method:false,shorthand:false,});}},},});context=recast.print(astTree).code;}catch(error){console.log('>>>>>>>>错误');}returncontext;};配置{test:/\.(tsx)$/,use:[require.resolve("./loaders/loader.js"),{loader:require.resolve("babel-loader"),options:{presets:[[require.resolve("babel-preset-react-app")]],},},],},十五。我的收获?虽然最后的代码不长,但是过程真的很坎坷,一直在尝试各种库,要想解决问题就得把这些库挖到底。what是不是,就这一次让我对编译有了更深的理解整个组件只能标记组件代码的位置,但不能很好的指出其parent的文件位置,需要打开console查看其父标签的tipx属性,但是至少在某个小组件出现问题的时候,恰好这个小组件的命名不规范,设置的有点深,我们不熟悉代码,然后尝试用这个loader找到他。