ES Agent 是一款类似 Postman 的轻量级 REST API 测试工具,支持 GET/POST/PUT/DELETE 等 7 种 HTTP 方法、多标签管理、参数和请求头编辑、请求体构建、响应格式化和历史记录等功能。本文记录将其基于 Electron 壳方案适配到鸿蒙 PC 平台的完整流程。
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_ESAgent
一、技术架构分析
1.1 功能定位
ES Agent 的核心功能是通过 HTTP 协议与远程服务器通信,验证 API 接口的正确性。与 Postman 相比,它更加轻量,无需安装大型客户端,适合在鸿蒙 PC 平台上快速调试 API。
1.2 目标架构(鸿蒙 Electron)
- 技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
- 核心逻辑:主进程使用 Node.js 原生 http/https 模块发送真实 HTTP 请求,渲染进程负责 UI 交互,通过 IPC 通道通信
1.3 架构设计
| 层级 | 职责 | 技术实现 |
|---|---|---|
| 主进程 | 窗口管理 + HTTP 请求 | Electron BrowserWindow + Node.js http/https |
| IPC 通道 | 进程间通信 | ipcMain.handle / ipcRenderer.invoke |
| 渲染进程 | UI 交互 + 状态管理 | 原生 DOM + JavaScript |
| 样式层 | 深色主题 | Catppuccin Mocha 配色方案 |
1.4 鸿蒙平台适配要点
鸿蒙 Electron 适配层存在三大兼容约束,必须在开发中严格遵守:
- 禁止使用原生 prompt/confirm/alert 对话框,调用会导致 SubWindow 崩溃
- 禁止使用原生 select 元素,调用会触发 SubWindow 崩溃
- 禁止使用 setWindowOpenHandler 和 will-navigate API,调用会导致页面纯白
二、环境准备
2.1 开发环境要求
- 操作系统:Windows 10/11
- 开发工具:DevEco Studio(鸿蒙官方 IDE)
- HarmonyOS SDK:API 21+(5.0.5+)
- Node.js:v20+
2.2 项目结构
bash
ohos_hap/
├── electron-apps/
│ └── ESAgent/ # ES Agent 应用源码
│ ├── main.js # Electron 主进程(HTTP IPC)
│ ├── renderer.js # 渲染进程(核心逻辑)
│ ├── index.html # HTML 布局
│ ├── package.json # 项目配置
│ └── styles/
│ └── esagent.css # Catppuccin Mocha 深色主题
├── web_engine/ # 鸿蒙 web_engine 模块
│ └── src/main/resources/
│ └── resfile/resources/app/ # 部署目录
└── build-profile.json5 # 鸿蒙构建配置
三、核心适配流程
3.1 第一步:创建主进程与 HTTP IPC 通道
文件:electron-apps/ESAgent/main.js
ES Agent 的核心是发送 HTTP 请求。在鸿蒙平台上,渲染进程无法直接使用 Node.js 的网络模块,必须通过 IPC 通道委托主进程完成。
js
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const http = require('http');
const https = require('https');
const { URL } = require('url');
let mainWindow = null;
function createWindow() {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
mainWindow = new BrowserWindow({
width: Math.floor(width * 0.9),
height: Math.floor(height * 0.85),
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
app.quit();
});
HTTP 请求 IPC 通道注册:
js
ipcMain.handle('http:send', async (event, options) => {
const { method, url, headers, body, timeout } = options;
return new Promise((resolve) => {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch (e) {
resolve({ success: false, error: 'URL 格式错误: ' + e.message });
return;
}
const isHttps = parsedUrl.protocol === 'https:';
const lib = isHttps ? https : http;
const reqOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: method,
headers: headers || {},
timeout: timeout || 30000,
rejectUnauthorized: false
};
const startTime = Date.now();
try {
const req = lib.request(reqOptions, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const rawBody = Buffer.concat(chunks);
const elapsed = Date.now() - startTime;
resolve({
success: true,
statusCode: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
body: rawBody.toString('utf-8'),
size: rawBody.length,
time: elapsed
});
});
});
req.on('error', (err) => {
resolve({ success: false, error: err.message });
});
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: '请求超时(' + (timeout || 30000) + 'ms)' });
});
if (body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
req.write(body);
}
req.end();
} catch (err) {
resolve({ success: false, error: err.message });
}
});
});
关键要点:
- 使用 Node.js 原生 http/https 模块,而非浏览器的 fetch/XMLHttpRequest,避免 CORS 限制
- 自动检测 http 和 https 协议,选择对应的库
- rejectUnauthorized: false 允许自签名证书的 HTTPS 请求
- 30 秒超时保护,防止请求挂起
3.2 第二步:设计双面板布局
文件:electron-apps/ESAgent/index.html
ES Agent 采用经典的请求/响应双面板布局,中间用拖动条分隔。顶部是工具栏(方法按钮组 + URL 输入框 + 发送按钮),下方是标签栏和主内容区。
方法选择器采用按钮组而非下拉框,这是鸿蒙平台的关键适配决策:
js
<div class="toolbar">
<div class="toolbar-brand">ES Agent</div>
<div class="url-bar">
<!-- 方法按钮组 -->
<div class="method-group" id="methodGroup">
<button class="method-btn method-get active" data-method="GET">GET</button>
<button class="method-btn method-post" data-method="POST">POST</button>
<button class="method-btn method-put" data-method="PUT">PUT</button>
<button class="method-btn method-patch" data-method="PATCH">PATCH</button>
<button class="method-btn method-delete" data-method="DELETE">DELETE</button>
<button class="method-btn method-head" data-method="HEAD">HEAD</button>
<button class="method-btn method-options" data-method="OPTIONS">OPT</button>
</div>
<input type="text" class="url-input" id="urlInput" placeholder="输入请求 URL,例如 https://httpbin.org/get" />
<button class="btn-send" id="btnSend">发送</button>
</div>
<div class="toolbar-actions">
<button class="tool-btn" id="btnHistory" title="历史记录">📋</button>
</div>
</div>
请求面板包含三个子标签(参数、请求头、请求体),每个标签页使用 KV 对编辑器模式:
js
<div class="request-panel">
<div class="panel-tabs">
<div class="panel-tab active" data-panel="params">参数</div>
<div class="panel-tab" data-panel="headers">请求头</div>
<div class="panel-tab" data-panel="body">请求体</div>
</div>
<div class="panel-content">
<div class="tab-panel active" id="panelParams">
<div class="kv-header">
<span class="kv-check"></span>
<span class="kv-key">参数名</span>
<span class="kv-value">参数值</span>
<span class="kv-action"></span>
</div>
<div class="kv-list" id="paramsList"></div>
<button class="btn-add-row" id="btnAddParam">+ 添加参数</button>
</div>
<div class="tab-panel" id="panelHeaders">
<div class="kv-header">
<span class="kv-check"></span>
<span class="kv-key">Header 名</span>
<span class="kv-value">Header 值</span>
<span class="kv-action"></span>
</div>
<div class="kv-list" id="headersList"></div>
<button class="btn-add-row" id="btnAddHeader">+ 添加请求头</button>
</div>
<div class="tab-panel" id="panelBody">
<div class="body-type-bar">
<span class="body-type-label">Content-Type:</span>
<div class="body-type-dropdown" id="bodyTypeDropdown">
<div class="body-type-trigger" id="bodyTypeTrigger">
<span id="bodyTypeLabel">JSON</span>
<span class="dropdown-arrow">▼</span>
</div>
<div class="body-type-options" id="bodyTypeOptions" style="display:none">
<div class="body-type-option" data-type="json">JSON</div>
<div class="body-type-option" data-type="text">Text</div>
<div class="body-type-option" data-type="xml">XML</div>
<div class="body-type-option" data-type="form">Form Data</div>
<div class="body-type-option" data-type="none">None</div>
</div>
</div>
</div>
<textarea class="body-editor" id="bodyEditor" placeholder='{"key": "value"}'></textarea>
</div>
</div>
</div>
关键要点:
- HTTP 方法使用按钮组替代下拉框,避免鸿蒙平台的定位和事件兼容问题
- Body Type 仍使用自定义 div 下拉框(非原生 select),通过 stopPropagation 防止事件冒泡
- KV 编辑器使用 checkbox + key input + value input + delete button 行模式
3.3 第三步:实现核心交互逻辑
文件:electron-apps/ESAgent/renderer.js
渲染进程的核心逻辑包括:发送请求、KV 对编辑器、多标签管理、历史记录和面板拖动。
发送请求:收集参数、请求头、请求体,通过 IPC 调用主进程的 http:send 通道:
js
async function sendRequest() {
if (isSending) return;
syncTabState();
const tab = tabs[activeTabId];
const url = tab.url.trim();
if (!url) {
document.getElementById('urlInput').focus();
return;
}
isSending = true;
const sendBtn = document.getElementById('btnSend');
sendBtn.textContent = '请求中...';
sendBtn.disabled = true;
document.getElementById('statusInfo').textContent = '正在发送请求...';
// 构建请求头
const headers = {};
tab.headers.filter(h => h.enabled && h.key).forEach(h => {
headers[h.key] = h.value;
});
// 构建请求体
let body = null;
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(tab.method) && tab.bodyType !== 'none') {
body = tab.body;
if (tab.bodyType === 'json' && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
} else if (tab.bodyType === 'xml' && !headers['Content-Type']) {
headers['Content-Type'] = 'application/xml';
} else if (tab.bodyType === 'text' && !headers['Content-Type']) {
headers['Content-Type'] = 'text/plain';
}
}
// 构建带参数的 URL
let finalUrl = url;
const enabledParams = tab.params.filter(p => p.enabled && p.key);
if (enabledParams.length > 0) {
const separator = finalUrl.includes('?') ? '&' : '?';
const queryString = enabledParams.map(p =>
encodeURIComponent(p.key) + '=' + encodeURIComponent(p.value)
).join('&');
finalUrl += separator + queryString;
}
try {
const result = await ipcRenderer.invoke('http:send', {
method: tab.method,
url: finalUrl,
headers: headers,
body: body,
timeout: 30000
});
if (result.success) {
tab.response = result;
displayResponse(result);
addHistory(tab.method, finalUrl, result.statusCode, result.time);
document.getElementById('statusInfo').textContent =
`${result.statusCode} ${result.statusText} | ${result.time}ms | ${formatSize(result.size)}`;
} else {
displayError(result.error);
document.getElementById('statusInfo').textContent = '请求失败: ' + result.error;
}
} catch (err) {
displayError(err.message);
document.getElementById('statusInfo').textContent = '请求异常: ' + err.message;
}
isSending = false;
sendBtn.textContent = '发送';
sendBtn.disabled = false;
}
KV 对编辑器:参数和请求头使用统一的 KV 行编辑器,支持 checkbox 启用/禁用、自动添加空行、删除行:
js
function addParamRow(listId, key = '', value = '', enabled = true) {
const list = document.getElementById(listId);
const row = document.createElement('div');
row.className = 'kv-row';
row.innerHTML = `
<div class="kv-check"><input type="checkbox" ${enabled ? 'checked' : ''} /></div>
<div class="kv-key"><input type="text" placeholder="Key" value="${escapeHtml(key)}" /></div>
<div class="kv-value"><input type="text" placeholder="Value" value="${escapeHtml(value)}" /></div>
<div class="kv-action"><button class="btn-remove-row" title="删除">✕</button></div>
`;
row.querySelector('input[type="checkbox"]').addEventListener('change', syncTabState);
row.querySelectorAll('input[type="text"]').forEach(input => {
input.addEventListener('input', () => {
syncTabState();
const rows = list.querySelectorAll('.kv-row');
const lastRow = rows[rows.length - 1];
if (row === lastRow) {
const lastKey = lastRow.querySelector('.kv-key input').value;
const lastVal = lastRow.querySelector('.kv-value input').value;
if (lastKey || lastVal) {
addParamRow(listId);
}
}
});
});
row.querySelector('.btn-remove-row').addEventListener('click', () => {
row.remove();
syncTabState();
});
list.appendChild(row);
}
多标签管理:每个标签保存完整状态(method、url、params、headers、bodyType、body、response),切换时保存/恢复:
js
function createTab() {
tabCounter++;
const id = tabCounter;
tabs[id] = createEmptyTab();
activeTabId = id;
const tabEl = document.createElement('div');
tabEl.className = 'tab active';
tabEl.dataset.tabId = id;
tabEl.innerHTML = `
<span class="tab-method method-get">GET</span>
<span class="tab-name">请求 ${id}</span>
<span class="tab-close" data-tab-id="${id}">✕</span>
`;
document.querySelectorAll('#tabBar .tab').forEach(t => t.classList.remove('active'));
document.getElementById('btnNewTab').before(tabEl);
resetPanels();
document.getElementById('urlInput').focus();
}
function switchTab(id) {
if (id === activeTabId) return;
syncTabState();
activeTabId = id;
document.querySelectorAll('#tabBar .tab').forEach(t => {
t.classList.toggle('active', parseInt(t.dataset.tabId) === id);
});
restoreTabState(tabs[id]);
}
function closeTab(id) {
const tabIds = Object.keys(tabs).map(Number);
if (tabIds.length <= 1) return;
delete tabs[id];
const tabEl = document.querySelector(`.tab[data-tab-id="${id}"]`);
if (tabEl) tabEl.remove();
if (activeTabId === id) {
const remaining = Object.keys(tabs).map(Number);
activeTabId = remaining[remaining.length - 1];
document.querySelectorAll('#tabBar .tab').forEach(t => {
t.classList.toggle('active', parseInt(t.dataset.tabId) === activeTabId);
});
restoreTabState(tabs[activeTabId]);
}
}
面板拖动条:通过 mousedown/mousemove/mouseup 三事件实现请求/响应面板宽度调整:
js
function initPanelResizer() {
const resizer = document.getElementById('panelResizer');
const requestPanel = document.querySelector('.request-panel');
const responsePanel = document.querySelector('.response-panel');
let isDragging = false;
let startX = 0;
let startReqWidth = 0;
resizer.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startReqWidth = requestPanel.offsetWidth;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const mainEl = document.querySelector('.main');
const mainWidth = mainEl.offsetWidth;
const historyWidth = document.getElementById('historyPanel').style.display !== 'none'
? document.getElementById('historyPanel').offsetWidth : 0;
const available = mainWidth - historyWidth - 4;
const newReqWidth = startReqWidth + (e.clientX - startX);
const minW = 250;
const maxW = available - minW;
if (newReqWidth >= minW && newReqWidth <= maxW) {
requestPanel.style.width = newReqWidth + 'px';
requestPanel.style.flex = 'none';
responsePanel.style.flex = '1';
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
}
3.4 第四步:实现默认测试用例
ES Agent 内置 6 个默认测试用例,覆盖 GET/POST/PUT/DELETE 方法和参数/请求头/请求体场景,方便在鸿蒙真机上快速验证功能:
js
const defaultTests = [
{
method: 'GET',
url: 'https://httpbin.org/get',
params: [
{ key: 'name', value: 'ES Agent', enabled: true },
{ key: 'version', value: '1.0', enabled: true }
],
headers: [
{ key: 'Accept', value: 'application/json', enabled: true }
],
bodyType: 'none',
body: '',
label: 'GET 带参数'
},
{
method: 'POST',
url: 'https://httpbin.org/post',
params: [],
headers: [
{ key: 'Content-Type', value: 'application/json', enabled: true },
{ key: 'X-App', value: 'ES-Agent', enabled: true }
],
bodyType: 'json',
body: '{\n "name": "ES Agent",\n "message": "Hello from HarmonyOS",\n "timestamp": 1234567890\n}',
label: 'POST JSON'
},
{
method: 'PUT',
url: 'https://httpbin.org/put',
params: [],
headers: [
{ key: 'Content-Type', value: 'application/json', enabled: true }
],
bodyType: 'json',
body: '{\n "id": 1,\n "name": "Updated Name",\n "status": "active"\n}',
label: 'PUT 更新数据'
},
{
method: 'DELETE',
url: 'https://httpbin.org/delete',
params: [
{ key: 'id', value: '123', enabled: true }
],
headers: [],
bodyType: 'none',
body: '',
label: 'DELETE 删除'
},
{
method: 'POST',
url: 'https://httpbin.org/post',
params: [],
headers: [
{ key: 'Content-Type', value: 'application/x-www-form-urlencoded', enabled: true }
],
bodyType: 'form',
body: 'username=admin&password=123456&remember=true',
label: 'POST 表单'
},
{
method: 'GET',
url: 'https://httpbin.org/headers',
params: [],
headers: [
{ key: 'X-Custom-Header', value: 'HarmonyOS-Test', enabled: true },
{ key: 'X-Request-ID', value: 'test-001', enabled: true }
],
bodyType: 'none',
body: '',
label: '自定义请求头'
}
];
点击测试模板时,restoreTemplateDetails 函数会恢复完整的参数、请求头和请求体:
js
function restoreTemplateDetails(item) {
document.getElementById('paramsList').innerHTML = '';
if (item.params && item.params.length > 0) {
item.params.forEach(p => addParamRow('paramsList', p.key, p.value, p.enabled));
}
addParamRow('paramsList');
document.getElementById('headersList').innerHTML = '';
if (item.headers && item.headers.length > 0) {
item.headers.forEach(h => addParamRow('headersList', h.key, h.value, h.enabled));
}
addParamRow('headersList');
bodyType = item.bodyType || 'json';
document.getElementById('bodyTypeLabel').textContent = getBodyTypeLabel(bodyType);
document.getElementById('bodyTypeOptions').style.display = 'none';
document.getElementById('bodyEditor').value = item.body || '';
}
3.5 第五步:Catppuccin Mocha 深色主题
文件:electron-apps/ESAgent/styles/esagent.css
ES Agent 采用 Catppuccin Mocha 配色方案,7 种 HTTP 方法各有独立颜色标识:
js
:root {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-toolbar: #11111b;
--bg-input: #313244;
--bg-hover: #45475a;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--text-muted: #6c7086;
--border-color: #313244;
--accent: #89b4fa;
--success: #a6e3a1;
--warning: #f9e2af;
--error: #f38ba8;
--radius: 6px;
--method-get: #a6e3a1;
--method-post: #fab387;
--method-put: #89b4fa;
--method-patch: #f9e2af;
--method-delete: #f38ba8;
--method-head: #cba6f7;
--method-options: #94e2d5;
}
方法按钮组的激活状态配色:
js
.method-btn {
padding: 0 8px;
height: 100%;
border: none;
background: transparent;
cursor: pointer;
font-weight: 700;
font-size: 11px;
color: var(--text-muted);
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.method-btn:hover {
background: var(--bg-hover);
}
.method-btn.active {
background: var(--bg-hover);
}
.method-btn.method-get.active { color: var(--method-get); }
.method-btn.method-post.active { color: var(--method-post); }
.method-btn.method-put.active { color: var(--method-put); }
.method-btn.method-patch.active { color: var(--method-patch); }
.method-btn.method-delete.active { color: var(--method-delete); }
.method-btn.method-head.active { color: var(--method-head); }
.method-btn.method-options.active { color: var(--method-options); }
响应状态码徽章配色:
js
.status-ok {
background: rgba(166, 227, 161, 0.15);
color: var(--success);
}
.status-redirect {
background: rgba(249, 226, 175, 0.15);
color: var(--warning);
}
.status-client-error, .status-error {
background: rgba(243, 139, 168, 0.15);
color: var(--error);
}
.status-server-error {
background: rgba(243, 139, 168, 0.25);
color: var(--error);
}
四、部署到鸿蒙平台
4.1 文件同步
将 Electron 应用文件复制到鸿蒙 web_engine 模块的部署目录:
js
$src = "electron-apps\ESAgent"
$dest = "web_engine\src\main\resources\resfile\resources\app"
Copy-Item "$src\main.js" "$dest\main.js" -Force
Copy-Item "$src\renderer.js" "$dest\renderer.js" -Force
Copy-Item "$src\index.html" "$dest\index.html" -Force
Copy-Item "$src\package.json" "$dest\package.json" -Force
Copy-Item "$src\styles\esagent.css" "$dest\styles\esagent.css" -Force
4.2 构建 HAP 包
在 DevEco Studio 中:
- 打开项目根目录
- 点击 Build > Build Hap(s)/APP(s)
- 选择 Build Hap(s)
- 等待构建完成
4.3 真机测试
- 连接鸿蒙设备(HUAWEI MateBook Pro)
- 点击 Run > Run 'electron'
- 安装完成后,应用自动启动
- 打开历史记录面板,点击默认测试用例验证功能



五、常见问题 FAQ
Q1:为什么 HTTP 方法使用按钮组而不是下拉框?
问题现象:最初使用方法下拉框(自定义 div 实现),在鸿蒙真机上点击 GET 无法切换到 POST
根本原因:鸿蒙 Electron 适配层对 flex 容器内的 position: absolute 下拉框定位和事件处理存在兼容性问题
解决方案:将方法选择器从下拉框改为按钮组
js
<!-- 方法按钮组(替代下拉框) -->
<div class="method-group" id="methodGroup">
<button class="method-btn method-get active" data-method="GET">GET</button>
<button class="method-btn method-post" data-method="POST">POST</button>
<button class="method-btn method-put" data-method="PUT">PUT</button>
<button class="method-btn method-patch" data-method="PATCH">PATCH</button>
<button class="method-btn method-delete" data-method="DELETE">DELETE</button>
<button class="method-btn method-head" data-method="HEAD">HEAD</button>
<button class="method-btn method-options" data-method="OPTIONS">OPT</button>
</div>
优势:
- 无需 position: absolute 定位
- 无需 display: none/block 切换
- 无 z-index 层级问题
- 纯按钮点击,鸿蒙平台兼容性最好
Q2:Body Type 下拉框为什么没有改成按钮组?
问题现象:方法下拉框改了,Body Type 下拉框是否也需要改?
分析:Body Type 下拉框位于请求体面板内部(非 flex 工具栏内),其父元素 body-type-dropdown 设置了 position: relative,下拉列表 position: absolute 定位正常
结论:Body Type 下拉框使用 stopPropagation 阻止事件冒泡后工作正常,无需改为按钮组
js
document.getElementById('bodyTypeTrigger').addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown('bodyTypeOptions');
});
document.getElementById('bodyTypeOptions').addEventListener('click', (e) => {
e.stopPropagation();
});
document.querySelectorAll('.body-type-option').forEach(opt => {
opt.addEventListener('click', (e) => {
e.stopPropagation();
bodyType = opt.dataset.type;
document.getElementById('bodyTypeLabel').textContent = opt.textContent;
document.getElementById('bodyTypeOptions').style.display = 'none';
syncTabState();
});
});
Q3:为什么请求要通过主进程 IPC 而不是直接用 fetch?
问题现象:渲染进程直接使用 fetch 发送请求是否可行?
根本原因:鸿蒙 Electron 壳应用中,渲染进程的 fetch/XMLHttpRequest 受 CORS 策略限制,无法直接访问外部 API
解决方案:通过 IPC 通道委托主进程使用 Node.js 原生 http/https 模块发送请求
js
// 渲染进程通过 IPC 调用
const result = await ipcRenderer.invoke('http:send', {
method: tab.method,
url: finalUrl,
headers: headers,
body: body,
timeout: 30000
});
优势:
- 主进程不受 CORS 限制
- 支持 http 和 https 协议自动切换
- 支持自签名证书(rejectUnauthorized: false)
- 30 秒超时保护
Q4:如何测试 ES Agent 的功能是否正常?
操作步骤:
- 启动应用后,点击历史记录按钮(📋)
- 历史记录列表顶部显示 6 个默认测试模板
- 点击 "GET 带参数" 模板,URL 和参数自动填充
- 点击发送按钮,等待响应
- 响应面板显示 200 绿色徽章和 JSON 格式化数据
测试端点说明:
- httpbin.org 是一个公开的 HTTP 测试服务
- 它接收请求并原样返回请求内容
- 用于验证 ES Agent 的请求发送功能是否正确
Q5:键盘快捷键有哪些?
ES Agent 支持以下快捷键:
- Ctrl + Enter:发送请求
- Ctrl + T:新建标签
- Ctrl + W:关闭当前标签
js
function onKeyDown(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
sendRequest();
} else if (e.ctrlKey && e.key === 't') {
e.preventDefault();
createTab();
} else if (e.ctrlKey && e.key === 'w') {
e.preventDefault();
closeTab(activeTabId);
}
}
Q6:如何同步文件到鸿蒙项目?
问题现象:修改了 electron-apps/ESAgent/ 下的文件,但构建后没有生效
根本原因:文件没有同步到鸿蒙 web_engine 模块的部署目录
解决方案:使用 PowerShell 脚本同步文件
js
$src = "electron-apps\ESAgent"
$dest = "web_engine\src\main\resources\resfile\resources\app"
Copy-Item "$src\main.js" "$dest\main.js" -Force
Copy-Item "$src\renderer.js" "$dest\renderer.js" -Force
Copy-Item "$src\index.html" "$dest\index.html" -Force
Copy-Item "$src\package.json" "$dest\package.json" -Force
Copy-Item "$src\styles\esagent.css" "$dest\styles\esagent.css" -Force
部署路径:web_engine/src/main/resources/resfile/resources/app/
注意:每次修改代码后都需要同步,否则构建的 HAP 包不会包含最新代码。