打印 window 对象,PageSpy 是如何处理的?

前端开发者福音,一定要试试的 Web 远程调试工具!

GitHub 仓库地址:github.com/HuolalaTech...

前言

PageSpy 自开源提供服务以来,社区中收到大量用户反馈说想了解 PageSpy 的一些工作原理,这么真挚而又热烈的要求,我们第一时间安排上了。本章就为大家带来深入实现原理系列之「打印 window 对象,PageSpy 是如何处理的?」。

Console 面板除了打印日志外,还支持在 Console 面板底部发送调试代码到客户端,获取运行时的变量信息。

还没有尝试过的小伙伴可以点击 在线体验,接下来将和大家一起进入主题:PageSpy 如何处理复杂数据的交互

设计目标

PageSpy Console 面板最起初的设计目标就是向浏览器控制台的 Console 面板看齐。用户如果打印复杂数据,单纯靠序列化源数据会面临以下问题:

  • 不支持序列化的属性值会丢失;
  • self-reference 的数据会报错;
  • Getter 属性值动态计算的交互特性会丢失;
  • 无法沿着原型链查看数据明细;

上述提到的每一项在各自的场景中都发挥着无比重要的作用。所以可以明确的是:

  • 想要打印复杂数据,肯定是需要先做什么处理后再使用;
  • 后续有查看属性明细的需求,所以需要将数据实体找个地方存起来;

什么是复杂数据?

对于数据「复杂不复杂」是按照如下定义的:

  • 可以点击查看明细的数据都是复杂数据,如 Object / Array / Set / Map 等类型的数据;
  • 一眼就看明白的都是不复杂数据,如 "Hello" / 12345 / true / Symbol(foo) / Error / undefined 等;

在 PageSpy 中,我们将对各种数据进行转换、缓存、查询、交互的逻辑封成 Atom 类(type: "atom" 代表复杂数据),结构体如下所示:

ts 复制代码
// SpyAtom.Overview
interface Overview {
  id: string;
  type:
    | 'string'
    | 'number'
    | 'bigint'
    | 'boolean'
    | 'symbol'
    | 'undefined'
    | 'object'
    | 'function'
    | 'null'
    | 'error'
    | 'debug-origin'
    | 'atom';
  value: string | PropertyDescriptor;
  __atomId?: string;
  instanceId?: string;
}

正是这 20 行类型代码决定了本次主题中的具体实现:打印 window 对象就是依赖这套接口类型定义。

为什么要强调打印的是 window 对象?因为 window 对象具备上述所有的 "问题" 于一身:

  • 有不支持序列化的属性数据,比如 window.alert() 这种全局方法的属性;
  • 有很多 Getter 属性,如 outerWidth / innerWidth / location 等;
  • self-reference,如 window.self
  • 可以沿着原型链查看数据明细;

如何实现

我们在上面提到了:「用户如果打印复杂数据,单纯靠序列化源数据会面临......」,那打印不复杂数据还会有那么多问题吗?答案是不会,对于这些不复杂的数据我们只需要拿到值,并获取到它的类型即可完成渲染。

明确了数据复不复杂之后,我们从以上类型定义中把 type: "atom" 复杂数据类型的定义单独拎出来:

ts 复制代码
// SpyAtom.Overview
interface Overview {
  id: string;
  type: 'atom';
  value: string | PropertyDescriptor;
  __atomId: string;
  instanceId: string;
}
  • id:作为数据的唯一标识。
  • __atomId :打印复杂数据时,数据实体会被记录在 Atom 的 storeMap 中,__atomId 在 storeMap 中作为 key,对应一个对象源数据。
  • instanceId :本质的值是由 __atomId 衍生。默认和 __atomId 相等、指向当前打印出来的对象;点击对象展开,每个内层属性都有一个 instanceId 指向父级(上一层)的对象;

除了以上三个 id 之外,我们还用到了一个被称为 parentId 的数据:

  • parentId :本质的值也是由 __atomId 衍生。用于获取 Getter 属性值,具体的获取行为原理是 Object.getOwnPropertyDescriptor(store[parentId], key).get.call(store[instanceId])

总结

通过以上的内容介绍,我们再来回顾下打印 window 对象会遇到的问题,现在都有了相对应的答案:

如何处理不支持序列化的属性数据,比如函数?

答:第一步先通过获取类型将数据以「复杂」维度区分,结合数据值来描述一个数据该如何显示;而函数属于不复杂数据,在确定是函数类型后、通过 fn.toString() 获取值即可在调试端渲染。

Getter 属性动态计算属性值的交互特性会丢失?

答:Getter 属性值的获取本质上是调用函数,函数体内会使用 this 指针。那么基于以上复杂数据结构的定义,我们有 parentId / key / instanceId,基于 parentId 和 key 可以获取 getter 函数本身,再通过 instanceId 知道了实例对象,那么接下来组合调用一下就能完美解决:Object.getOwnPropertyDescriptor(store[parentId], key).get.call(store[instanceId])

如何处理 self-reference 对象无法序列化的问题,如 window.self

答:PageSpy 只会转化当前对象层级下的不复杂的数据属性。展开 window 时,PageSpy 会拿到 self 属性,但判断出它是一个复杂数据,SDK 不会直接进行计算、转换处理,而是告诉调试端 window.self 的描述符具备 getter 属性,等待调试端下次来查询时,再进值计算并返回值。

如何沿着原型链查看数据明细?

答:虽然通过 Object.getOwnPropertyDescriptors(someObj) 返回的 descriptors 描述符不会给我们原型对象的数据,但是我们可以手动添加一个 [[Prototype]] 加上去就可以满足用户沿着原型链点击查看数据明细啦!

以上就是本次深入 PageSpy 实现原理系列文章的全部内容了,欢迎大家在工作中集成使用,遇到问题可以在 Github 反馈或者加入技术支持群(中文 README 有群二维码),我们将第一时间解答。

相关推荐
Sean_summer3 分钟前
暑期第二周
前端·数据库·python
_未完待续9 分钟前
框架实战指南-组件参考
前端·vue.js
李文旺9 分钟前
图片加载优化-Nextjs与webpack源码
前端·react.js
不想当小卡拉米9 分钟前
高德地图上marker过多(超过3000个)渲染卡顿过慢问题解决
前端
dalancon12 分钟前
WMShell初始化
前端
半花13 分钟前
【Vue】通信组件
前端·vue.js
劫大大14 分钟前
前端开发公众号或服务号,本地怎么与后端测试服接口打通呢
前端·微信
芒果12515 分钟前
【转载】vue3 Ts axios 封装
前端
蓝倾15 分钟前
京东商品SKU数据采集方式及接口说明
前端·后端·api
前端 贾公子17 分钟前
vue如何在data里使用this
前端·javascript·vue.js