基于 electron 实现前端页面远程调试工具
muwoo opened this issue · 1 comments
前言
当业务代码发布到线上的时候,突然报了某一个机型白屏或者某个功能无法使用的时候,这种场景我们最需要的就是能知道究竟是代码哪里出错了。常见的手段是通过 vconsole
注入到我们的代码中,然后再找一下对应机型,进行功能调试。但是往往影响功能的兼容性不仅仅跟机型有关,有可能和系统版本,客户端版本等等综合因素。也就是说别人报错,你的相同机型不一定会报错,这种情况可能就又得找到当事人的手机来再注入vconsole
来复现问题。
但 vconsole
终究是需要注入到代码里面的,如果想不注入代码里,可以通过 Charles 做请求劫持,给页面再插入 vconsole
的地址,让页面来加载,从而达到在用户手机远程查看的目的。但 vconsole
在有限的屏幕上去做展示,整体体验不是很好。其次 vconsole
也并没有达到远程调试的目的。接下来我们将尝试借助于 rubick
的能力来实现一个桌面端真正意义上的远程调试工具。
先直接上代码:
动手实现
基于 Chrome devtools
要实现一个远程调试工具,其实 chrome
已经开源了一个可用于远程调试的工具,可以借助于 devtools-frontend 来实现远程调试功能,要说到 devtools-frontend
我们有必要先了解一下 chrome devtools
原理。
Chrome DevTools
是辅助开发者进行 Web 开发的重要调试工具,DevTools
是 Chromium
的一部分,可整体集成于一些常见应用中,比如 electron
、微信开发者工具
。
DevTools
主要由四部分组成:
- Frontend:调试器前端,默认由 Chromium 内核层集成,DevTools Frontend 是一个 Web 应用程序;
- Backend:调试器后端,Chromium、V8 或 Node.js;
- Protocol:调试协议,调试器前端和后端使用此协议通信。 它分为代表被检查实体的语义方面的域。 每个域定义类型、命令(从前端发送到后端的消息)和事件(从后端发送到前端的消息)。该协议基于 json rpc 2.0 运行;
- Message Channels:消息通道,消息通道是在后端和前端之间发送协议消息的一种方式。包括:Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。
这四部分的交互逻辑如下图所示:
简单来说:被调试页面引入 Backend 后,会跟 Frontend 通过 websocket
建立连接;在 backend 中,对于一些 JavaScript API 或者 DOM 操作等进行了监听和 mock,从而页面执行对应操作时,会发送消息到 Frontend
。同时 Backend 也会监听来自于 Frontend 的消息,收到消息后进行对应处理。
所以要去实现基于 electron
的远程调试工具,我们需要一个 frontend
客户端来对 backend
发过来的消息进行展示。此时需要将 frontend
内置到 electron
当中,基于此,我已经实现了一个版本:
但是发现了2个问题:
- electron 集成
devtools-frontEnd
导致项目体积增加 - devtools 远程调试响应速度特别慢
所以我们暂时放弃了此方案,寻找一些替代方案。直到发现了 weinre
基于 weinre
weinre 就相对简单多了,只需要在 weinre
启动服务的时候,为需要调试的页面注入 target-script-min.js
即可和 weinre
建立连接。但是难点在于怎么为页面自动注入 target-script-min.js
。其实我们可以参考 Charles
那样去实现一个代理服务器,当检测到页面是我们的目标页面时,动态注入 target-script-min.js
即可。要实现一个代理服务器可以参考之前写的一篇文章 基于 electron 实现简单易用的抓包、mock 工具
这次的远程调试工具也是基于该插件进行的修改。先来看一下我们实现的效果:
可以通过该插件实现对移动端的远程调试动作。接下来看看通过 rubick
如何实现这样一款插件。
Rubick
是基于electron
的工具箱,媲美 utools的开源插件,已实现 utools 大部分的 API 能力,所以可以做到无缝适配 utools 开源的插件。 之所以做这个工具箱一方面是 utools 本身并未开源,但是公司内部的工具库又无法发布到 utools 插件中,所以为了既要享受 utools 生态又要有定制化需求,我们自己参考 utools 设计,做了 Rubick
rubick 使用和 utools 使用几乎一毛一样。首先先创建 plugin.json
作为入口文件
{
"pluginName": "网络抓包",
"description": "网络抓包、mock、多环境联调",
"main": "index.html",
"version": "0.0.1",
"logo": "logo.png",
"preload": "preload.js",
"author": "muwoo",
"name": "rubick-network",
"features": [
{
"explain": "网络抓包",
"cmds":["network", "抓包"]
},
{
"explain": "远程调试",
"code": "devtools",
"cmds":["devtools", "远程调试"]
}
]
}
network
功能之前我已经实现了,这里不再讲解如何实现抓包功能,主要介绍一下 devtools
。这里 plugin.json
声明了 2 个 feature,当我们再功能栏搜索 devtools
或者 远程调试的时候
会在 rubick
的 onPluginEnter
生命周期中传入 code
告诉启动方式。所以只需要监听该生命周期跳转到远程调试页面即可:
export default {
setup() {
const router = useRouter();
window.utools.onPluginEnter(({code}) => {
if (code === 'devtools') {
router.push('/devtools');
}
})
},
}
接下来用户需要输入为哪个页面开启远程调试功能,开启远程调试需要干3件事情:
1. 初始化 weinre
在 rubick
的 preload.js
中可以使用 nodejs
,注入 weinre
// preload.js
const weinre = require('weinre2');
const optionDefaults = {
httpPort: '9333',
boundHost: network.getIPAddress(),
verbose: false,
debug: false,
readTimeout: 5
};
weinre.run(optionDefaults);
2. 启动抓包服务
addUrlListener({commit, state}, payload) {
if (!payload) return;
// 服务未启动需要启动服务
if (!state.serverInfo.ipAddress) {
// 启动 anyproxy 服务
effects.actions.startServer({commit, state});
}
commit('setDevtoolsUrl', payload);
}
3. 监听返回地址,如果是需要远程调试的页面,注入 weinre
beforeSendResponse: (requestDetail, responseDetail) => {
// ...
// 如果返回的 url 和需要远程调试的url一致,则注入 target-script-min.js
if (state.devtoolsUrl && requestDetail.url === state.devtoolsUrl) {
const result = parseUri(state.devtoolsUrl);
newResponse.body = `<script src="https://wenire.${result.host}/target/target-script-min.js#anonymous"></script>${newResponse.body}`;
}
return { response: newResponse };
}
这里需要注意的是我们注入的 js 是 "https://wenire.${result.host}/target/target-script-min.js#anonymous"
这样的方式,注意到 src 并不是本地的 wenire
启动的服务地址,为什么要这个搞呢?主要因为微信环境如果在 https
网站上发起了一个和当前域名主域名不一致的 http
请求,可能会被拦截,本地 server 启动的大多是 http://192.168.xx.x
这样的地址,经常会导致注入的js加载失败。
但是由于 https://wenire.${result.host}
这个域名并没有真实存在,所以需要修改请求体,让其指向正确的资源地址:
beforeSendRequest: (requestDetail) => {
// wenire
const result = parseUri(state.devtoolsUrl);
// 如果是自定义域名,需要重新指向正确的地址
if (requestDetail.url.indexOf(`https://wenire.${result.host}`) >= 0) {
const newRequestOptions = requestDetail.requestOptions;
requestDetail.protocol = 'http';
newRequestOptions.hostname = network.getIPAddress();
newRequestOptions.port = DEVTOOLS_PORT;
}
return requestDetail;
}
故事到这里就差不多结束了,我们已经开发好了一个基于 rubick
的远程调试插件,快去拿给小伙伴 show
一下吧!
结语
什么是 rubick
?
基于 electron 的工具箱,媲美 utools的开源插件,已实现 utools 大部分的 API 能力,所以可以做到无缝适配 utools 开源的插件。 之所以做这个工具箱一方面是 utools 本身并未开源,但是公司内部的工具库又无法发布到 utools 插件中,所以为了既要享受 utools 生态又要有定制化需求,我们自己参考 utools 设计,做了 Rubick.
欢迎大家给rubick
pr 和提 issue 帮助我们完善
Rubick: https://github.com/clouDr-f2e/rubick
本章节插件代码已上传github: https://github.com/clouDr-f2e/rubick-network
请问基于Chrome devtools,你们是如何将frontend内置到electron里面的?