vuereact & vue & angular

vue-devtools技术揭秘

2023-01-12  本文已影响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如何实现

上一篇 下一篇

猜你喜欢

热点阅读