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

相关推荐
爱吃的强哥几秒前
vue3 使用 vite 管理多个项目,实现各子项目独立运行,独立打包
前端·javascript·vue.js
谈不譚网安9 分钟前
CSRF请求伪造
前端·网络安全·csrf
TT模板15 分钟前
苹果cmsV10主题 MXonePro二开优化修复开源版
前端·html5
拖孩16 分钟前
【Nova UI】十一、组件库中 Icon 组件的测试、使用与全局注册全攻略
前端·javascript·vue.js·ui·sass
去伪存真21 分钟前
不用动脑,手把手跟着我做,就能掌握Gitlab+Jenkins提交代码自动构部署
前端·jenkins
天天扭码1 小时前
深入解析 JavaScript 中的每一类函数:从语法到对比,全面掌握适用场景
前端·javascript·面试
小希爸爸1 小时前
4、中医基础入门和养生
前端·后端
自由鬼1 小时前
开源AI开发工具:OpenAI Codex CLI
人工智能·ai·开源·软件构建·开源软件·个人开发
kooboo china.1 小时前
Tailwind CSS 实战:基于 Kooboo 构建企业官网页面(一)
前端·css·编辑器
uhakadotcom1 小时前
Fluid:云原生数据加速与管理的简单入门与实战
前端