云计算百科
云计算领域专业知识百科平台

前端页面调用应用侧函数:鸿蒙 ArkWeb 双向通信学习笔记

在这里插入图片描述

本文是一篇从"踩坑"到"想通"的实战记录,围绕 HarmonyOS 中 Web 组件与 ArkTS 应用侧的双向调用,记录原理、对比、代码与心得。

为什么前端页面需要"反过来"调应用侧

做混合开发久了,会形成一种惯性思维:应用侧是"宿主",前端页面是"被加载的内容",方向永远是从宿主去驱动内容。但在真实业务里,这个方向经常需要反过来。

最典型的几个场景:

  • 前端 H5 页面里点一个"保存到本地"按钮,需要触发 ArkTS 把数据写入应用沙箱或偏好数据库;
  • H5 想拿到设备唯一标识、系统版本、网络状态这些只有原生才有的信息;
  • 前端发起一个需要鉴权的请求,要把 token 的获取和刷新交给应用侧统一管理;
  • 页面内嵌的富文本编辑器要调起原生的图片选择器、相机或文件系统。

这些需求的共同点是:数据或能力的"源头"在应用侧,而"触发时机"在前端。如果每次都绕一圈走 HTTP 接口或者 postMessage 那套老办法,既慢又脆。鸿蒙的 ArkWeb 给了一条更直接的路——把 ArkTS 对象"注入"到前端,让前端像调用一个普通 JS 对象那样调用应用侧函数。

这个机制叫 JavaScriptProxy。

T

R

A

E

R

E

F

]

(

h

t

t

p

s

:

/

/

b

l

o

g

.

s

e

g

m

e

n

t

f

a

u

l

t

.

c

o

m

/

a

/

1190000046913687

)

[

TRAE_REF](https://blog.segmentfault.com/a/1190000046913687)[

TRAEREF](https://blog.segmentfault.com/a/1190000046913687)[TRAE_REF

两种注册方式:不是二选一,而是时机不同

ArkWeb 提供了两个接口把 ArkTS 对象注册到前端页面,初学时很容易把它们当成"两种等价写法",其实它们的核心差异在于调用时机。

javaScriptProxy():Web 组件初始化时注入

javaScriptProxy() 是 Web 组件的链式方法,跟着 Web(…) 一起声明,在组件初始化阶段就把对象注入到前端页面。这意味着页面一加载,注册的对象就已经存在,前端可以无感调用,不会出现"页面跑得太快、对象还没注册"的时序问题。

适合那些从第一行 JS 就要用到的对象,比如全局配置、设备信息、基础工具方法。

// xxx.ets
import { webview } from '@kit.ArkWeb';

class DeviceInfoClass {
constructor() {}

getOSVersion(): string {
return 'HarmonyOS 5.0';
}

getDeviceId(): string {
return 'device-uuid-xxxx';
}

saveToLocal(key: string, value: string): void {
console.log(`保存到本地:${key} = ${value}`);
}
}

@Entry
@Component
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
@State deviceInfo: DeviceInfoClass = new DeviceInfoClass();

build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.webviewController })
.javaScriptProxy({
object: this.deviceInfo,
name: 'deviceInfo',
methodList: ['getOSVersion', 'getDeviceId', 'saveToLocal'],
controller: this.webviewController,
asyncMethodList: [],
permission: ''
})
}
}
}

前端这边就像调一个全局对象:

<!– index.html –>
<!DOCTYPE html>
<html>
<body>
<button onclick="showInfo()">获取设备信息</button>
<p id="info"></p>
<script>
function showInfo() {
const os = deviceInfo.getOSVersion();
const id = deviceInfo.getDeviceId();
document.getElementById('info').innerText = `系统:${os},设备ID:${id}`;
}
</script>
</body>
</html>

javaScriptProxy() 的参数里有几个容易忽略的点:methodList 声明哪些方法会被暴露,没在列表里的方法前端调不到;asyncMethodList 单独声明异步方法,和 methodList 是分开的;permission 控制哪些 URL 能访问,留空表示不做限制(开发期方便,上线前一定要补上)。

registerJavaScriptProxy():初始化完成后动态注册

registerJavaScriptProxy() 是 WebviewController 的方法,不跟在 Web 组件声明里,而是在组件初始化完成之后、由业务代码主动调用。它的价值在于"动态"——可以根据运行时条件决定注册什么对象,或者在页面加载到某个阶段后再注入。

这里有一个官方文档反复强调、但我第一次用就踩到的坑:注册之后必须调用 refresh() 才能生效。$TRAE_REF

// xxx.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

class UserServiceClass {
constructor() {}

getToken(): string {
return 'token-from-arkts';
}

refreshToken(): string {
return 'token-refreshed';
}
}

@Entry
@Component
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
@State userService: UserServiceClass = new UserServiceClass();

build() {
Column() {
Button('动态注册 UserService')
.onClick(() => {
try {
this.webviewController.registerJavaScriptProxy(
this.userService,
'userService',
['getToken', 'refreshToken']
);
// 关键:注册后必须 refresh 才生效
this.webviewController.refresh();
} catch (error) {
const e = error as BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})

Web({ src: $rawfile('index.html'), controller: this.webviewController })
}
}
}

我第一次写的时候漏掉了 refresh(),前端调用一直报 userService is not defined,排查了将近半小时才意识到注册和生效是两步。这个设计其实是合理的——动态注册往往伴随着页面状态变更,refresh() 让开发者显式控制生效时机,避免注册到一半就被前端调到。但文档里如果不特别强调,确实容易漏。

两种方式的对比

维度javaScriptProxy()registerJavaScriptProxy()
调用主体 Web 组件(链式声明) WebviewController
调用时机 组件初始化阶段 初始化完成后任意时机
是否需要 refresh 不需要 必须调用 refresh()
适合场景 全局基础对象、首次加载就要用的能力 动态注入、条件注册、分阶段加载
时序风险 低,对象在页面加载前就绪 高,需自行保证注册早于调用

选型上我的体会是:能用 javaScriptProxy() 就用它,时序最稳;只有当你确实需要"运行时才知道该注册什么"的灵活性时,才上 registerJavaScriptProxy(),并且把 refresh() 当成肌肉记忆。

反向也不难:应用侧调用前端函数

理解了前端调应用侧,反向其实更简单。应用侧通过 WebviewController 的 runJavaScript() 方法,可以直接执行前端页面里的 JS 代码。$TRAE_REF

// 前端定义一个函数
// function updateContent(data) { document.getElementById('content').innerText = data; }

// 应用侧触发它
this.webviewController.runJavaScript('updateContent("来自ArkTS的问候")');

runJavaScript() 的参数是一段 JS 代码字符串,所以可以传函数调用、甚至一段小脚本。鸿蒙还提供了 runJavaScriptExt(),在参数类型和返回处理上更强,适合需要拿到执行结果或传结构化数据的场景。

这种反向调用在"应用侧拿到数据后通知前端刷新"的场景特别好用,比如:原生网络请求完成后,调前端函数更新 UI;或者收到推送后,调前端函数刷新消息列表。

复杂类型:不是只能传字符串

初看文档会以为 JavaScriptProxy 只能传基础类型,实际它对数组和对象的支持很完整。

传数组

class NoteServiceClass {
getRecentNotes(): Array<string> {
return ['产品周会纪要', '鸿蒙适配笔记', '读书笔记'];
}
}

前端拿到的就是一个正常的 JS 数组,可以直接 forEach、map。

传对象

class NoteItem {
title: string = '';
createTime: string = '';
tags: Array<string> = [];
}

class NoteServiceClass {
getCurrentNote(): NoteItem {
const note: NoteItem = {
title: '前端页面调用应用侧函数',
createTime: '2026-08-10',
tags: ['鸿蒙', 'ArkWeb', 'JavaScriptProxy']
};
return note;
}
}

前端拿到的就是一个普通 JS 对象,字段名和 ArkTS 里的保持一致。这里有个心得:ArkTS 侧的对象字段命名要和前端约定好,因为注入后字段名是直接透传的,ArkTS 用驼峰前端就用驼峰,别一边驼峰一边下划线,调试时会绕。

异步调用:Promise 没那么神秘

JavaScriptProxy 对 Promise 的支持,是我在实际项目里用得最多的能力。很多应用侧操作是异步的——读文件、发请求、查数据库——前端不可能同步等。$TRAE_REF

应用侧返回一个 Promise:

class FileServiceClass {
readFile(path: string): Promise<string> {
return new Promise((resolve, reject) => {
// 模拟异步读文件
setTimeout(() => {
if (path) {
resolve(`文件内容:${path} 的数据`);
} else {
reject('路径不能为空');
}
}, 1000);
});
}
}

前端用 .then() / .catch() 正常处理:

function loadFile() {
fileService.readFile('notes/test.md')
.then((content) => {
document.getElementById('content').innerText = content;
})
.catch((err) => {
console.error('读取失败:', err);
});
}

异步方法的声明位置要注意:在 javaScriptProxy() 里,异步方法放在 asyncMethodList,不是 methodList。放错了前端调用会拿不到 Promise,这是个隐蔽的坑。

权限配置:上线前必须补上的一环

开发阶段为了方便,permission 经常留空。但真正要发布时,这一块是安全防线。权限配置是一个 JSON 字符串,分两层:对象级权限控制哪些 URL 能访问该对象的所有方法,方法级权限更细,控制哪些 URL 能访问特定方法。$TRAE_REF

{
"javascriptProxyPermission": {
"urlPermissionList": [
{
"scheme": "resource",
"host": "rawfile",
"port": "",
"path": ""
}
],
"methodList": [
{
"methodName": "getDeviceId",
"urlPermissionList": [
{
"scheme": "https",
"host": "your-trusted-domain.com",
"port": "",
"path": ""
}
]
}
]
}
}

几个匹配规则值得记住:

  • scheme 和 host 是精确匹配,不能为空;
  • port 精确匹配,留空表示不检查端口;
  • path 是前缀匹配,留空表示不检查路径。

我的实践建议:敏感方法单独配方法级权限,只放行可信域名。比如 getDeviceId、getToken 这类方法,对象级权限可能放得比较宽,但方法级权限收紧到只允许特定来源调用,这样即使页面被 XSS 注入了恶意脚本,也无法越权调到敏感方法。

几个踩坑心得

写到这里,把实战中反复出现的问题梳理一下,希望能帮你少走弯路。

第一,registerJavaScriptProxy() 之后忘记 refresh()。 前面已经强调过,这是最高频的坑。症状是前端调用报对象未定义,排查方向很容易跑偏到"是不是方法名拼错了"。记住:动态注册 = 注册 + refresh,缺一不可。

第二,异步方法放错列表。 methodList 放同步方法,asyncMethodList 放异步方法。如果异步方法放进了 methodList,前端调用时不会报错,但拿不到 Promise 对象,.then() 直接异常。判断依据:方法返回值是不是 Promise<T>,是就走 asyncMethodList。

第三,对象引用与状态更新。 注册到前端的对象,是 ArkTS 对象的引用。如果用 @State 声明并且对象内部状态变化,前端调用时拿到的是最新值。但如果整个对象被重新赋值(this.deviceInfo = new DeviceInfoClass()),前端持有的还是旧引用。需要重新注册或者避免整体替换。

第四,方法名大小写敏感。 methodList 里的方法名和 ArkTS 类里定义的方法名必须完全一致,包括大小写。getToken 写成 gettoken 不会报错,但前端调不到。

第五,前端调用时机。 即使用 javaScriptProxy(),也要保证前端在 DOM 加载完成后调用。最稳的做法是在 window.onload 或脚本放在 body 末尾,避免在对象还没注入时就触发调用。

第六,runJavaScript() 的字符串转义。 应用侧调前端时,如果参数里有引号、换行,直接拼字符串会出问题。建议对传入参数做 JSON 序列化后再拼到调用代码里,比如 runJavaScript('updateContent(' + JSON.stringify(data) + ')'),避免特殊字符破坏 JS 语法。

一个完整的双向通信例子

把前面的能力串起来,做一个最小但完整的双向通信 Demo:前端展示笔记列表,点击笔记调应用侧读取详情,应用侧读取完成后再调前端函数渲染详情。

应用侧:

// xxx.ets
import { webview } from '@kit.ArkWeb';

class NoteBridge {
getNoteList(): Array<{ id: number, title: string }> {
return [
{ id: 1, title: '平行视界适配心得' },
{ id: 2, title: 'ArkWeb 双向通信笔记' },
{ id: 3, title: '折叠屏布局实践' }
];
}

getNoteDetail(id: number): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`这是笔记 ${id} 的详细内容,由 ArkTS 异步读取。`);
}, 500);
});
}
}

@Entry
@Component
struct NoteWebPage {
webviewController: webview.WebviewController = new webview.WebviewController();
@State bridge: NoteBridge = new NoteBridge();

build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.webviewController })
.javaScriptProxy({
object: this.bridge,
name: 'noteBridge',
methodList: ['getNoteList'],
controller: this.webviewController,
asyncMethodList: ['getNoteDetail'],
permission: ''
})
}
}
}

前端:

<!DOCTYPE html>
<html>
<body>
<ul id="list"></ul>
<div id="detail"></div>
<script>
function renderList() {
const list = noteBridge.getNoteList();
const ul = document.getElementById('list');
ul.innerHTML = list.map(item =>
`<li onclick="loadDetail(${item.id})">${item.title}</li>`
).join('');
}

function loadDetail(id) {
noteBridge.getNoteDetail(id)
.then(content => {
document.getElementById('detail').innerText = content;
});
}

window.onload = renderList;
</script>
</body>
</html>

这个例子里,前端调应用侧的同步方法 getNoteList 拿列表,再调异步方法 getNoteDetail 拿详情,应用侧的 Promise 在前端用 .then() 接住。整条链路没有 HTTP 请求,没有 postMessage 序列化,调用就像本地函数一样直接。

延伸:除了 JavaScriptProxy 还有什么

JavaScriptProxy 解决的是"前端调应用侧",反过来 runJavaScript() 解决"应用侧调前端"。但如果需要持续的双向数据流,比如前端不断上报状态、应用侧不断推送更新,单次调用模式会比较啰嗦。

ArkWeb 还提供了 建立应用侧与前端页面数据通道 的能力(createWebMessagePort),可以建立一个持久化的消息端口,两侧互相 post 消息,适合实时性要求高的场景。这是 JavaScriptProxy 之外的另一条路,思路更接近浏览器的 MessageChannel。

我的选择思路是:

  • 一次性调用、请求-响应模式 → JavaScriptProxy + Promise;
  • 应用侧单向通知前端 → runJavaScript();
  • 持续双向通信、流式数据 → WebMessagePort。

三者不是互斥的,一个稍复杂的混合应用里,往往三种都会用到。

结语

从"前端调应用侧"这个点切入鸿蒙 ArkWeb,最大的感受是:鸿蒙在混合开发这块的设计相当克制和务实。没有发明一套全新的通信协议,而是沿用前端开发者熟悉的"对象注入 + Promise"模型,学习成本很低;同时通过 methodList / asyncMethodList 的显式声明、permission 的分层控制,把安全边界交还给开发者。

真正花时间的不是 API 本身,而是那些"文档写了但容易漏"的细节:refresh() 的必要性、异步方法的归属列表、对象引用的生命周期、权限 JSON 的匹配规则。把这些细节理清,混合开发的通信层就能搭得很稳。

下一篇打算聊聊 WebMessagePort 和 Web 组件的多实例管理,那是另一个有意思的方向。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 前端页面调用应用侧函数:鸿蒙 ArkWeb 双向通信学习笔记
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!