muwoo/blogs

基于 electron 实现前端页面远程调试工具

muwoo opened this issue · 1 comments

muwoo commented

前言

当业务代码发布到线上的时候,突然报了某一个机型白屏或者某个功能无法使用的时候,这种场景我们最需要的就是能知道究竟是代码哪里出错了。常见的手段是通过 vconsole 注入到我们的代码中,然后再找一下对应机型,进行功能调试。但是往往影响功能的兼容性不仅仅跟机型有关,有可能和系统版本,客户端版本等等综合因素。也就是说别人报错,你的相同机型不一定会报错,这种情况可能就又得找到当事人的手机来再注入vconsole来复现问题。

vconsole 终究是需要注入到代码里面的,如果想不注入代码里,可以通过 Charles 做请求劫持,给页面再插入 vconsole 的地址,让页面来加载,从而达到在用户手机远程查看的目的。但 vconsole 在有限的屏幕上去做展示,整体体验不是很好。其次 vconsole 也并没有达到远程调试的目的。接下来我们将尝试借助于 rubick 的能力来实现一个桌面端真正意义上的远程调试工具。

先直接上代码:

rubick 工具箱

远程调试 rubick 插件

动手实现

基于 Chrome devtools

要实现一个远程调试工具,其实 chrome 已经开源了一个可用于远程调试的工具,可以借助于 devtools-frontend 来实现远程调试功能,要说到 devtools-frontend 我们有必要先了解一下 chrome devtools 原理。

Chrome DevTools 是辅助开发者进行 Web 开发的重要调试工具,DevToolsChromium 的一部分,可整体集成于一些常见应用中,比如 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。
    这四部分的交互逻辑如下图所示:

image.png

简单来说:被调试页面引入 Backend 后,会跟 Frontend 通过 websocket 建立连接;在 backend 中,对于一些 JavaScript API 或者 DOM 操作等进行了监听和 mock,从而页面执行对应操作时,会发送消息到 Frontend。同时 Backend 也会监听来自于 Frontend 的消息,收到消息后进行对应处理。

所以要去实现基于 electron 的远程调试工具,我们需要一个 frontend 客户端来对 backend 发过来的消息进行展示。此时需要将 frontend 内置到 electron 当中,基于此,我已经实现了一个版本:

image.png

但是发现了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 工具
这次的远程调试工具也是基于该插件进行的修改。先来看一下我们实现的效果:

image.png

可以通过该插件实现对移动端的远程调试动作。接下来看看通过 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 或者 远程调试的时候 会在 rubickonPluginEnter 生命周期中传入 code 告诉启动方式。所以只需要监听该生命周期跳转到远程调试页面即可:

export default {
  setup() {
    const router = useRouter();
    window.utools.onPluginEnter(({code}) => {
      if (code === 'devtools') {
        router.push('/devtools');
      }
    })
  },
}

接下来用户需要输入为哪个页面开启远程调试功能,开启远程调试需要干3件事情:

1. 初始化 weinre

rubickpreload.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里面的?