PageSpy 实现原理

在专栏的前几篇文章中,我们从 "是什么、为什么、怎么用" 三个角度分别介绍了 PageSpy,大家对这款产品也有了初步的印象。

接下来我会再分享 PageSpy 的内部实现,对实现原理好奇的同学可以阅读这篇文章。

三个仓库

PageSpy 主要由三个仓库组成:

仓库职能

SDK 做了什么

下面这些行为在实际开发中非常常见:

js 复制代码
// 输出日志
console.log(res.data.users)

// 发起请求
fetch("https://example.com/todos")

// 更新缓存
localStorage.setItem("foo", "foo")

在客户端调用上面这些原生方法后,大家可以看到 PageSpy 会以控制台级别的 UI 风格展示输出。首先可以明确的是,PageSpy 调试端展示的数据来源于 SDK 告诉它的,但 SDK 是如何做到的呢?

console.log(res.data.users) 为例,SDK 大致如下处理:

typescript 复制代码
// SDK 通过插件的形式组织能力
export default class ConsolePlugin implements PageSpyPlugin {
  
  public async onCreated() {
    const type: SpyConsole.ProxyType[] = ['log', 'info', 'error', 'warn'];
    type.forEach((item) => {
      this.console[item] = window.console[item];
      // 对原生方法进行包装(重写)
      window.console[item] = (...args: any[]) => {
        this.printLog({
          logType: item,
          logs: args,
          url: window.location.href,
        });
      };
    });
  }
​
  private printLog(data: SpyConsole.DataItem) {
    if (data.logs && data.logs.length) {
      // 调用原生方法,以保证行为预期
      this.console[data.logType](...data.logs);
      // 对参数进行整理,随后发送到调试端
      socketStore.broadcastMessage(log);
    }
  }
}

可以看到我们只是对原生方法进行了简单的包装,整个过程中没有银弹。上面提到 SDK 通过插件的形式组织能力,除了 ConsolePlugin 之外,这里列出当前包含的所有插件:

  • plugins/console:包装 console.<log | info | warn | error>,处理日志信息的输出;

    为什么只代理这四个?因为最常用。

  • plugins/network:包装 fetch | XMLHttpRequest | navigator.sendBeacon 的网络请求;

  • plugins/system:检查 ES5 之后的标准 API 在客户端环境的兼容性;

    SDK 本身的兼容性配置参考:["chrome > 75", "safari > 12", "> 0.1%", "not dead", "not op_mini all"]

  • plugins/database:处理 window.indexedDB 的数据;

  • plugins/storage:包装 localStorage | sessionStorage | cookie 获取缓存数据;

  • plugins/error:处理 onerror | 资源加载错误 | unhandledrejection 报错事件;

  • plugins/page:通过 document.documentElement.outerHTML 获取页面结构数据;

除了以上内容之外,SDK 还有「事件调试」的能力。所谓「事件调试」,即对调试端发送过来的信息做出响应,例如:

  • 调试端 "上线" 了,SDK 会将缓存的信息全部推给调试端;
  • 调试端在「Console」面板输入代码并执行,比如打印 window 信息;
  • 调试端在「Page」面板点击 "刷新" 功能按钮;
  • 调试端在「Storage - indexedDB」面板点击上下翻页查询数据;
  • ......等等事件

我们没有将事件调试的能力做成插件的形式,是出于两方面的原因:

  1. 它强依赖于 Socket 的实例;
  2. 事件响应的行为都是由各个插件去主张,也就是各个插件去监听由调试端发送过来和自身相关的事件,然后执行响应的逻辑;

即便如此,「事件调试」所具备的能力并不亚于插件。

另外这里再起个话题,PageSpy 的 「Console」面板支持打印 window,但是大家都知道 window 是自引用对象,它的 window.self / window.globalThis / window.window 指向自身,简单的 JSON.stringify(window) 肯定行不通,对于这种情况 PageSpy 是如何考虑、处理的呢?敬请关注下篇文章:《打印 window 对象,PageSpy 是如何处理的?》

SDK 对上下文的感知

在我们成功部署 PageSpy 后,第一步都是在客户端引入 SDK 文件,假设你将 PageSpy 部署在 https://example.com,那么会有:

html 复制代码
<script crossorigin="anonymous" src="https://example.com/page-spy/index.min.js"></script>

随后就是对 PageSpy 实例化:

html 复制代码
<script>
  window.$pageSpy = new PageSpy();
</script>

一般在实例化时无需指定 apiclientOrigin 参数,这是因为默认情况下,PageSpy 会通过引入 SDK 的路径,在这里是 https://example.com/page-spy/index.min.js,去分析并决定 Server 的地址和调试端的地址:

  • Server 地址用于向服务端发起请求,例如请求创建房间、建立 WebSocket 连接等;
  • 调试端地址,当客户端引入 PageSpy 后左下角会出现图标控件,点击后会出现带有连接信息的弹窗,上面的 "Copy" 按钮会直接生成调试端的链接并复制;

那么内部的实现逻辑如下:

typescript 复制代码
// 获取引入地址,在这里是 https://example.com/page-spy/index.min.js
const scriptLink = (document.currentScript as HTMLScriptElement)?.src;

const resolveConfig = () => {
  if (!scriptLink) {
    return null;
  }
  const { host, origin } = new URL(scriptLink);
  return {
    api: host, // 在这里是 example.com
    clientOrigin: origin, // 在这里是 https://example.com
    project: 'default',
    autoRender: true,
    title: '',
  };
};

服务端做了什么

Server 在整个产品体系中,主要提供了以下接口能力:

  • /api/v1/room/create:供客户端的 SDK 创建房间;
  • /api/v1/room/list:供调试端查询所有的房间(调试连接);
  • /api/v1/ws/room/join:供 SDK、调试端两端建立 WebSocket 连接;

除了对外暴露的接口外,Server 还会:

  • 当判断调试的房间无效时,会在一段时间后自动销毁房间,比如 SDK 断开连接;
  • 在两端的 ws 消息交互过程中,根据消息的类型,决定消息是单播或者广播;

调试端做了什么

调试端的职责就很明显了:充分利用 SDK 提供过来的信息,提供一套交互友好的调试界面,因为各位开发者们在使用 PageSpy 的整个时间周期内,几乎面对的都是调试端。

交互图例

数据安全

在产品开源后,我们了解到很多开发者对于产品是否会收集数据的担忧。在此我们向大家郑重承诺:

PageSpy 不会主动收集任何用户数据。

相关推荐
光影少年4 小时前
usemeno和usecallback区别及使用场景
react.js
Rense15 小时前
开源RK3588 AI Module7,并与Jetson Nano生态兼容的低功耗AI模块
人工智能·开源
吕彬-前端8 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白8 小时前
react hooks--useCallback
前端·react.js·前端框架
程序员皮皮林8 小时前
开源PDF工具 Apache PDFBox 认识及使用(知识点+案例)
java·pdf·开源·apache
恩婧9 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
程序员小杨v110 小时前
如何使用 React Compiler – 完整指南
前端·react.js
谢尔登11 小时前
Babel
前端·react.js·node.js
卸任11 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
MinIO官方账号13 小时前
从 HDFS 迁移到 MinIO 企业对象存储
人工智能·分布式·postgresql·架构·开源