PageSpy 是一款兼容 Web、小程序、React Native、鸿蒙 App 等平台项目的开源调试平台。通过封装原生 API,它能将调用原生方法时的参数进行过滤和转化,并整理成特定格式的消息,供调试端消费。调试端接收到这些消息后,会以类似本地控制台的功能界面呈现出来。
当遇到无法在本地使用控制台调试的情况时,PageSpy 就显得尤为强大和便捷。该开源神器支持Web、小程序和HarmonyOS等各种平台。相当于你可以远程查看设备的日志,即便设备选在千里之外,也可以抓取它的日志和查看报警报错信息,岂不美哉?
接下来,我将通过几个典型场景来具体了解 PageSpy 的优势。
PageSpy For HarmonyOS介绍
@huolala/page-spy-harmony 是由货拉拉开源项目 PageSpy 在鸿蒙端实现的 SDK。 PageSpy 用于本地开发调试、远程调试用户设备,支持调试 WEB / 小程序 / 鸿蒙等平台,更多细节内容介绍请参考 主仓库。
page-spy-harmony 三方库地址 :
https://ohpm.openharmony.cn/#/cn/detail/@huolala%2Fpage-spy-harmony
常见调试场景
-
本地调试 H5 与 Webview 应用:传统 H5 信息查看面板在移动端存在屏幕尺寸限制、操作不便、显示效果差、信息被截断等问题。PageSpy 则解决了这些问题,提供了更为直观和高效的方式。
-
远程办公与跨地域协作:传统沟通方式如邮件、电话或视频会议,不仅效率低下,且信息传递不完整,容易引发误解和误判。PageSpy 可以帮助开发者通过一个网页界面实时查看调试信息,提高了远程调试的效率。
-
用户终端出现白屏问题:面对用户终端异常问题,通常依赖数据监控和日志分析定位问题,耗时且需要深入了解业务和技术实现。PageSpy 的介入可以简化这一流程,帮助快速定位问题。
搭建后台服务环境
在引入 鸿蒙客户端SDK 前,需先部署 PageSpy 服务,因为该方案是需要在网页上查看调试信息的,需要部署个后台服务。PageSpy 后台服务提供多种部署方案,可以根据自身情况选择任意一种进行部署:
方式一: 使用 Node 部署:通过 yarn 或 npm 安装。
bash
yarn global add @huolala-tech/page-spy-api@latest # 如果你使用 npm
npm install -g @huolala-tech/page-spy-api@latest
安装完成后,可以在命令行中执行 page-spy-api
启动服务。启动后,打开浏览器访问 http://localhost:6752
体验,测试完成后部署到服务器上。
方式二:使用 Docker 部署:
bash
docker run -d --restart=always -v ./log:/app/log -v ./data:/app/data -p 6752:6752 --name="pageSpy" ghcr.io/huolalatech/page-spy-web:latest
启动完成后,打开浏览器访问 http://localhost:6752
体验,测试完成后部署到服务器上。推荐使用docker方式在服务端部署,简单快捷,一行命令搞定。

这里我选择了使用 docker进行部署。安装部署完成后服务自动启动,端口号是6752.浏览器打开 http://yourremoteip:6752
就可以体验了。

服务部署完成后,打开对应网页,点击web页面左上角的开始调试按钮。如果有客户端接入进来了,则能看到以下页面:
鸿蒙应用集成使用方法
鸿蒙应用的集成方法很简单,只需几行代码,无侵入的完成集成。
-
安装依赖 :由于鸿蒙应用商店最低要求使用 API 12,建议使用 API 17。我们的应用最低也是在 API 17,可以使用基于 API 16 版本开发的
@huolala/page-spy-harmony@2.1.x
版本。bashohpm install @huolala/page-spy-harmony
这将安装
^2.1.0
版本。 -
连接服务:首先创建一个 axios 的单例对象并导出,项目中所有网络请求均使用该对象。
javascriptimport axios from '@ohos/axios'; const axiosInstance = axios.create({ baseURL: 'https://api.github.com', timeout: 1000, }); export {axiosInstance};
然后在 EntryAbility 的
onWindowStageCreate
生命周期方法中创建 PageSpy 对象。javascriptimport { PageSpy } from '@huolala/page-spy-harmony'; onWindowStageCreate(windowStage: window.WindowStage): void { new PageSpy({ context: this.context, api: "192.168.1.25:6752", enableSSL: false, axios: axiosInstance }) windowStage.loadContent('pages/Index', (err) => { if (err.code) { hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); return; } hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); }); }
注意事项:
1.如果是在自己的电脑上进行测试(服务端装在你自己的本地电脑上),需要手机和电脑连接同一个 Wi-Fi,api 就写电脑的 ip 地址 + 6752 端口号。本地部署没有 https 证书,所以 enableSSL 必须设置为 false。axios 为我们前面创建的 axios 单例对象。
2.如果服务是部署在云服务器上,则这里的ip需要填写你部署的PageSpy 的服务地址和端口。如果不是https 连接,则必须把enableSSL 设置为false.
3.当前PageSpy的HarmonyOS的sdk只支持axios客户端。如果嫌axios客户端的使用太繁琐,推荐坚果派的nutpi/axios三方库,该库极大简化了axios网络库的使用,且也是支持PageSpy。
nutpi/axios三方库地址: https://ohpm.openharmony.cn/#/cn/detail/@nutpi%2Faxios
测试连接:将应用运行到手机上,在 log 日志中看到下面两条日志:
bash
[PageSpy] [LOG] Device ID: f764
[PageSpy] [LOG] Connect successful.
然后点击网页右上角的开始调试,选择在线实时即可开始调试。此时在新页面上可以看到我们的设备。
使用 PageSpy 调试
当环境搭建好后,可以通过点击调试按钮进入调试页面。可以看到左侧有输出、网络、存储、系统四个选项。
- 输出:展示 console 打印的日志,不能展示 hilog 输出的日志。
- 网络请求:PageSpy 将网络请求的详细信息保存下来,对远程协同办公和内部测试特别有用。
- AppStorage 存储:PageSpy 可以实时查看 AppStorage 中保存的数据,无需手动刷新网页。
- 系统:展示系统概览信息,如 Platform 和 User Agent。
生产环境部署
确认测试无误后,可以将 PageSpy 部署到生产环境中。PageSpy 服务端会自动读取当前运行目录下的 config.json
配置文件,支持配置运行端口、多实例部署、跨域配置、日志数据配置等。
版本兼容性
@huolala/page-spy-harmony@1.x
:基于 API 9 版本开发。@huolala/page-spy-harmony@2.0.x
:基于 API 11 版本开发。@huolala/page-spy-harmony@2.1.x
:基于 API 16 版本开发。
能力概览
- console 打印日志
- 项目报错信息输出
- 网络请求信息输出
- 应用数据查看
开始正式使用
在引入 SDK 之前,请先部署 PageSpy 服务,部署方式参见上文或者可参考官方文档的部署教程。
部署成功后,在项目中引入 HAR SDK:
bash
ohpm install @huolala/page-spy-harmony
然后在项目中引入 SDK 并实例化:
javascript
import { PageSpy } from '@huolala/page-spy-harmony';
import axiosInstance from 'path/to/your/axios';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
const $pageSpy = new PageSpy({
context: this.context,
api: '<your-pagespy-server-host>',
enableSSL: true,
axios: axiosInstance,
});
}
}
PageSpy 默认会在 DevEcho Studio 的 Log 面板中输出调试的连接信息;也可以使用 $pageSpy.showPanel();
方法手动弹窗查看。
如果使用的是坚果派的nutpi/axios的三方库,则使用方式如下:
javascript
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import PreferencesUtils from '../utils/PreferencesUtils';
import { PageSpy } from '@huolala/page-spy-harmony';
import { axiosClient } from '../utils/axiosClient';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
new PageSpy({
context: this.context,
api: "120.27.146.2xx:6752", //你的PageSpy后台服务地址
enableSSL: false,
axios: axiosClient.instance,
})
windowStage.loadContent('pages/StartPage', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
}
axiosClient从哪来?axiosClient是使用nutpi/axios三方库封装的一个网络请求工具类。
源码如下:
javascript
//axiosClient.ets
import {
AxiosError,
AxiosHeaders,
AxiosHttpRequest,
AxiosRequestHeaders,
FileUploadConfig,
HttpPromise,
UploadFile
} from '@nutpi/axios';
import { Log } from './logutil';
import { promptAction, router } from '@kit.ArkUI';
import { AppAuthManager } from './AppAuthManager';
function showToast(msg: string) {
Log.debug(msg)
promptAction.showToast({ message: msg })
}
function showLoadingDialog(msg: string) {
Log.debug(msg)
// promptAction.showToast({ message: msg })
}
function hideLoadingDialog() {
}
// 移除硬编码的token,改为动态获取
// AppStorage.SetOrCreate('Authorization', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwibmlja25hbWUiOm51bGwsImF2YXRhciI6bnVsbCwiaWF0IjoxNzU0NjY2ODk3LCJleHAiOjE3NTQ3NTMyOTd9.32Kp1pCt-HIqKglOpLrRhOMdSjyonutEDkifQRaoZCo");
// 图片地址的前缀
export const IMAGE_BASE_URL = 'http://xxx.felin.xxxx.com'
/**
* axios请求客户端创建
*/
const axiosClient = new AxiosHttpRequest({
baseURL: "http://xxx.maotuai.com/api",
timeout: 10 * 1000,
checkResultCode: false,
showLoading: true,
headers: new AxiosHeaders({
'Content-Type': 'application/json'
}) as AxiosRequestHeaders,
interceptorHooks: {
requestInterceptor: async (config) => {
// 在发送请求之前做一些处理,例如打印请求信息
Log.debug('网络请求Request 请求方法:', `${config.method}`);
Log.debug('网络请求Request 请求链接:', `${config.url}`);
Log.debug('网络请求Request Params:', `\n${JSON.stringify(config.params)}`);
Log.debug('网络请求Request Data:', `${JSON.stringify(config.data)}`);
// 动态获取token
const token = AppAuthManager.getToken();
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
axiosClient.config.showLoading = config.showLoading
if (config.showLoading) {
showLoadingDialog("加载中...")
}
if (config.checkLoginState) {
// 检查登录状态
if (!AppAuthManager.getToken()) {
if (config.needJumpToLogin) {
// 可以在这里跳转到登录页面
// router.replaceUrl({ url: 'pages/LoginPage' });
}
throw new AxiosError("请登录")
}
}
return config;
},
requestInterceptorCatch: (err) => {
Log.error("网络请求RequestError", err.toString())
if (axiosClient.config.showLoading) {
hideLoadingDialog()
}
return err;
},
responseInterceptor: (response) => {
//优先执行自己的请求响应拦截器,在执行通用请求request的
if (axiosClient.config.showLoading) {
hideLoadingDialog()
}
if (response.status === 200) {
if (response.data.code != 0) {
errorHandle(response)
// 处理业务错误
return Promise.reject(response)
}
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
responseInterceptorCatch: (error) => {
if (axiosClient.config.showLoading) {
hideLoadingDialog()
}
Log.error("网络请求响应异常", error.toString());
errorHandler(error);
return Promise.reject(error);
},
}
});
// 处理业务错误
function errorHandle(response) {
switch (response.data.code) {
case 401:
AppAuthManager.clearToken();
router.replaceUrl({ url: 'pages/LoginPage' });
break;
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
showToast("登录过期,请重新登录")
AppAuthManager.clearToken();
router.replaceUrl({ url: 'pages/LoginPage' });
break;
// 404请求不存在
case 404:
showToast("网络请求不存在")
break;
default:
showToast(response.data.message)
break
}
}
function errorHandler(error: any) {
if (error instanceof AxiosError) {
//showToast(error.message)
} else if (error != undefined && error.response != undefined && error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
AppAuthManager.clearToken();
router.replaceUrl({ url: 'pages/LoginPage' });
break;
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
showToast("登录过期,请重新登录")
AppAuthManager.clearToken();
router.replaceUrl({ url: 'pages/LoginPage' });
break;
// 404请求不存在
case 404:
showToast("网络请求不存在")
break;
// 其他错误,直接抛出错误提示
default:
if (error.response.data && error.response.data.message) {
showToast(error.response.data.message)
}
}
}
}
// 导出AppAuthManager供其他模块使用
export { AppAuthManager as AppAuthManager, axiosClient, HttpPromise, UploadFile, FileUploadConfig };
有了上述封装后,写网络接口变得超级简单了,如以下使用方式,清晰明了:
javascript
/**
* 照片管理相关api实现
*/
import { axiosClient, HttpPromise } from "../../../utils/axiosClient";
import { GetPhotoDetailResp } from "../bean/photo/getPhotoDetailResp";
import { GetPhotoListReq } from "../bean/photo/getPhotoListReq";
import { GetPhotoListResp } from "../bean/photo/getPhotoListResp";
import { PhotoAddReq } from "../bean/photo/photoAddReq";
import { PhotoAddResp } from "../bean/photo/photoAddResp";
// 创建照片
export const photoAdd = (req:PhotoAddReq): HttpPromise<PhotoAddResp> => axiosClient.post({url:'/app/photo/add',data: req});
// 获取照片列表
export const getPhotoList = (req:GetPhotoListReq): HttpPromise<GetPhotoListResp> => axiosClient.get({url:'/app/photo/',params: req});
// 获取照片详情
export const getPhotoDetail = (photoId:string): HttpPromise<GetPhotoDetailResp> => axiosClient.get({url:'/app/photo/'+photoId});
PageSpy 后台界面调试
进入调试后,可以清晰看到后台的网络接口访问,请求的数据和响应的数据清晰明了。
在左侧的输出选项页面中,则可以看到应用console.log打印的日志:

在左侧的存储选项中,则可以查看应用的AppStorage中存放的数据。

总结
通过以上介绍,可以看出 PageSpy 是一款强大的调试工具,适用于多种开发场景,特别是对于移动端开发调试提供了极大的便利性。推荐大家尝试并体验下这一网络调试神器。