文章目录
- 前言
- 运行效果图
- 整体架构:两层结构,各司其职
- 第一步:本地 HTML 放在哪?
- 第二步:开启必要权限
- 第三步:HTML 里的 Hash 路由怎么写
- 第四步:ArkTS → JS 方向通信
- 第五步:JS → ArkTS 方向通信(JSBridge)
- 双向通信全链路汇总
- 踩坑记录
- 写在最后
前言
在 HarmonyOS 开发里有个很常见但不太好搞的需求:把一个本地 HTML 页面嵌进 App,还要和原生 ArkTS 代码互相通信。
本篇文章就拿一个真实的WebHashRoutePage实现来拆解,把整个链路讲清楚。代码不多,但坑不少。
项目文件地址 : https://gitcode.com/QZRuoCheng/demoCase
运行效果图
整体架构:两层结构,各司其职
这个方案的核心思路很清晰:
- ArkTS 层:负责原生导航栏、底部按钮、状态管理
- Web 层:加载 rawfile 目录里的
index.html,跑纯前端 Hash 路由
两层之间通过JSBridge互相喊话,谁都不越界。
第一步:本地 HTML 放在哪?
放在src/main/resources/rawfile/目录下,比如index.html。
加载方式:
Web({src:$rawfile('index.html'),controller:this.controller})$rawfile()是 ArkTS 提供的资源引用语法,专门用来加载 rawfile 目录里的文件。不能用相对路径,也不能用file://,就得用这个。
第二步:开启必要权限
Web 组件默认很保守,要用 JS 就得手动开:
Web({src:$rawfile('index.html'),controller:this.controller}).javaScriptAccess(true)// 允许 JS 执行,必须开.domStorageAccess(true)// 允许 localStorage.fileAccess(true)// 允许访问本地文件.mixedMode(MixedMode.All)// 允许混合内容.cacheMode(CacheMode.Default)javaScriptAccess(true)是最关键的,没开的话 JS 直接不跑,啥都没有。
第三步:HTML 里的 Hash 路由怎么写
这个index.html实现了一个轻量 Hash 路由引擎,核心逻辑就这几十行:
// 路由表:路径 → 渲染函数varroutes={'/home':renderHome,'/list':renderList,'/detail':renderDetail,'/about':renderAbout};// 路由核心逻辑functionroute(){varhash=window.location.hash||'#/home';varpath=hash.split('?')[0].replace('#','');// 去掉 # 和查询参数varrenderFn=routes[path];// 更新导航高亮document.querySelectorAll('.nav a').forEach(function(a){a.className=a.getAttribute('data-route')==='#'+path?'active':'';});// 渲染页面内容document.getElementById('app').innerHTML=renderFn?renderFn():'<div class="page"><h2>404</h2></div>';}// 监听 Hash 变化window.addEventListener('hashchange',route);// 初始加载document.addEventListener('DOMContentLoaded',function(){if(!window.location.hash)window.location.hash='#/home';route();});路由跳转很简单,直接改window.location.hash就行:
// JS 内部跳转window.location.hash='#/list';// 带参数跳转(Detail 页解析 id)functiongoDetail(id){window.location.hash='#/detail?id='+id;}Detail 页解析参数:
functionrenderDetail(){varhash=window.location.hash;varid=hash.split('?id=')[1]||'0';// 拿到 id 参数// ...}第四步:ArkTS → JS 方向通信
从原生侧触发 Web 里的路由跳转,用runJavaScript():
privatenavigateTo(hash:string){this.currentRoute=hash// 同步 ArkTS 状态this.controller.runJavaScript(`window.location.hash = '${hash}'`)// 驱动 JS 路由}底部按钮点击时调用这个方法:
Button('列表').backgroundColor(this.currentRoute==='#/list'?'#007DFF':'#CCCCCC')// 高亮当前路由.onClick(()=>this.navigateTo('#/list'))注意:runJavaScript()是异步的,但这里直接改 hash 不需要等回调,所以没问题。
页面加载完成后,也要主动同步一次当前 hash:
.onPageEnd(()=>{this.controller.runJavaScript('window.location.hash',(err,result)=>{if(!err&&result){this.currentRoute=result// 把 JS 里的 hash 同步到 ArkTS 状态}})})第五步:JS → ArkTS 方向通信(JSBridge)
这是整个方案里最关键的部分,也是最容易踩坑的地方。
先定义桥接对象:
classNativeBridge{privatepage:WebHashRoutePageconstructor(page:WebHashRoutePage){this.page=page}// JS 调用:window.nativeBridge.onRouteChange('#/list')onRouteChange(route:string){this.page.currentRoute=route}// JS 调用:window.nativeBridge.showToast('hello')showToast(msg:string){promptAction.showToast({message:msg,duration:2000})}}注册 JSProxy(必须在页面加载前):
privatebridge:NativeBridge=newNativeBridge(this)aboutToAppear(){try{this.controller.registerJavaScriptProxy(this.bridge,// 暴露的对象'nativeBridge',// JS 里的变量名:window.nativeBridge['onRouteChange','showToast']// 允许被 JS 调用的方法白名单)}catch(e){console.error('注册JS Proxy失败: '+JSON.stringify(e))}}** 关键点**:registerJavaScriptProxy必须在aboutToAppear()里调用,也就是页面加载之前注册。如果在onPageEnd之后注册,JS 那边window.nativeBridge就是 undefined。
JS 侧调用方式:
functionnotifyArkTS(){if(window.nativeBridge&&window.nativeBridge.showToast){window.nativeBridge.showToast('来自 JS 的消息');}}调用前一定要判空,因为如果 Web 组件在某些设备上没有正确注册 Proxy,直接调用会报错。
双向通信全链路汇总
| 方向 | 方式 | 代码示例 |
|---|---|---|
| ArkTS → JS | controller.runJavaScript() | controller.runJavaScript("window.location.hash = '#/list'") |
| JS → ArkTS | window.nativeBridge.xxx() | window.nativeBridge.showToast('hello') |
| ArkTS 读 JS 数据 | runJavaScript()带回调 | controller.runJavaScript('window.location.hash', callback) |
| ArkTS 监听 JS 事件 | 注册 JSProxy 方法 | registerJavaScriptProxy(bridge, 'nativeBridge', ['onRouteChange']) |
踩坑记录
坑1:JSProxy 注册时机
registerJavaScriptProxy必须在aboutToAppear()里调用,不能在onPageEnd里。官方文档说"页面加载前注册",但没说清楚具体位置。踩过一次才知道。
坑2:runJavaScript 的字符串拼接
// 容易出问题this.controller.runJavaScript(`window.location.hash = '${hash}'`)// 如果 hash 里包含单引号就会语法错误// 更安全的写法this.controller.runJavaScript(`window.location.hash =${JSON.stringify(hash)}`)坑3:JS 调用 ArkTS 方法白名单
registerJavaScriptProxy第三个参数是方法白名单,只有在列表里的方法才能被 JS 调用。如果在 JS 里调用了一个不在白名单里的方法,会静默失败,不报错,但也没效果。
坑4:javaScriptAccess默认是 false
Web 组件默认不允许执行 JS。这个属性没有在文档里特别强调,很多人直接用 Web 组件加载 HTML,然后发现 JS 完全不跑,排查半天。
写在最后
这套方案适合什么场景?
适合:已有 H5 页面需要嵌入原生 App,要和原生能力互调(Toast、导航、数据存储)。
不适合:纯前端内容展示(直接用 Web 组件就行,不用 JSBridge);或者复杂的双向数据同步(考虑用原生 ArkUI 组件代替)。
Hash 路由本身没什么难度,难的是 ArkTS 和 JS 之间的状态同步——谁是状态的"真相来源"要想清楚。这个示例里 ArkTS 的currentRoute是主状态,JS 的 hash 跟着 ArkTS 走,方向很清晰,不会乱。