货拉拉开源的前端远程调试工具 PageSpy 在 Github 已经获得了 3000+ star,取得现在的成果当然离不开大家的支持。为了更好地满足前端在各种场景下的调试需求,小程序调试能力正在紧锣密鼓地内测中,大家可以期待一下。
有不少用户对实现原理很感兴趣,为解答大家的疑惑,同时也为了更进一步降低 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 通过插件的形式组织能力
abstract class PageSpyPlugin {
public constructor(public name: string) {}
// 加载后立即生效
public abstract onCreated?(): void;
// 用户主动触发的回调
public abstract onLoaded?(): void;
}
// Console 插件
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[]) => {
// ...
};
});
}
}
可以看到我们只是对原生方法进行了简单的包装,整个过程中没有银弹。上面提到 SDK 通过插件的形式组织能力,除了 ConsolePlugin
之外,这里列出当前包含的所有插件:
-
plugins/console:包装
console.<log | info | warn | error | debug>
,处理日志信息的输出;为什么只代理这几个?因为最常用。
-
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 的实例;
- 事件响应的行为都是由各个插件去主张,也就是各个插件去监听由调试端发送过来和自身相关的事件,然后执行响应的逻辑;
即便如此,"事件调试" 所具备的能力并不亚于插件。
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 的整个时间周期内,几乎面对的都是调试端。
交互图例
数据安全
在产品开源的整个生命周期中,我们都非常重视用户隐私、数据安全。一些开发者对于产品是否会收集数据表示担忧。在此我们向大家郑重承诺:
- 完全透明:PageSpy 的源代码都已经在 GitHub 上开源,我们致力于保持对用户的透明度;
- 私有化部署:在部署完成后,客户端代理功能所收集的数据将只会经过你自己维护的服务器上,不会被传输到任何非必要的地方。我们将采取一切必要的措施,确保您的数据安全存储;
我们明白数据安全对您的重要性,因此以实际行动来保护您的隐私。以上措施将确保您在使用我们的产品时能够获得数据安全与隐私保护。
如有任何疑虑或问题,请随时联系我们。最后,看到这里的读者,不妨到 github.com/HuolalaTech... 给仓库增加一个 ⭐️ 吧!