ES Agent 的核心功能是通过 HTTP 协议与远程服务器通信,验证 API 接口的正确性。与 Postman 相比,它更加轻量,无需安装大型客户端,适合在鸿蒙 PC 平台上快速调试 API。
技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
核心逻辑:主进程使用 Node.js 原生 http/https 模块发送真实 HTTP 请求,渲染进程负责 UI 交互,通过 IPC 通道通信
| 层级 | 职责 | 技术实现 | | --- | --- | --- | | 主进程 | 窗口管理 + HTTP 请求 | Electron BrowserWindow + Node.js http/https | | IPC 通道 | 进程间通信 | ipcMain.handle / ipcRenderer.invoke | | 渲染进程 | UI 交互 + 状态管理 | 原生 DOM + JavaScript | | 样式层 | 深色主题 | Catppuccin Mocha 配色方案 |
鸿蒙 Electron 适配层存在三大兼容约束,必须在开发中严格遵守:
禁止使用原生 prompt/confirm/alert 对话框,调用会导致 SubWindow 崩溃
禁止使用原生 select 元素,调用会触发 SubWindow 崩溃
禁止使用 setWindowOpenHandler 和 will-navigate API,调用会导致页面纯白
操作系统:Windows 10/11
开发工具:DevEco Studio(鸿蒙官方 IDE)
HarmonyOS SDK:API 21+(5.0.5+)
Node.js:v20+
ohos_hap/ ├── electron-apps/ │ └── ESAgent/ │ ├── main.js │ ├── renderer.js │ ├── index.html │ ├── package.json │ └── styles/ │ └── esagent.css ├── web_engine/ │ └── src/main/resources/ │ └── resfile/resources/app/ └── build-profile.json5
文件 :electron-apps/ESAgent/main.js
ES Agent 的核心是发送 HTTP 请求。在鸿蒙平台上,渲染进程无法直接使用 Node.js 的网络模块,必须通过 IPC 通道委托主进程完成。
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
(
)
;
}
)
;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 请求
秒超时保护,防止请求挂起
文件 :electron-apps/ESAgent/index.html
ES Agent 采用经典的请求/响应双面板布局,中间用拖动条分隔。顶部是工具栏(方法按钮组 + URL 输入框 + 发送按钮),下方是标签栏和主内容区。
方法选择器采用按钮组而非下拉框,这是鸿蒙平台的关键适配决策:
< 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 对编辑器模式:
<
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 行模式
文件 :electron-apps/ESAgent/renderer.js
渲染进程的核心逻辑包括:发送请求、KV 对编辑器、多标签管理、历史记录和面板拖动。
发送请求 :收集参数、请求头、请求体,通过 IPC 调用主进程的 http:send 通道:
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'
;
}
}
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 启用/禁用、自动添加空行、删除行:
function
addParamRow
(
listId
,
key
=
''
,
value
=
''
,
enabled
=
true
)
{
const
list
=
document
.
getElementById
(
listId
)
;
const
row
=
document
.
createElement
(
'div'
)
;
row
.
className
=
'kv-row'
;
row
.
innerHTML
=
`✕`
;
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),切换时保存/恢复:
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
=
`GET请求
${
id
}✕`
;
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
{
t
.
classList
.
toggle
(
'active'
,
parseInt
(
t
.
dataset
.
tabId
)
===
activeTabId
)
;
}
)
;
restoreTabState
(
tabs
[
activeTabId
]
)
;
}
}面板拖动条 :通过 mousedown/mousemove/mouseup 三事件实现请求/响应面板宽度调整:
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
{
if
(
isDragging
)
{
isDragging
=
false
;
document
.
body
.
style
.
cursor
=
''
;
document
.
body
.
style
.
userSelect
=
''
;
}
}
)
;
}ES Agent 内置 6 个默认测试用例,覆盖 GET/POST/PUT/DELETE 方法和参数/请求头/请求体场景,方便在鸿蒙真机上快速验证功能:
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 函数会恢复完整的参数、请求头和请求体:
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
||
''
;
}文件 :electron-apps/ESAgent/styles/esagent.css
ES Agent 采用 Catppuccin Mocha 配色方案,7 种 HTTP 方法各有独立颜色标识:
:
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
;
}.
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
)
;
}.
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
)
;
}将 Electron 应用文件复制到鸿蒙 web_engine 模块的部署目录:
$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
打开项目根目录
点击 Build > Build Hap(s)/APP(s)
选择 Build Hap(s)
等待构建完成
连接鸿蒙设备(HUAWEI MateBook Pro)
点击 Run > Run ‘electron’
安装完成后,应用自动启动
打开历史记录面板,点击默认测试用例验证功能



Q1:为什么 HTTP 方法使用按钮组而不是下拉框?
问题现象:最初使用方法下拉框(自定义 div 实现),在鸿蒙真机上点击 GET 无法切换到 POST
根本原因:鸿蒙 Electron 适配层对 flex 容器内的 position: absolute 下拉框定位和事件处理存在兼容性问题
解决方案:将方法选择器从下拉框改为按钮组
< ! -- 方法按钮组(替代下拉框) -- >< 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 阻止事件冒泡后工作正常,无需改为按钮组
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 模块发送请求
const
result
=
await
ipcRenderer
.
invoke
(
'http:send'
,
{
method
:
tab
.
method
,
url
:
finalUrl
,
headers
:
headers
,
body
:
body
,
timeout
:
30000
}
)
;主进程不受 CORS 限制
支持 http 和 https 协议自动切换
支持自签名证书(rejectUnauthorized: false)
秒超时保护
Q4:如何测试 ES Agent 的功能是否正常?
启动应用后,点击历史记录按钮(📋)
历史记录列表顶部显示 6 个默认测试模板
点击 “GET 带参数” 模板,URL 和参数自动填充
点击发送按钮,等待响应
响应面板显示 200 绿色徽章和 JSON 格式化数据
httpbin.org 是一个公开的 HTTP 测试服务
它接收请求并原样返回请求内容
用于验证 ES Agent 的请求发送功能是否正确
Ctrl + Enter:发送请求
Ctrl + T:新建标签
Ctrl + W:关闭当前标签
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
)
;
}
}问题现象:修改了 electron-apps/ESAgent/ 下的文件,但构建后没有生效
根本原因:文件没有同步到鸿蒙 web_engine 模块的部署目录
解决方案:使用 PowerShell 脚本同步文件
$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 包不会包含最新代码。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:CSDN HarmonyOS
原创地址:https://blog.csdn.net/weixin_62765017/article/details/165006540
原文作者:wei_shuo;如有侵权,请联系管理员删除。