跳到主要内容鸿蒙 Web 组件混合开发实战 | 极客日志TypeScript大前端
鸿蒙 Web 组件混合开发实战
综述由AI生成前言 说句实在话,**只写原生的人,迟早要面对 H5;只写 H5 的人,也迟早会被拉去接原生**。不管你情不情愿,这年头'Hybrid 应用'已经是常态——活动页是前端同学的一套 Vue/React,主流程又是原生 ArkUI;老板一句'这个网页能不能直接嵌进去?'你就懂了:跑不掉了。 好消息是:鸿蒙给我们准备好了 **Web 组件**,用来做内嵌网页、Hybrid 混合开发。一套视图里,你既可以…
竹影清风25K 浏览 前言
说句实在话,只写原生的人,迟早要面对 H5;只写 H5 的人,也迟早会被拉去接原生。不管你情不情愿,这年头'Hybrid 应用'已经是常态——活动页是前端同学的一套 Vue/React,主流程又是原生 ArkUI;老板一句'这个网页能不能直接嵌进去?'你就懂了:跑不掉了。
好消息是:鸿蒙给我们准备好了 Web 组件,用来做内嵌网页、Hybrid 混合开发。一套视图里,你既可以用 ArkTS 写原生 UI,又能在一个区域里嵌一整块 H5,甚至还能 ArkTS ↔ JS 互相发消息,做登录、分享、设备能力调用这一整套联动。
但是啊,真要落地的时候,问题就出来了:
- Web 组件到底有哪些 API?能不能控制前进、后退、注入 JS?
- JS 到底怎么跟 ArkTS 说话?Native 又怎么把数据塞给网页?
- 本地 html / 资源加载老报错:404、跨域、白屏、缓存问题一大堆?
- 做 Hybrid 项目到底怎么设计结构,才能不变成'奇怪的大杂烩'?
今天就来好好聊聊这件事:
鸿蒙 Web 组件:嵌入网页与混合开发的完整实践。
一、Web 组件 API:先把'浏览器这块砖'掌握住
鸿蒙里用 Web 做嵌入网页,核心是 ArkUI 的 <Web> 组件 + WebController 控制对象。
1.1 最小可用 Web 组件示例
先来个最小 demo,有个感觉:
import webview from '@ohos.web.webview';
@Entry
@Component
struct WebDemoPage {
private controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Row() {
Button('Back').onClick(() => {
if (this.controller?.canGoBack()) {
this.controller.goBack();
}
})
Button('Reload').margin({ left: }).( ..())
}.({ : , : })
({
: ,
: .
})
.()
.()
.()
.()
.()
}
.()
.()
}
}
8
onClick
() =>
this
controller
reload
margin
top
8
left
12
Web
src
'https://www.example.com'
controller
this
controller
javaScriptAccess
true
zoomAccess
true
domStorageAccess
true
width
'100%'
height
'100%'
width
'100%'
height
'100%'
Web({ src, controller }):
src 可以是远程 URL,也可以是本地 html
controller 用来做'浏览器导航控制'
.javaScriptAccess(true):不开这个,很多 H5 功能直接废掉
.domStorageAccess(true):涉及 localStorage / sessionStorage 的页面要开
.zoomAccess(true):是否允许手势缩放(视业务看要不要)
可以把它理解成:ArkUI 里嵌了一个小浏览器窗口,你再用控制器来对它下命令。
1.2 WebviewController 能干啥?
private controller: webview.WebviewController = new webview.WebviewController();
常用 API 大概这些(只列最常用的,日常开发够用):
loadUrl(url: string):动态加载网页
reload():刷新当前页
canGoBack() / goBack():后退
canGoForward() / goForward():前进
stop():停止加载
zoomIn() / zoomOut():缩放
runJavaScript(script: string, callback?):执行 JS
Button('打开活动页').onClick(() => {
this.controller.loadUrl('https://m.xxx.com/promo/2025-11-11');
})
Button('调用 H5 函数').onClick(() => {
this.controller.runJavaScript('window.fromNative && window.fromNative("hello")');
})
二、JS ↔ ArkTS 双向交互:Hybrid 的灵魂
只嵌页面不交互,其实只能算'简单内嵌';
真正的 Hybrid,一定离不开:
- ArkTS 调 JS:比如通知网页登录结果、传 token、传用户信息
- JS 调 ArkTS:比如让原生弹出分享、打开扫码、调原生支付
2.1 ArkTS 调 JS:用 runJavaScript 注入
刚刚提过的 runJavaScript,是最直接、最常用的方式:
sendUserInfoToWeb(user: { id: string; nickname: string }) {
const json = JSON.stringify(user);
const script = `window.onNativeUserInfo && window.onNativeUserInfo(${json})`;
this.controller.runJavaScript(script);
}
window.onNativeUserInfo = function(user) {
console.log('收到来自原生的用户信息:', user)
document.getElementById('nickname').innerText = user.nickname
}
这种方式非常直接:ArkTS 拼出一段 JS 字符串,让 Webview 去执行。
适合'简单调用 + 传少量参数'。
2.2 JS 调 ArkTS:建立'消息通道'
- URL 拦截
- JS 修改 location,例如
myapp://doSomething?xxx=yyy
- ArkTS 侧通过 Web 的 URL 拦截回调解析
- 消息机制(推荐)
下面以一种常见的'约定 URL 协议 + 拦截'方式举例(思路清晰易懂):
function callNativeShare(data) {
const encoded = encodeURIComponent(JSON.stringify(data))
window.location.href = `myapp://share?data=${encoded}`
}
ArkTS 侧监听 URL 变化(具体 API 名称依版本有所差异,这里用伪代码表达下思路):
Web({ src: this.url, controller: this.controller }).onUrlLoad((event) => {
const url = event.url
if (url.startsWith('myapp://')) {
this.handleJsBridge(url)
event.preventDefault && event.preventDefault()
}
})
handleJsBridge(url: string) {
const [schemaPart, queryPart] = url.split('?')
if (schemaPart === 'myapp://share') {
const searchParams = new URLSearchParams(queryPart)
const data = searchParams.get('data')
if (data) {
const payload = JSON.parse(decodeURIComponent(data))
this.doShare(payload)
}
}
}
doShare 就是原生侧的分享逻辑:跳系统分享、调 SDK 都可以。
- 前端同学很好理解,只要改 URL 就行
- ArkTS 这边只要解析 URL 即可
- 不容易乱七八糟
当然,如果后续用到官方提供的 MessagePort、WebMessage 之类更正式的通道也可以,思路是一样的:双方约定协议 + 明确函数名 + 数据格式 JSON 化。
2.3 一份常用交互约定建议
为了不搞成一团糊,建议团队约定个简单的'JSBridge 协议',比如:
- JS → Native:
myapp://action?data=xxx
action:动作名称,比如 login, share, openScanner
data:JSON 字符串,需 encodeURIComponent
- Native → JS:统一通过
window.onNativeMessage && window.onNativeMessage({ type, payload })
sendToWeb(type: string, payload: any) {
const json = JSON.stringify({ type, payload })
const script = `window.onNativeMessage && window.onNativeMessage(${json})`
this.controller.runJavaScript(script)
}
window.onNativeMessage = function(msg) {
switch (msg.type) {
case 'loginSuccess':
break
case 'refreshToken':
break
}
}
三、资源加载问题:本地 html、图片、缓存那些坑
实际项目里,很少所有内容都在远端,有很多 Hybrid 场景会用到:
- 本地打包 html 页面(比如某个内置帮助页、离线页面)
- H5 里引用的静态资源(图片、css、js)
- 缓存 / 刷新 / 404 / 白屏 等问题
3.1 本地 html 加载
假设你在工程 resources/rawfile 下放了一个 help/index.html:
entry
├── src
└── resources
└── rawfile
└── help
└── index.html
ArkTS 里可以这样加载(不同版本写法略有区别,示意):
import webview from '@ohos.web.webview';
import resourceManager from '@ohos.resourceManager';
@Entry
@Component
struct LocalHtmlPage {
private controller: webview.WebviewController = new webview.WebviewController();
aboutToAppear() {
this.controller.loadUrl('rawfile://help/index.html');
}
build() {
Web({
src: '',
controller: this.controller
})
.javaScriptAccess(true)
.width('100%')
.height('100%')
}
}
如果你遇到'本地 html 里的 js/css 404',别慌:
- 检查资源路径是否相对正确(
./js/main.js vs /js/main.js)
- 确保 html 所在路径和资源目录结构对应
- 有些场景下需要使用
loadData 来加载 html 字符串与 baseUrl
3.2 远程资源加载与跨域
- H5 引用了第三方接口,报跨域
- 一些图片 / 脚本走 http 而页面是 https,会有'混合内容'警告
- Cookie / Storage 导致登录态混乱
一般 Hybrid 项目里,这些应该让前端统一配置:
- 尽量全站 https
- 域名统一(或 CORS 设置完整)
- 不在 H5 里随便写死业务域名,而是通过配置或注入方式
- 不乱把 Web 当浏览器那样打开任何地址
- 对加载失败提供 fallback 页面或错误提示
3.3 缓存与刷新
'我们后台刚改完 H5,为什么用户还看到旧版本?'
- H5 侧给静态资源加版本号:
main.js?v=20251114
- 活动页面 URL 自身也带上版本:
/promo/2025?v=2
- ArkTS 这边在必要时调用
controller.reload() 强刷
- 为调试提供一个隐藏长按入口:长按某个区域 3 秒 → 出现'清缓存 / 刷新 H5 配置'的选项
四、Hybrid 应用开发案例:一套'原生 + H5 活动中心'的组合拳
说了这么多,咱们用一个稍微贴近实战的案例,把整个流程串一下:
场景:
你在做一个电商类 App,首页、商品详情、购物车等都是 ArkUI 原生写的。
运营同学三天两头丢一个'营销活动 H5 地址'给你,要你接入:需要在 App 里打开要能拿到用户登录态 H5 里点击'立即分享'要调原生分享有时要调起原生支付
4.1 页面结构设计
- 标题栏:原生
- 网页区域:Web 组件
- 底部可以视情况扩展一些原生按钮(比如'关闭'、'回到首页')
@Entry
@Component
struct HybridActivityPage {
private controller: webview.WebviewController = new webview.WebviewController();
private url: string = '';
aboutToAppear() {
const params = router.getParams() as Record<string, string>
this.url = params?.url ?? ''
}
build() {
Column() {
Row() {
Button('< 返回').onClick(() => {
if (this.controller.canGoBack()) {
this.controller.goBack()
} else {
router.back()
}
})
Text('活动页面').fontSize(18).margin({ left: 12 })
}
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor(0xFFFFFF)
Web({
src: this.url,
controller: this.controller
})
.onPageEnd((res) => {
console.info('页面加载完成', JSON.stringify(res))
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
4.2 登录态注入:ArkTS → JS
用户登录在原生侧完成,H5 又想知道当前用户是谁,这个场景非常普遍。
aboutToAppear() {
this.controller.on('pageEnd', () => {
const user = {
id: 'u123',
token: 'xxxx',
nick: 'Mark'
}
const json = JSON.stringify(user)
const script = `window.onNativeLogin && window.onNativeLogin(${json});`
this.controller.runJavaScript(script)
})
}
window.onNativeLogin = function(user) {
localStorage.setItem('token', user.token)
renderUserName(user.nick)
}
4.3 JS 调原生分享
H5 里很多'分享活动给好友'按钮,其实都希望调用 App 原生的分享面板,而不是简单用 H5 的那套。
function triggerShare() {
const payload = {
title: '双十一神券大放送',
desc: '全场满 199 减 100',
url: window.location.href,
icon: 'https://xxx.com/promo.png'
}
const encoded = encodeURIComponent(JSON.stringify(payload))
window.location.href = `myapp://share?data=${encoded}`
}
Web({ src: this.url, controller: this.controller }).onUrlLoad((event) => {
const url = event.url
if (url.startsWith('myapp://')) {
this.handleBridge(url)
event.preventDefault && event.preventDefault()
}
})
handleBridge(url: string) {
const [schemaPart, queryPart] = url.split('?')
const params = new URLSearchParams(queryPart)
const data = params.get('data')
const payload = data ? JSON.parse(decodeURIComponent(data)) : null
if (schemaPart === 'myapp://share' && payload) {
this.doShare(payload)
}
}
doShare(payload: { title: string; desc: string; url: string; icon?: string }) {
console.info('执行原生分享:', JSON.stringify(payload))
}
4.4 Hybrid 工程结构建议
entry
├── src
│ └── main
│ └── ets
│ ├── ability
│ │ └── MainAbility.ets
│ ├── pages
│ │ ├── HomePage.ets
│ │ ├── ProductDetailPage.ets
│ │ └── HybridActivityPage.ets
│ ├── hybrid
│ │ ├── bridge
│ │ │ └── JsBridgeHandler.ets
│ │ └── config
│ │ └── HybridConfig.ets
│ ├── services
│ │ └── api
│ └── ...
└── resources
└── rawfile
└── hybrid
└── offline.html
- ArkTS 与 JS 交互逻辑集中管理 →
hybrid/bridge
- 域名白名单 / 特殊跳转策略单独配置 → 防止随便加载未知地址
- 离线兜底页(offline html)单独放 rawfile
- Web 组件最好封装成一个复用组件,例如
HybridWebView,不要在每个页面重复写乱七八糟的逻辑
最后一点小感慨
混合开发这东西,说简单也简单,说复杂也复杂。简单是因为技术上无非就那几个点:
嵌 Web、控导航、搞交互、管资源。
复杂是因为你得协调:
前端、客户端、服务端、运营的各种诉求,既要'随时能改 H5',又要'体验接近原生'。
鸿蒙给了我们一个不错的工具:Web 组件 + WebviewController + 通信能力,剩下的就看你怎么把这些拼成一个有秩序、好维护的 Hybrid 体系。
- Web 组件 API:怎么嵌、怎么控、能做哪些事
- JS ↔ ArkTS 通信:怎么设计协议、怎么落地实现
- 本地 / 远程资源加载要注意什么、怎么防坑
- Hybrid 案例:登录态、分享、活动中心怎么串起来
那你在鸿蒙这边搞 Hybrid,基本就算是'入门 + 实战'一条龙了。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online