做过 H5 混合开发的同学都知道:最难受的不是写代码,而是 Native 和 Web 之间那道"墙"。
在鸿蒙里,这道墙叫 ArkWeb,打通它你需要看这篇。
一、为什么要做混合开发?
纯鸿蒙原生性能最优,但现实项目里有大量历史 H5 页面、跨平台运营活动页,全部用 ArkTS 重写成本高到离谱。混合开发(Hybrid)是工程化的最优解:
- H5 负责:活动营销、内容展示、频繁迭代的功能
- Native 负责:登录鉴权、支付、设备能力、高性能渲染
鸿蒙提供的 Web 组件(底层是 ArkWeb 引擎)是实现混合开发的核心,本文从零带你吃透它。
二、ArkWeb 基础:搭起骨架
2.1 最简单的 Web 组件
typescript
// pages/WebDemo.ets
@Entry
@Component
struct WebDemo {
// WebviewController 是控制 Web 组件的遥控器
private webController: webview.WebviewController = new webview.WebviewController()
build() {
Column() {
Web({ src: 'https://example.com', controller: this.webController })
.width('100%')
.height('100%')
}
}
}
注意:在 module.json5 中需要声明网络权限:
json
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
}
}
2.2 加载本地 HTML(rawfile)
把 H5 文件放到 resources/rawfile/ 目录:
typescript
Web({
src: $rawfile('index.html'), // 加载 rawfile 中的本地 HTML
controller: this.webController
})
本地 HTML 可以使用相对路径引用同目录的 CSS、JS 文件。
2.3 核心生命周期回调
typescript
Web({ src: 'https://example.com', controller: this.webController })
.onPageBegin((event) => {
// 页面开始加载,可以在这里显示 Loading
console.log('开始加载:', event?.url)
})
.onPageEnd((event) => {
// 页面加载完成,DOM 已就绪
console.log('加载完成:', event?.url)
// ✅ 推荐在这里执行 JS 注入
this.injectToken()
})
.onErrorReceive((event) => {
// 网络错误、资源加载失败
console.error('加载失败:', event?.error.getErrorInfo())
})
.onHttpErrorReceive((event) => {
// HTTP 状态码错误(4xx/5xx)
console.warn('HTTP错误:', event?.response.getResponseCode())
})
三、Native → Web:向 H5 注入能力
3.1 runJavaScript:直接执行 JS
这是最简单直接的方式,页面加载完成后调用:
typescript
// 向 H5 注入用户 Token
private injectToken(): void {
const token = UserService.getToken()
const userInfo = JSON.stringify({
token: token,
userId: UserService.getUserId(),
nickname: UserService.getNickname()
})
// runJavaScript 在主线程执行,返回 Promise<string>
this.webController.runJavaScript(
`window.__NATIVE_BRIDGE__.setUserInfo(${userInfo})`
).then((result) => {
console.log('注入结果:', result) // H5 函数的返回值(字符串)
}).catch((err: Error) => {
console.error('注入失败:', err.message)
})
}
H5 端配合代码:
javascript
// H5 中提前注册好接收函数
window.__NATIVE_BRIDGE__ = {
setUserInfo(userInfo) {
store.commit('SET_USER', userInfo)
console.log('收到 Native 注入的用户信息', userInfo)
return 'ok' // 这个返回值会被 runJavaScript 的 Promise 接收到
}
}
3.2 loadUrl + 参数:通过 URL 传参
适合简单场景(Token 不要放 URL!):
typescript
// 页面跳转时带上必要参数
this.webController.loadUrl(
'https://example.com/activity?source=app&version=1.2.0'
)
3.3 Cookie 注入:最"无感"的方式
对于需要 SSO 登录的场景,直接往 WebView 注入 Cookie:
typescript
import webview from '@ohos.web.webview'
// 注意:setCookie 是同步操作,建议在 onPageBegin 前调用
async function injectCookieForSso(domain: string, token: string): Promise<void> {
try {
webview.WebCookieManager.setCookie(
domain,
`auth_token=${token}; Path=/; Secure; HttpOnly`
)
// 如果需要立刻生效(某些机型有缓存),强制刷新 Cookie Store
await webview.WebCookieManager.saveCookieAsync()
console.log('Cookie 注入成功')
} catch (err) {
console.error('Cookie 注入失败:', (err as Error).message)
}
}
四、Web → Native:H5 反向调用原生能力
这是混合开发的精髓,也是最容易踩坑的地方。
4.1 javaScriptProxy:注册 JS 对象(推荐)
通过 javaScriptProxy 属性,将一个 ArkTS 对象暴露给 H5 的 window 对象:
typescript
// 定义要暴露给 H5 的 Native 接口对象
class NativeBridge {
// ✅ 方法必须是同步的
openCamera(): string {
// 触发摄像头
router.pushUrl({ url: 'pages/CameraPage' })
return JSON.stringify({ code: 0, msg: 'ok' })
}
getDeviceInfo(): string {
// 返回设备信息
return JSON.stringify({
brand: deviceInfo.brand,
model: deviceInfo.productModel,
osVersion: deviceInfo.osFullName
})
}
// 支付接口(H5 调起 Native 支付)
launchPay(orderJson: string): string {
const order = JSON.parse(orderJson) as OrderInfo
// 触发 Native 支付流程
PayService.launch(order)
return JSON.stringify({ code: 0, msg: 'launched' })
}
// 关闭 WebView(H5 触发 Native 返回)
closeWebView(): string {
router.back()
return JSON.stringify({ code: 0 })
}
}
@Entry
@Component
struct HybridPage {
private webController: webview.WebviewController = new webview.WebviewController()
private nativeBridge: NativeBridge = new NativeBridge()
build() {
Web({ src: $rawfile('h5/index.html'), controller: this.webController })
.javaScriptProxy({
object: this.nativeBridge, // 要注入的对象
name: 'NativeBridge', // H5 中使用的名称:window.NativeBridge
methodList: [ // 白名单:只暴露这些方法
'openCamera',
'getDeviceInfo',
'launchPay',
'closeWebView'
],
controller: this.webController
})
}
}
H5 端调用方式:
javascript
// H5 直接调用 window.NativeBridge 上的方法
function openCamera() {
const result = window.NativeBridge.openCamera()
const res = JSON.parse(result)
if (res.code === 0) {
console.log('摄像头已打开')
}
}
// 获取设备信息
const deviceInfo = JSON.parse(window.NativeBridge.getDeviceInfo())
console.log('设备型号:', deviceInfo.model)
⚠️ 关键坑点 :
javaScriptProxy注册的方法只能同步返回字符串。如果你需要异步操作(比如等待支付结果),需要用回调方案(见下文)。
4.2 异步回调:用 runJavaScript 反向通知
对于需要异步结果的场景(支付回调、拍照完成后返回图片),流程是:
- H5 调用 Native 方法,Native 开始异步操作
- Native 异步操作完成后,用
runJavaScript将结果推给 H5
typescript
// NativeBridge 中的支付方法(异步流程)
launchPay(orderJson: string): string {
const order = JSON.parse(orderJson) as OrderInfo
// 开启异步支付,完成后回调 H5
PayService.launch(order, (payResult: PayResult) => {
const resultJson = JSON.stringify(payResult)
// 支付完成,调用 H5 的回调函数
this.webController.runJavaScript(
`window.__NATIVE_CALLBACK__.onPayResult(${resultJson})`
)
})
return JSON.stringify({ code: 0, msg: 'payment_launched' })
}
H5 端:
javascript
// 注册 Native 回调接收点
window.__NATIVE_CALLBACK__ = {
onPayResult(result) {
if (result.code === 0) {
showSuccessToast('支付成功')
refreshOrderList()
} else {
showErrorToast('支付失败: ' + result.msg)
}
}
}
4.3 URL 拦截(shouldOverrideUrlLoading)
适合"协议化"的 JSBridge 方案,通过自定义 URL Scheme 拦截:
typescript
Web({ src: ..., controller: this.webController })
.onInterceptRequest((event) => {
const url = event?.request.getRequestUrl() ?? ''
// 拦截自定义协议
if (url.startsWith('hybrid://')) {
this.handleHybridScheme(url)
// 返回空响应,阻止 WebView 真正加载这个 URL
return new WebResourceResponse()
}
// 其他请求正常处理
return null
})
private handleHybridScheme(url: string): void {
const uri = new URL(url)
const action = uri.hostname // hybrid://openCamera
const params = Object.fromEntries(uri.searchParams)
switch (action) {
case 'openCamera':
router.pushUrl({ url: 'pages/CameraPage' })
break
case 'share':
this.triggerShare(params)
break
case 'close':
router.back()
break
}
}
H5 端:
javascript
// 通过 iframe src 或 location.href 触发协议
function callNative(action, params) {
const query = new URLSearchParams(params).toString()
location.href = `hybrid://${action}?${query}`
}
// 示例:调起分享
callNative('share', { title: '我的文章', url: 'https://...' })
五、完整实战:一个有登录、有支付的混合页面
下面是整合了上述技术点的完整实战代码:
typescript
// pages/ShopHybridPage.ets
import webview from '@ohos.web.webview'
import router from '@ohos.router'
import { UserService } from '../services/UserService'
import { PayService, PayResult } from '../services/PayService'
class ShopNativeBridge {
// webController 引用,用于异步回调
private controller: webview.WebviewController
constructor(controller: webview.WebviewController) {
this.controller = controller
}
// 获取登录态
getLoginInfo(): string {
if (!UserService.isLoggedIn()) {
return JSON.stringify({ code: 401, msg: 'not_logged_in' })
}
return JSON.stringify({
code: 0,
data: {
token: UserService.getToken(),
userId: UserService.getUserId(),
avatarUrl: UserService.getAvatarUrl()
}
})
}
// 唤起登录页(异步流程)
launchLogin(): string {
router.pushUrl({
url: 'pages/LoginPage',
params: { fromHybrid: true }
})
// 登录完成后通过 EventBus 通知
EventBus.once('loginSuccess', () => {
const loginInfo = this.getLoginInfo()
this.controller.runJavaScript(
`window.HybridCallback.onLoginResult(${loginInfo})`
)
})
return JSON.stringify({ code: 0, msg: 'login_page_opened' })
}
// 唤起支付
launchPay(orderJson: string): string {
const order = JSON.parse(orderJson)
PayService.launch(order, (result: PayResult) => {
this.controller.runJavaScript(
`window.HybridCallback.onPayResult(${JSON.stringify(result)})`
)
})
return JSON.stringify({ code: 0, msg: 'pay_launched' })
}
// 复制到剪贴板
copyToClipboard(text: string): string {
pasteboard.getSystemPasteboard().setData(
pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text)
)
return JSON.stringify({ code: 0 })
}
}
@Entry
@Component
struct ShopHybridPage {
@State isLoading: boolean = true
private webController: webview.WebviewController = new webview.WebviewController()
private bridge: ShopNativeBridge = new ShopNativeBridge(this.webController)
// 获取路由参数
private pageUrl: string = (router.getParams() as Record<string, string>)?.url ?? 'https://shop.example.com'
aboutToAppear(): void {
// 提前注入登录 Cookie(如果已登录)
if (UserService.isLoggedIn()) {
webview.WebCookieManager.setCookie(
'shop.example.com',
`auth=${UserService.getToken()}; Path=/`
)
}
}
build() {
Stack() {
// Web 主体
Web({ src: this.pageUrl, controller: this.webController })
.width('100%')
.height('100%')
.javaScriptProxy({
object: this.bridge,
name: 'NativeBridge',
methodList: ['getLoginInfo', 'launchLogin', 'launchPay', 'copyToClipboard'],
controller: this.webController
})
.onPageBegin(() => {
this.isLoading = true
})
.onPageEnd(() => {
this.isLoading = false
})
.onInterceptRequest((event) => {
const url = event?.request.getRequestUrl() ?? ''
if (url.startsWith('hybrid://close')) {
router.back()
return new WebResourceResponse()
}
return null
})
// Loading 遮罩
if (this.isLoading) {
Column() {
LoadingProgress()
.width(40)
.height(40)
.color('#007DFF')
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
六、常见坑与解法速查
| 坑位 | 现象 | 原因 | 解法 |
|---|---|---|---|
javaScriptProxy 方法不触发 |
H5 调用无响应 | methodList 未包含该方法 | 在 methodList 中添加方法名 |
runJavaScript 报空指针 |
偶发崩溃 | 在 onPageBegin 前调用 | 移至 onPageEnd 回调中 |
| Cookie 注入不生效 | H5 仍跳登录 | 时序问题,WebView 已启动 | 在 Web 组件创建前注入 Cookie |
| H5 内跳转 Native 页面被拦截 | 路由失效 | URL 被 WebView 加载而非拦截 | 在 onInterceptRequest 中处理 |
| javaScriptProxy 异步不支持 | 返回 undefined | 接口只支持同步返回字符串 | 改用 runJavaScript 反向通知 |
| HTTPS 混合内容警告 | 图片/资源不加载 | HTTP 资源在 HTTPS 页面被阻止 | .mixedMode(MixedMode.All) |
| 调试看不到日志 | 不知道 H5 报什么错 | DevTools 未开启 | .webDebuggingAccess(true) |
typescript
// 开启 DevTools(仅 Debug 构建)
Web({ ... })
.webDebuggingAccess(true) // 允许 Chrome DevTools 调试
.mixedMode(MixedMode.All) // 允许 HTTP 混合内容(慎用)
.domStorageAccess(true) // 允许 localStorage/sessionStorage
.fileAccess(true) // 允许访问文件系统
七、安全注意事项
混合开发有几条安全红线,碰了会出大事:
- 不要把 Token 放 URL 参数:URL 会出现在日志、浏览器历史里,用 Cookie 或 JS 注入代替
- methodList 要精确:暴露最小必要接口,不要把整个对象都暴露
- 域名白名单 :对
javaScriptProxy只对信任域名开启 - 输入校验 :
launchPay等接口的orderJson一定要做类型校验,防止 H5 被 XSS 注入后攻击 Native - 不要在 Release 包开启 webDebuggingAccess
总结
鸿蒙 ArkWeb 混合开发的核心通信方案:
css
Native → H5:runJavaScript() / Cookie 注入
H5 → Native:javaScriptProxy / URL 拦截(onInterceptRequest)
异步回调:H5调Native启动异步 → Native完成后runJavaScript推结果
掌握这几把钥匙,你就能在鸿蒙上搭出一套稳健的 Hybrid 架构,让历史 H5 资产无缝复用,新功能用原生拿到最优性能。