HarmonyOS 6商城开发学习:Web商品详情页标题显示URL的根治——H5 <title>治理 + onTitleReceive防泄漏
做购物比价/商城时,商品详情页往往是一张 H5 活动页/商品落地页(运营自己发版、随时换活动),外面包一层 Web组件。你切到这个页面,顶部(或导航栏中间)有时会赫然出现一行字:
https://m.xxx.com/promo/summer-sale.html?utm_source=xxx&...
不是标题,不是"夏日焕新季",而是一整坨原始URL——看起来像页面没加载完,也像你 App 泄露了链接结构,运营和产品都会皱眉。
华为官方购物比价实践把这个归为一条高频FAQ,根因一句话:
H5 的
<head>里没给<title>,或 title 内容为空,于是 Web 容器的标题回退机制就把当前加载的 URL 显示出来了。
下面把"为什么出现 → H5侧怎么治 → ArkUI侧怎么接 → 防泄漏兜底"完整走一遍。
一、先确认:它不是"你读错字段",而是 H5 没给标题
浏览器/Web容器对标题的优先级通常是:
-
<title>内容存在且非空 → 用 title -
否则 → 回退到其他可显示信息(很多实现里就是 URL 字符串,或 URL 的 path/host 变体)
所以你看到标题区显示:
https://m.shop.com/promo/summer-sale.html?utm_source=xxx
系统没坏——它只是在说:"你要我显示标题,但你没告诉我标题是什么,我只能用链接顶着。"
二、H5侧根治:运营/前端必须写 <title>,而且不能只写 SEO 那套
最常见翻车是:前端同学把标题只写进 JS 动态设置(document.title = ...),但页面初始 HTML 里 <title>是空的或者占位:
<!-- ❌ 空标题 / 占位,加载瞬间Web容器读到就是空 -->
<head>
<title></title>
<!-- 后面JS才设 document.title,但标题已在UI上闪过URL了 -->
</head>
✅ 正确写法——SSR/静态HTML里直接给确定值
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<!-- ✅ 关键:给一个人类可读的标题 -->
<title>夏日焕新季 · 精选爆款低至5折 | 某商城</title>
<!-- 可选但推荐:opengraph也跟上,分享回流更稳 -->
<meta property="og:title" content="夏日焕新季 · 精选爆款低至5折" />
</head>
<body>
…
</body>
</html>
三句话守则这个文件:
-
<title>别空着,也别只写" "空格(很多容器仍当空处理) -
标题尽量短而有辨识度(顶部导航区空间有限)
-
动态SPA场景:至少给一个合理的初始
<title>,JS 改document.title只是"后来追改",不能消除第一帧的空档
三、ArkUI侧正确接法:onTitleReceive接管,别让 URL 有机会裸奔
即使 H5 侧写对了,你的 ArkUI 代码也要做到两件事:
-
从 Web 事件里拿标题(
event.title) -
做一层清洗:如果标题看起来像 URL / 空 / 纯
/路径 → 强制显示你自己的兜底文案
3.1 最小正确写法(代码克制版)
// pages/ProductH5Page.ets
import { webview } from '@kit.ArkWeb'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct ProductH5Page {
private ctrl = new webview.WebviewController()
// 页面标题(给导航栏中间显示)
@State pageTitle: string = '商品详情'
// URL(只用于埋点/分享,绝对不直接当标题显示)
private rawUrl = 'https://m.shop.com/promo/summer-sale.html'
aboutToAppear() {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext
// 如果你要做沉浸式全屏(视频/活动页常见),再开;否则不需要
// window.getLastWindow(ctx).then(w => w.setWindowLayoutFullScreen(true))
}
/** 清洗标题:URL泄漏防护 */
private sanitizeTitle(v: string | undefined): string {
if (!v || v.trim().length === 0) return '商品详情'
// 像URL就掐掉
if (/^https?:\/\//.test(v.trim())) return '商品详情'
// 有些H5会给 title=" /promo/..." 这种路径
if (v.trim().startsWith('/')) return '商品详情'
return v.trim()
}
build() {
Column() {
// —— 导航栏(你自己画的,不是系统ActionBar)——
Row() {
Text(this.pageTitle)
.fontSize(17).fontWeight(700)
}
.width('100%')
.height(48)
.padding({ left: 16 })
.backgroundColor('#FFFFFF')
// —— Web主体 ——
Web({ src: this.rawUrl, controller: this.ctrl })
.width('100%')
.layoutWeight(1)
.javaScriptAccess(true)
.domStorageAccess(true)
// ✅ 从Web侧拿标题,过清洗再上屏
.onTitleReceive((ev) => {
const t = this.sanitizeTitle(ev?.title)
this.pageTitle = t
// 可选:顺手把标题回灌给H5(防某些SPA里title抖动)
// this.ctrl.runJavaScript(`document.title = ${JSON.stringify(t)}`)
})
// 加载失败兜底
.onErrorReceive((req, err) => {
// 不把err信息当标题!
this.pageTitle = '商品详情'
})
}
.width('100%')
.height('100%')
}
}
3.2 为什么必须 sanitizeTitle
你可能会想:"我H5写了title就不会有问题了吧?"
现实里仍有两条漏道:
-
加载第一帧 vs
<title>解析完成的时序差:第一瞬 Web 容器可能用 URL 顶着,直到 DOM/网络标题就绪才触发onTitleReceive——如果你不清洗,哪怕50ms,标题区也闪一下URL -
H5侧JS改 title但改成了脏值:有人 debug 写了一句
document.title = location.href,就漏了
sanitizeTitle就是你的安全网,保证"URL永远当不成标题"。
四、沉浸式全屏活动页的衍生坑(官方示例里藏着这个坑)
官方示例里为了"全屏视频/H5活动页"做了:
window.getLastWindow(context).then(lastWindow => {
lastWindow.setWindowLayoutFullScreen(true)
})
然后 UI 写成:
Column() {
Text(this.pageTitle) // 顶部标题区 10%
.width('100%').height('10%')
Web(...)
.height('90%')
.padding({ top: '123px', bottom: '91px' }) // ← 手工硬写px
.expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.TOP,SafeAreaEdge.BOTTOM])
}
这里有两个工程风险你要知道:
坑1:标题写进10%高的区域,但pageTitle为空时——那10%区不是"空着",而是显示默认/URL
修法就是上面那套:sanitizeTitle+ 默认文案。同时那个 height('10%')建议改成固定 56vp(导航条高度),别用百分比——因为 setWindowLayoutFullScreen后,"%"算的是真全屏高,状态栏区被吃进去,百分比会漂移。
坑2:padding({ top: '123px', bottom: '91px' })硬写px必须跟着设备算
正确姿势是拿状态栏/导航条高度做计算:
// 用 getWindowAvoidArea 拿真实避让尺寸,而不是写死 123/91
window.getLastWindow(ctx).then(w => {
w.setWindowLayoutFullScreen(true)
const avoid = w.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
const topInset = avoid.topRect.height // px
// 你再 vp2px / px2vp 换算,或干脆用 expandSafeArea + 内部 padding 更稳
})
但更推荐的现代写法:别手写这些px,而是用 expandSafeArea+ 内部布局分层:
// 导航栏(自己画,固定高)
Row(){ Text(this.pageTitle)... }
.width('100%')
.height(56)
.padding({ top: 顶部避让 }) // 这里吃 safeArea
.backgroundColor('#FFF')
.zIndex(2)
// Web
Web(...)
.layoutWeight(1)
.expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])
// 顶部不expand:让导航栏压在Web上面(导航栏 zIndex更高)
这样标题区永远在你的组件树里受控,Web里URL再漂也漂不到你标题文字上。
五、运营侧的检查清单(你直接贴给运营/前端)
|
检查项 |
对/错 |
备注 |
|---|---|---|
|
|
✅ |
"夏日焕新季 - 低至5折" |
|
标题别带 |
✅ |
URL泄漏风险 |
|
初始HTML就有title(不只靠JS设) |
✅ |
防首帧URL闪 |
|
标题长度 |
≤32字符可读 |
顶部栏空间有限 |
|
og:title 同步 |
可选 |
分享回流更稳 |
六、总结
页面顶部显示网址,不是 ArkUI 的神秘bug,而是H5标题缺席时,Web容器的合法回退行为。根治链路就三步:
-
H5:给
<title>写人类可读文案,别空、别写URL、别只留JS动态设 -
ArkUI:用
onTitleReceive接管标题,过清洗函数(URL-like → 兜底文案)再显示 -
沉浸式全屏时:标题放你自己画的导航条里(固定高度+zIndex),别让Web的回退标题有机可乘
做到这三条,你的商品详情/H5活动页的标题就永远不会变成一行裸URL——不管H5是SSR、SPA、还是某个运营临时拼的落地页。
更多推荐


所有评论(0)