vue-devtools技术揭秘

作者: 习惯水文的前端苏 | 来源:发表于2023-01-12 16:57 被阅读0次

    \bullet 前言

        说实话,我几乎没用过devtools,日常debugger已经足够了。不过它的实现还是让我挺感兴趣的,但这是一个体量巨大的项目,一篇文章肯定说不清楚,因此本文的目的就只进行流程梳理,以了解其实现思路为主

    \bullet 初始化

        使用npm run dev启用示例项目,这将被作为iframe加载到页面

        当页面load后,将会进行控制面板的初始化

        \alpha installHook

            在全局安装一组通信接口,这将用于vue侧与devtools的通信工作

            而vue包和devtools包作为两个独立体,又共用着同一个页面窗口,则最直接的通信方式应该就是window了

        \beta initDevTools

            这会创建一个vue应用,也就是我们在chrome控制台看到的如下部分,在使用devtools调试应用时的操作逻辑也都在这里

            既然这是一个vue应用,那vue相关的代码就可省略,关键是去梳理devtools侧的逻辑是如何被接入的

            这里的关键点在inject和new Bridge

                \alpha inject 

                    使用JavaScript动态的创建script标签,用于加载backend的创建逻辑

                \beta new Bridge

                        定义:

                            前端:app-frontend,即由vuejs创建的控制面板中显示的内容

                            后端:app-backend-core

                        这是一个继承自events库的符合发布订阅者模式的自定义类,用于前后端通信,比如在后端初始化结束后会通知前端自动选中第一个App应用;又或者,当我们在前端切换不同的组件实例时,要通知后端做相应的数据采集等

    \bullet 创建backend

        前一部分,我们梳理了初始化流程,它为我们后续的操作预置了一系列接口,这包括对backend的初始化

        进入initBackend,代码如下

        在具体分析之前,我们先将代码做下优化(在不影响理解执行逻辑的前提下)

            首先,在数据采集阶段我们会提到,vue2和vue3发出的初始化请求不同:init和app:init,每一组都是由"已连接(如hook.Vue)"和"待连接(如hook.on)"两部分组成,因此可以强制视为已连接,它们分别对应的订阅消息如下

                \alpha vue2

                \beta vue3

            接着,我们打开_legacy_getAndRegisterApps函数,我们发现它核心还是调用的registerApp 

            最后,else分支下的connectBridge函数在初次建立connect时也会执行,则不做区分,每次都重新建立应该也问题不大,毕竟当前仍处于初始化阶段,并不必担心数据状态丢失的情况

        即如是,则去除"多余"代码后长这样

            \alpha createBackendContext

                创建的上下文是唯一的,并不会因为多个根实例而多次创建

            \beta registerApp

                这将进入应用注册流程

            \gamma connect

                    进行bridge和hook的消息订阅

    \bullet 应用注册

        经过初始化阶段,我们已经埋好了通信接口,也知晓了在vue源码中存在"消息发布"的代码

            \alpha vue2中触发的是"init"事件

            \beta vue3触发的是"app:init"事件

            \gamma 在devtools中分别对应如下消息订阅

        有了订阅,亦有了发布,就可以进行应用注册了,其入口在backend创建中已经被找到

            \alpha getBackend

                框红一的位置,会根据当前使用的vue版本安装对应的backend

                这本质上是一组操作接口,用于收集组件状态

                2和3版本都共同配置了frameworkVersion、features和setup选项

                    \vdash frameworkVersion

                        该属性代表与vue版本对应的backend版本,这在初始化过程中由vue中发起的init或app:init请求中的参数可以拿到

                    \vdash features

                        该属性是backend2特有的,值为['flush'],该标识会影响后续对组件状态的收集   

                    \vdash setup

                        该属性用于注册hook回调,目前来看其是在做一些参数的修正工作

                        从代码组织上来看,它是发布订阅模式的实现

                            \lceil 订阅消息:这些回调函数将被缓存起来备用

                            \lceil 发布消息:在初始化面板过程中调用,以获取页面中组件树的相关信息

                    \vdash setupApp

                        该属性是只针对backend2版本的,包含了部分兼容性的代码,比如:对vue原型上的方法进行重写

                        再比如,通过mixins向vue组件中混入一些生命周期,其本质上也是在重写,它向vue的hooks中埋点了devtools的采集接口

            \beta createAppRecord

                其实,此时才算真正意义上完成了所有的准备工作,所有的通信接口都被正确的安装,也找到了当前版本最契合的操作列表(backend),这其实可以被类比作vue的beforeCreate和created流程,而接下来的则是应用程序的beforeMout和mounted流程

                \vdash 获取根组件实例

                    这将触发对应backend中在setup中订阅的消息,其实获取的就是在vue中触发init传递的根instance

                \vdash 判断是否启用了devtools

                    从取值来源来看,即每个组件实例上的devtools配置

                    比如配置根实例对devtools不可见后,控制面板将会是"暂无数据"状态

                \vdash 获取name和id

                    name即在业务代码中配置的name属性对应的值,如果没有,则devtools会内置生产一个

                    id被当作组件的唯一标识并挂载到对应的实例对象上

                \vdash 获取组件对应的页面dom

                    即实例上的el属性

                \vdash 生成当前根组件的配置对象并保存到上下文中,以便在多根情况下快速区分或查找

                \vdash onBeforeMount

                    devtools中是没有相关的消息订阅的,个人以为,此处可以视为一个与用户侧的通信接口,其相当于vue组件中的beforeMount钩子,标识整个应用程序的即将挂载

                则在我们自己的项目中,可以去监听该事件,并通过payload上挂载的__VUE_DEVTOOLS_APP_RECORD__属性获取devtools的api从而达到获取或影响内部状态的效果

                \vdash onMounted

                    接着会与控制面板进行消息互换

                    (这里的消息可以认为是在告知控制面板:我马上就要给你发消息了,你准备一下......。也就是说,此时控制面板会做一些预备工作,这并不是必须的,但是却能提高程序的执行效率,此思路很值得学习......)

                    最后等待应用程序“mounted”之后执行控制面板的渲染

    \bullet 渲染控制面板

        \alpha 菜单           

            和我们日常开发需求一样,菜单列表一般都是通过后台获取的

                \vdash 获取菜单列表数据

                    其列表数据是保存在上下文的appRecords上的

                        由于存在多根的情况,因此需要一种增量添加的机制,即在上文提到的注册当前app时发出的BridgeEvents.TO_FRONT_APP_ADD消息       

                        它们之间通过postMessage和addEventListener完成消息收发

            \vdash 回填页面

                    那么只需要页面中对应的值是响应式的,就可以在收到app列表更新时自动渲染到页面了

            \vdash 设置默认选中

                    当应用初始化后,默认选中第一个菜单页,这只需要监听app列表并取第一个值即可

                这里有一个初看可能会比较懵逼的地方(至少我是没用过这种写法),那就是router.push是没有传递对应的path或name参数的,一开始我以为是devtools在接入vue-router时做的重写,但是仔细看了相应的代码,并不是!

                能这么用,是因为如果不传递url或path或name时,vue-router会默认使用当前路由,并根据路由信息去与params中的值做匹配以生成路由地址

        \beta 组件列表

            上一步,我们通过路由跳转,已经打开了对应的页面,这会触发对组件树列表的获取

            这会再一次的调用selectApp函数

            这实际上会做两件事

                \vdash 创建时间穿梭(其他文章单独分享)

                \vdash 拉取组件列表

                    除了启动TimeLine(时间穿梭)创建外,也会去执行component tree的获取

                    省略中间过程,这调用的即创建backend时注册的通信接口

                    其核心是从根实例开始递归获取子组件的$children属性,结果如下

                    最后,前端只需要对响应对象设置值即可

                    还有一点,由于hmr下,组件的创建或者更新是通过url单独拉取并替换的,因此这不会触发root的mounted,也就无法重新收集,因此需要在vue指定的增删改位置向devtools发送通知(在vue2中发起的"flush"通知)

                  在devtools侧,只需要将该消息再转发给控制面板即可做到组件列表的同步

        \gamma 组件状态

            \vdash 状态读取

                没看源码之前,我觉得这个需求是很简单的,因为在获取tree list的时候已经在前端保持了一份列表缓存

                所以,在选中对应的组件的时候,直接从实例上取我觉得是可行的

                但是实际上,获取的组件列表是"残缺的",它只包含了渲染列表项所需的内容

                因此在具体选中后,还需要单独去获取对应实例的相关状态信息,即上图中的loadComponent

                相比实例上对应的信息要如何获取而言,我觉得更重要的是,为什么要这么设计?我不是库作者,只能想到如下两者:

                    \cdot 减少内存占用,提升组件列表渲染效率

                    \cdot 按需加载

            \beta 状态更新

                在获取状态的时候,是以对象形式返回的,则由于是引用类型,直接更新是可以触发控制面板的更新的

                但这仅作用于.map中生成的对象的value属性,而对实例本身并不产生影响,因此无法达到同步更新页面的效果,且切换后再切回来,值还是上一次的

                因此,需要从实例修改

                即我们在组件中定义的data中的属性,这是一个被收集过的属性,并在set发生时向相关依赖广播更新,这一点,和在项目中使用this.xxx = new value的效果是一样的

                最后,就只需要重新将更新后的组件状态向控制面板更新一份即可

    \bullet 映射关系

        这体现在两处:选择页面中的dom时,能定位到控制面板对应的组件;选中组件能滚动到页面对应的dom

        \alpha 映射到dom

            由于vue在patch过程中会生成el属性标识一个组件的根

            则只要拿到这个根,就可以调用原生dom api做滚动

        \beta 映射到控制面板

            devtools和业务项目共用同一个window,因此可以通过绑定事件来检测用户行为

            当用户点击选中后,查找对应的组件,由于可能存在多层嵌套关系,因此也需要找到所有的父组件以便在选中后默认打开

        然后通知控制面板选中对应的组件,并重新拉取对应的状态信息即可

    \bullet chrome

        我们在本地开发环境通过webpack设置开发服务器并以转发的方式将示例项目作为iframe嵌入到devtools中,说到底这只是一个多页的vue应用,想要能被用户访问,还需要将其"部署"至chorme中才行

        这也不难,只需要将devtools的入口代码按规定引入并调用即可

            \alpha installHook

                当前不是通过iframe进行的用户项目的加载,因此我们无法得知是否是vue项目且是否已完成加载,但hooks又是vue和devtools交互的桥梁,因此需要提前进行加载

            \beta initDevTools

                我们知道,installHook会向window挂载__VUE_DEVTOOLS_GLOBAL_HOOK__属性,我们只需要对其轮询,等监听到值后去触发devtools的初始化逻辑即可,剩下的,就基本和之前的分析是一模一样的了   

    \bullet 写在最后

        至此,其实现流程就了解的差不多了,不过还有一些值得深入研究的细节点:

            \alpha 为什么vue3中不需要设置setupApp和features.flush属性

            \beta 第三方库如何接入devtools(如vux和vue-router)

            \gamma 如何将其改造成chrome扩展程序

            \delta 如何与编辑器建立连接(vscode)

            \varepsilon 4种通信方式的实现与总结

            \zeta TimeLine如何实现

    相关文章

      网友评论

        本文标题:vue-devtools技术揭秘

        本文链接:https://www.haomeiwen.com/subject/yutaqdtx.html