打印 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 有群二维码),我们将第一时间解答。

相关推荐
逆天的蝈蝈6 分钟前
开源与商业的碰撞TPFLOW与Gadmin低代码的商业合作
低代码·开源
光影少年16 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_17 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891119 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾21 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu23 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym28 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫29 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫33 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat34 分钟前
前端性能优化2
前端