在专栏的前几篇文章中,我们从 "是什么、为什么、怎么用" 三个角度分别介绍了 PageSpy,大家对这款产品也有了初步的印象。
接下来我会再分享 PageSpy 的内部实现,对实现原理好奇的同学可以阅读这篇文章。
三个仓库
PageSpy 主要由三个仓库组成:
- 调试端 WebUI 代码在 HuolalaTech/page-spy-web 仓库维护;
- 服务端代码在 HuolalaTech/page-spy-api 仓库维护;
- 需要在客户端引入的 SDK 代码在 HuolalaTech/page-spy 仓库维护;
仓库职能
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」面板点击上下翻页查询数据;
- ......等等事件
我们没有将事件调试的能力做成插件的形式,是出于两方面的原因:
- 它强依赖于 Socket 的实例;
- 事件响应的行为都是由各个插件去主张,也就是各个插件去监听由调试端发送过来和自身相关的事件,然后执行响应的逻辑;
即便如此,「事件调试」所具备的能力并不亚于插件。
另外这里再起个话题,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>
一般在实例化时无需指定 api
和 clientOrigin
参数,这是因为默认情况下,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 的整个时间周期内,几乎面对的都是调试端。
交互图例
数据安全
在产品开源后,我们了解到很多开发者对于产品是否会收集数据的担忧。在此我们向大家郑重承诺: