当前位置: 首页 > Web前端 > HTML

Luna:你想要的ReactNative调试工具

时间:2023-03-27 23:53:07 HTML

本文作者:虾皮数购前端团队。一、后台ReactNative(以下简称RN)目前在Shopee的前端团队中被广泛使用。RN虽然有很多优点,但是相对于MobileWeb,它的开发和调试过程并不是那么友好,尤其是运行时的调试。在开发模式上,虽然RN提供了官方的调试工具,但是相对于纯前端浏览器Devtool来说,功能还是比较弱的;在非开发模式下,比如Test和UAT环境,RN代码被打包成一个Bundle,这时候官方的调试工具也没用,不仅阻碍了测试同学问题的复现,也使得开发学生更难定位问题。目前业界虽然有RN调试的工具,但都或多或少存在缺陷(如下图),而且这些工具都是针对开发模式下的调试,打包后的生产环境的调试往往需要人肉去做,效率比较低。因此,一个能够在非开发环境下帮助定位问题的工具显得尤为重要,Luna应运而生。本文将介绍该RN工具关键技术的设计与实现。2.功能介绍我们通过以下图片来了解一下Luna:从图片中可以看出,Luna是一款RN的应用内调试工具,更倾向于解决生产环境调试的痛点。Luna由一个橙色的触发按钮和一个占据屏幕一半的主体组成。Ontology包括四个版块:Log、Network、Redux和Shopee,分别承载日志记录、网络请求查看、Redux树查看、Shopee相关信息查看功能。其中,Log和Network作为核心模块存在,而Shopee和Redux作为Luna提供的公共插件引入。这种Core-Plugin模式是Luna目前的运行模式:默认提供Log、Network等功能,也支持用户编写自定义模块导入Luna。四大板块的作用如下:1)日志板块日志板块接管console.log,将所有的Log和未捕获的错误收集到Luna中,并倒序显示。支持根据Log的类型进行过滤,也支持Log的模糊搜索。如下图所示:2)Network部分Network部分收集了页面发送的请求信息,包括请求状态、请求时间、请求头、请求体、响应头和响应体等,用户可以方便的查看API请求.3)虾皮版块虾皮版块提供了一些ShopeeApp相关的功能,如方便的翻译文案切换、Cookies查看、DataStore存储查看和删除、用户ID/名称和设备系统信息,以及版本号相关信息查看等。这些功能可以帮助开发者更方便地调试应用程序,也可以方便QA更快地重现和定位bug。4)ReduxsectionReduxsection展示了Store(共享数据存储仓库)树,方便用户查看整个Store的状态。3.方案设计3.1总体设计Luna作为一个monorepo多包单仓架构项目,包括三个包模块:Core、ShopeePlugin和ReduxPlugin。其中,Core核心模块包括三部分:Log日志部分、Network网络部分、Plugins插件接入部分。下面将对各个模块的设计一一进行介绍。3.2CoreCore模块是Luna的核心模块。它作为一个单独的npm包存在,提供最基本的功能和插件接入能力。Core模块作为Provider嵌套在组件树的根部,接受业务代码并将Luna插入其中。Core使用mobx作为存储,维护Log日志和Network记录的收集和展示,以及自定义插件的控制等。3.2.1接入方案Luna的灵感来自两个开源调试工具vConsole和Eruda在web上,但是在Luna的接入方案的选择上,我们遇到了一个在MobileWeb中从来没有遇到过的问题:在现代的web开发中,无论是Vue还是React,只要是单页应用,都有将是挂载的根节点,整个组件树将从这个根节点构建。因此调试工具只需要挂在某个根节点下就可以感知整个应用的状态:在ReactNative中,每个页面(View)都有自己的根节点(如下图),不同的页面它们之间没有共同的祖先节点。如果要保证每个页面都能访问Luna,就得对每个页面分别进行注入。不仅访问成本急剧增加,数据保留也成为一个大问题。问题。因此,如何保证所有页面都能访问到Luna,在不同页面保留数据,出错时不影响Luna,同时降低页面访问成本就成了一个难题。那么露娜是怎么做到的呢?首先,Luna将初始化与页面注册分离,并将Luna.init添加到应用程序初始化之前。这将数据收集与页面注册分开,确保页面切换不会导致数据丢失。从“@shopee/luna”导入Luna;Luna.init();然后,Luna使用ShopeePlugin重写了注册ShopeeRNPage的方法,将传入的页面组件用新的组件包裹起来,同时,Luna也Included在里面,将组件作为HOC返回到外层。使用这种注册页面方式注册的每个页面都会自动在页面中包含Luna,无需在每个页面手动引入Luna,并且每个页面也可以访问Luna。最后,Luna还在传入的Component上包裹了一层ErrorBoundary,用于捕获页面产生的运行时错误,这样当页面出现错误时Luna仍然可以访问到,可以在Luna中看到错误信息.3.2.2日志收集顾名思义,日志模块用于展示系统和用户打印的日志。Luna劫持全局变量global.console,收集各类Log;同时,Luna还劫持了console.tron.log来收集Reactotron在开发过程中打印的相关日志;Luna还劫持了ErrorUtils来收集未捕获的错误,并收集到日志Store中。这三类日志是Log部分的数据来源。Luna通过类似中间件的方式劫持全局控制台,在劫持过程中将其添加到Logstore中,然后执行其原有的执行功能。主要代码如下:mixinType.forEach((type)=>{//@ts-ignoreconstoriginConsoleFun=global.console[type];//@ts-ignoreglobal.console[type]=(...params)=>{consoleStore.添加日志(参数,类型);originConsoleFun(...参数);};});};日志显示日志日志包括类型过滤、搜索框和日志列表。由于Luna日志类型多,内容复杂,并且一直处于动态更新状态,很容易造成性能问题。因此,在日志列表的展示部分,我们做了很多性能优化,主要包括两部分,如下图所示:1)嵌套类型展示优化由于与树展示库的兼容性问题开源方案,我们选择自己写一个树形展示组件,解决数据类型复杂、数据量大带来的展示问题。具有以下特点:支持多行文本的展开和收缩,收缩时只显示部分内容;对大数组和对象采用懒加载方案,展开后只显示不到100行的内容,剩下的部分(N),再显示最后的N*100条数据。这种方式避免了大数据显示带来的性能问题;对一行超长文本进行换行控制,每条Log不超过三行,保证每屏Log条数受控。2)列表滑动性能优化Luna'sLog不是一次性加载的,而是实时生成的。这使得在列表滑动过程中可能会产生新的数据,用户经常需要向下滑动才能找到自己打印的日志。因此,Luna也针对滑动的性能做了一些具体的优化:Luna使用FlatList渲染Log列表,同时在采集Log时隐式生成一个ID,作用于FlatList的keyExtractor,提高渲染效率;由于Log是动态生成的,这对FlatList的性能影响很大。为此,Luna将Log列表倒序显示,将最后生成的数据,也就是用户点击Luna时最关心的数据,放在FlatList的最前面,同时打印出时间。这减少了用户滑动的频率;我们还计划为Luna实现更严格的日志分页加载,将显示和存储的日志列表分开。确保动态数据生成时的列表滑动性能。3.2.3NetworkNetwork模块的数据采集来自于XMLHttpRequest。Luna劫持了ReactNative的XMLHttpRequest,重写了open、send和setRequestHeader方法,将每个请求以及与请求相关的字段存储在Network列表中。由于RN的Fetch底层实际上使用了XHR,所以劫持XHR可以实现全覆盖。Network劫持的主要代码如下:exportconstoverrideNetwork=(consoleStore)=>{originOpen=XMLHttpRequest.prototype.open;constoriginSetHeader=XMLHttpRequest.prototype.setRequestHeader;XMLHttpRequest.prototype.open=function(...args){this._xmlItem={openData:args};this.addEventListener("load",()=>{constxmlItem=this._xmlItem;constrequestHeaders=this._requestHeaders;constendTime=newDate().getTime();consttime=endTime-xmlItem.startTime;consoleStore.addNetworkLog({url:this.responseURL,method:xmlItem.openData[0],status:this.status,rspHeader:this.getAllResponseHeaders(),response:this.response,body:xmlItem.sendData,});});originOpen.apply(this,args);};};在Network列表的显示方案中,我们加入了很多细节上的考虑,比如:在末尾显示请求的URLfirstPath;根据响应的状态码设置不同的背景颜色;根据请求时间的长短显示不同的时间单位。这些细节都是在日积月累的使用中做出的小改进,真正让Luna的实际用户体验更上一层楼。3.3Plugins3.3.1插件机制为什么需要插件机制?在介绍什么是外挂机制之前,大家心里可能会有一个疑问,为什么会有外挂机制呢?原因是Luna实现功能时,部分功能是依赖Shopee的SDK实现的;Redux等其他功能可选,用户使用的状态管理框架可能是mbox;为了保持Luna核心模块的纯净,同时保留Luna对非Shopee框架的扩展性,我们解开了这些不必要的耦合,将Shopee模块和Redux模块改造成插件机制,供用户按需引用。什么是插件机制?除了Luna的核心模块,Core还支持自定义插件。Luna提供了两个第一方插件:ReduxPlugin和ShopeePlugin。如果你对自己的APP有自定义需求,也可以很方便的编写自己的插件导入Luna中,如下图所示。3.3.2官方插件Luna同样采用插件机制提供了两个官方插件:ReduxPlugin和ShopeePlugin。这两个包作为单独的npm包引入,供有需要的用户使用。其中:ReduxPlugin作为Redux中间件存在,通过Store.getState获取Redux的状态,显示在界面上。用户可以很方便的找到Redux当前的存储值。ShopeePlugin是一款基于ShopeeReactNativeSDK的插件,专门用于ShopeeApp中的项目开发。它通过Shopee的SDK提供了很多功能。本插件主要是针对虾皮内部开发测试的同学,方便他们在虾皮App中进行调试。3.3.2开发自定义插件除官方插件外,用户还可以自行扩展插件。如何开发Luna插件?Luna的插件机制和Vue的install-use机制非常相似,只是省略了Vue插件的install步骤,只要在Luna提供的use方法中注入组件内容即可。所以其实步骤很简单,只需要两步:编写你的组件,声明名称;将组件和名称导入LunaCore的实例。Luna可以识别你的组件并显示在主界面上,然后你可以在插件中添加你需要的功能。4、未来展望Luna现阶段在Shopee的部分业务中稳定运行,也得到了使用它的开发者和测试者的一致好评。未来,我们将努力实现两个主要目标:1)自动访问Luna。现阶段,Luna接入仍然是侵入式的手动代码接入。未来我们计划通过部署平台在部署过程中自动连接Luna。进去,只在开发测试环境生效,不仅可以做到零代码访问成本,不影响生产环境,还减少了打包代码的大小。2)组件树状态查看器几乎每个Web开发者都会用到ReactDevtool,其中最流行的是Components模块,它展示了开发过程中的整个组件树,以及Props、State和Hooks。在ReactNative端,目前还没有像ReactDevtool这样好用的开发调试工具,而查看RN的状态是开发者的一大痛点,因此Luna计划在未来,在RN上同时查看Log、Network和component状态将不再是难事。