鸿蒙平台 ES Agent REST API 测试工具适配实战:基于 Electron 壳方案的跨平台 HTTP 调试客户端开发

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 中:

  1. 打开项目根目录
  2. 点击 Build > Build Hap(s)/APP(s)
  3. 选择 Build Hap(s)
  4. 等待构建完成

4.3 真机测试

  1. 连接鸿蒙设备(HUAWEI MateBook Pro)
  2. 点击 Run > Run 'electron'
  3. 安装完成后,应用自动启动
  4. 打开历史记录面板,点击默认测试用例验证功能

五、常见问题 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 的功能是否正常?

操作步骤:

  1. 启动应用后,点击历史记录按钮(📋)
  2. 历史记录列表顶部显示 6 个默认测试模板
  3. 点击 "GET 带参数" 模板,URL 和参数自动填充
  4. 点击发送按钮,等待响应
  5. 响应面板显示 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 包不会包含最新代码。

相关推荐
wei_shuo5 小时前
鸿蒙平台 H2 SQL 工作台适配实战:基于 Electron 壳方案的 Navicat 风格数据库管理工具开发
openharmony
wei_shuo1 天前
Flutter 三方库 OpenHarmony 鸿蒙适配实战:in_app_update 应用内更新插件 ArkTS 原生适配全流程
openharmony·in_app_update
Java的搬运工3 天前
HarmonyOS ArkUI V2 实战:Schema 驱动表单、2in1 适配与实时通信
harmonyos·arkts·openharmony·harmonyos next·表单校验·arkui v2·2in1
熊猫钓鱼>_>4 天前
Flutter app_settings 鸿蒙适配实战:Intent 体系到 Want 的跨越
flutter·华为·harmonyos·openharmony·intent·want
哈__5 天前
Flutter 3.44.9 + OpenHarmony7:image_cropper三方库 图片裁剪的应用
flutter·openharmony
moyh-blog24 天前
OpenHarmony播放音乐start请求从media_service到audio_server的调用流程02
openharmony·audio
Industio_触觉智能1 个月前
在瑞芯微RK平台下的开源鸿蒙OpenHarmony分区镜像/散包固件烧录
嵌入式硬件·openharmony·烧录·开源鸿蒙·瑞芯微·鸿蒙开发板·鸿蒙嵌入式
nullregedit1 个月前
OpenHarmony 小鸿 AI 开发实战 10:CI1302 语音上行的 UART 帧、Opus 与 VAD
openharmony·语音交互·开源鸿蒙·ws63·ci1302
nullregedit2 个月前
OpenHarmony 小鸿 AI 开发实战 04:WS63 BurnTool 烧录、Loader、导出与回滚
openharmony·嵌入式开发·开源鸿蒙·ws63·burntool