CDP篇: 用 Chrome devtools 的 network 标签页调试 node 请求

1 前言

大家好,我是心锁。

早在我在学校的时候,常做的一件事是用各种手段爬学校的网站。从最开始用 python ,到后边在小程序环境,再到最后用 node。

期间,学习了前端,在最后慢慢发展到一个JS 技术栈工程师。

我逐渐发现一个问题,熟悉了浏览器甚至是小程序的网络调试体验后,在我用 node 写爬虫或者调用第三方接口的过程中,对应的调试体验真是一落千丈。

尤其是,根据库的不同,node 在进行爬虫行为时的各种行为并不完全一致,这点和浏览器的默认行为会有更大的差异。

实际上,在刚开始经历 node 爬虫之苦的时候我就在社区中不断寻找解决方案,最终并不是很理想。

于是上周,我在又一次爬虫之旅结束,在社区中寻找解决方案时,竟然还是没找到心中的方案时,决定自己写一个。

2 社区调研

2.1 调研对象

社区调研阶段,我主要分官方方案和社区方案去看。

其中目前官方提供的调试方案主要就是node v8 inspector,目前最主流且实用的方式应该是 node 自带的调试系统,即 v8 inspector ,常见的启动方式如下。

node --inspect ./src/index.js
node --inspect-brk ./src/index.js

这种情况下会在 9229 端口创建 websocket 服务,此时打开 http://localhost:9229/json 可以看到相关的服务。

此时可以复制 devtoolsFrontendUrl 来打开窗口,或者从 F12 的 node 图标跳转过去,但是会发现打开的页面包含的 tab 并不包含我们需要的 network 标签页。

因为经过阉割,v8 inspector 并没有实现期望中的 network 调试,但是其为我们提供了一个方向,因为 inspector 同样是基于 CDP 协议实现,我们完全可以自己想办法对接。

官方方案主要就是这个,https://github.com/GoogleChromeLabs/ndb 同样是早期的官方调试库,其虽然也是基于 Chrome devtools 的调试体验,但是没有 network 相关的内容。

剩下的就是社区第三方库,我尝试看了不少项目,把其中认为比较具有参考价值的列举了出来:

  • node-inspector。在 node 早期版本,有一个非常优秀的开源库https://github.com/node-inspector/node-inspector,这个库支持 network 标签,但很遗憾,这只适用于 node 8 以及以下版本的情况——它上次更新是在7年前,现在是废弃状态。
  • debugging-aid。https://github.com/naugtur/debugging-aid是我看完一圈代码最简洁的一个,它通过 mitm 库实现了监听 HTTP 请求,能做到自动打印请求的相关信息以及堆栈所在。但是存在的主要问题是终究是终端打印,并且 mitm 已经三年没有更新了,调试体验并没有增加到什么程度。
  • network-activity-viewer。https://github.com/saivishnutammineni/network-activity-viewer这个库,是对我启发最大的一个社区库。作者通过编辑官方库的形式,做到了无代理也能得到响应信息。美中不足的是,思路上是自己尝试去完成了 UI 部分的实现。

再多的信息的话,是一些存在时间比较久的 issues:

其中 Stack Overflow 中记载,想做 node 请求监控,还可以尝试使用代理的形式来监控()。当然,在代理这块我没有再深入探究,代理的缺陷比较明显,有一定门槛且会影响现有的代理。

除却 node 网络这块的调研,我还试着了解了一下关于更多第三方 devtool 的实现。其中就包括 react-native devtool ,我们知道 react-native 的请求本质上是安卓或者苹果设备发出的请求,但是却能在 devtool 上调试,很明显,这就是借助 CDP 的力量,至于 CDP 的细节内容我们后边再说。

总之, react-native devtool 可以让安卓/ IOS 网络拥有 devtool 调试能力,那么我们的思路就清晰了。

2.2 调研总结

经过一阵子的调研,基本可以得出结论,即官方在引入 network 调试这一块不够积极,2016 年的帖子到现在仍在讨论,也期待官方能在这一块努努力。

而社区库这块,说实话比较惊讶,过去了好几年,没想到在这一块仍有缺失,没有能达到我心中语气的解决方案。

我尝试理解了目前的整块逻辑,那么我们其实要做的内容明了路:

  1. 想办法监听 node 发起的各种请求,从 http/https 包发起的请求,到原生 fetch 发起的请求,再到 websocket 相关的内容。
  2. 将监听到的内容根据 CDP 协议,将信息推送到 devtool 上。

在第一期,预期先实现最基本的调试能力,看看效果再优化。

3 开发篇

那么,现在进入正文。本篇我们将主要探讨技术方案的可行性,并且实现第一期的 demo,同时阅读本篇,我们都可以对 CDP 协议有更深的理解。

3.1 拦截/监听请求

那么首先,我们先思考一下怎么去拦截/监听 node 请求。我整理了几个思路,包括了网络代理、暴露包装器函数、node:async_hooks 监听、修改原始类。

在这几个思路中,首先被我排除的是网络代理,原因在于通过网络代理的形式监听程序,不仅要对 HTTPS 服务做本地证书,成本高。而且需要额外占用资源,同时对于已存在本地代理的应用友好程度并不够高。关于这块,一个场景是调试国外的 支付 API。

其次是包装器函数的方案,我们知道网络请求库这块,不管是 axios、got 还是 node-fetch,它们都提供了不同程度的钩子来对全局请求做管理,但是这个方案也不能考虑,因为我们无暇去给这一个个库适配他们的请求,这不现实。

那么剩下两个思路,这两个思路预计在我们之后的开发中混合使用。

  • node:async_hooks 具有极高的可行性,但是由于两个因素,本期没有考虑进来,一个是其是实验性功能,其次是在我完成了代码半周之后才看到了这么个东西。
  • 修改原始类这一块有一个现成的代码供我们参考https://github.com/saivishnutammineni/network-activity-viewer,虽说作者上次更新是一年前了,但是实验了一下其中的思路是没有问题的。唯一的缺陷是,想对原生模块做 hack 的话,esm 标准下我们无法编辑导出的库,目前的 hack 操作只能先覆盖 commonjs。

💡 commonjs 标准下,我们通过 require 函数导入包之后,可以修改其内部属性。

而我们知道,在 nodejs 18 之前,可以说九成九的网络请求都是通过 http/https 这两个原生包发起的,而各个三方库本质上都是对这两个库的封装。所以我们在实现上,只要确保我们对 http/https 两个模块的动工在三方库引入之前,就能拦截请求。

总之,现在我们先通过修改原始类的方式完成部分代码的编写。这要求我们对原生发起请求有一定了解,这一块的参考代码如下。

    const options = {
   
      hostname: 'jsonplaceholder.typicode.com',
      port: 80,
      path: '/todos/1',
      method: 'GET',
    };

    const req = http.request(options, (res) => {
   
      let data = '';

      res.on('data', (chunk) => {
   
        data += chunk;
      });

      res.on('end', () => {
   
        resolve(JSON.parse(data));
      });
    });
    
    req.end();

也就是理论上我们只要对 http/https 模块导出的 request 方法做一层包装,就可以得到请求的参数、返回。

request 函数的参数我们可以从 Ts 类型这里看到,是一个重载函数,相应的,我们要实现对应的重载。

那么,整个函数大致的样子如图,我们设置一个 request 工厂,其接受原本的 request 函数,返回一个新的 request 函数。

工厂内部,要基于参数对具体的 callback 函数也做包装,并作为参数传递给真正的 request 函数。同时,我们还需要对返回的 ClientRequest 实例做一层 write 代理,这一步在拦截 payload 的写入操作。

完成了这一块的基础代码,我们现在拥有了拦截请求的能力。我们现在可以尝试在各个代理函数中做我们的逻辑,这个逻辑比较清晰了。

也就是说,我们能拿到所有的内容了,现在要做的就是梳理好数据存储和更新的逻辑,并且和 CDP 对接上。

关于数据存储的逻辑我就不提了,这是用到的类:


export class RequestDetail {
   
  id: string;
  type?: "Fetch" | "XHR" | "Script" | "Document" | "Other";
  constructor(needStack = true) {
   
    this.id = Math.random().toString(36).slice(2);
    this.type = "Fetch"
  }
  documentURL?: string;

  url?: string;
  method?: string;
  cookies: any;

  requestHeaders: any;
  requestData: any;

  responseData: any;
  responseStatusCode?: number;
  responseHeaders: any;

  requestStartTime?:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值