做购物比价/商城时,商品详情页往往是一张 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容器对标题的优先级通常是:

  1. <title>内容存在且非空 → 用 title

  2. 否则 → 回退到其他可显示信息(很多实现里就是 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>

三句话守则这个文件:

  1. <title>别空着,也别只写 " "空格(很多容器仍当空处理)

  2. 标题尽量短而有辨识度(顶部导航区空间有限)

  3. 动态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就不会有问题了吧?"

现实里仍有两条漏道:

  1. 加载第一帧 vs <title>解析完成的时序差:第一瞬 Web 容器可能用 URL 顶着,直到 DOM/网络标题就绪才触发 onTitleReceive——如果你不清洗,哪怕50ms,标题区也闪一下URL

  2. 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再漂也漂不到你标题文字上


五、运营侧的检查清单(你直接贴给运营/前端)

检查项

对/错

备注

<title>非空,可读

"夏日焕新季 - 低至5折"

标题别带 https:/// 原始域名

URL泄漏风险

初始HTML就有title(不只靠JS设)

防首帧URL闪

标题长度

≤32字符可读

顶部栏空间有限

og:title 同步

可选

分享回流更稳


六、总结

页面顶部显示网址,不是 ArkUI 的神秘bug,而是H5标题缺席时,Web容器的合法回退行为。根治链路就三步:

  1. H5:给 <title>写人类可读文案,别空、别写URL、别只留JS动态设

  2. ArkUI:用 onTitleReceive接管标题,过清洗函数(URL-like → 兜底文案)再显示

  3. 沉浸式全屏时:标题放你自己画的导航条里(固定高度+zIndex),别让Web的回退标题有机可乘

做到这三条,你的商品详情/H5活动页的标题就永远不会变成一行裸URL——不管H5是SSR、SPA、还是某个运营临时拼的落地页。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐