鸿蒙 Tab 中的 WebView 如何优雅地拦截侧滑返回?

上午的阳光
发布于 2025-10-10 10:48
浏览
0收藏

今天正好要实现一个功能:对于放在 Tab 组件中的 WebView,我希望使用系统的侧滑返回手势时作用的是通过 Web 组件打开的网页而不是作用于应用导致应用直接退出。

搜索了下感觉不难,但是其中也遇到了一些麻烦耗费了不少时间,遂想和大家分享下经验。


核心武器:onBackPress 回调

什么是 onBackPress?

它是一个 HarmonyOS Ability 级别 的系统回调,专门用于拦截"返回"行为

那么知道这个就好办了,我们只需要在 Web 组件页中添加该回调并在其中编写对应的页面返回逻辑不就完事了么?

import { webview } from '@kit.ArkWeb';

@Component
struct DoubinTab {
  private webCtrl: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({
        src: 'https://doubao.com',  
        controller: this.webCtrl
      })
    }
    .expandSafeArea()
  }

  /**
   * 拦截系统返回键/侧滑手势
   * 返回 true  → 事件已消费,系统不再 finish
   * 返回 false → 网页已退无可退,交给系统默认处理
   */
  onBackPress(): boolean | void {
    if (this.webCtrl.accessStep(-1)) {   // 还能后退?
      this.webCtrl.backward();           // 网页回退
      return true;                       // 吃掉返回事件
    }
    return false;                        // 退无可退,正常退出
  }
}

第一个坑:onBackPress 根本没触发

但正当我愉快地构建并进行测试时,却发现丝毫没有生效。难道是我 onBackPress 的逻辑有问题?

经过几番挣扎和调试我才知道大方向都错了,onBackPress 根本没有被触发

原来 onBackPress 需要放在页面的根组件下才能拦截到系统手势

于是我赶紧把 onBackPress 放到 entry 中,但 onBackPress 涉及到 webController 的使用,于是我又调整了下 Web 页面的 webController ,改为由 entry 中传入:

import { Log, TabModel, getAuthPreferences } from 'basic'
import { SAFE_AREA_BOTTOM, SAFE_AREA_TOP, QuickLoginButtonComponent } from "basic";
import { Index as HomePage } from 'home';
import { AIWeb } from 'aiweb'
import { emitter } from '@kit.BasicServicesKit';
import { preferences } from '@kit.ArkData';

@Builder
function EntryBuilder() {
  NavDestination() {
    Index()
  }
  .hideTitleBar(true) // 取消自带的 bar
}

@Entry
@Component
struct Index {
  @State currentIndex: number = 0
  @State showTabBar: boolean = true
  tabsController: TabsController = new TabsController()
  controller: webview.WebviewController = new webview.WebviewController();

  onBackPress(): boolean | void {
    if (this.webCtrl.accessStep(-1)) {   // 还能后退?
      this.webCtrl.backward();           // 网页回退
      return true;                       // 吃掉返回事件
    }
    return false;                        // 退无可退,正常退出
  }

  tabList: TabModel[] = [
    {
      text: '首页',
      src: $r("app.media.icon_home"),
      acSrc: $r("app.media.icon_home_active")
    },
    {
      text: 'web',
      src: $r("app.media.icon_ai"),
      acSrc: $r("app.media.icon_ai_active")
    },
    {
      text: '设置',
      src: $r("app.media.icon_setting"),
      acSrc: $r("app.media.icon_setting_active")
    }
  ]

  build() {
    Navigation() {
      Column() {
        Tabs({ controller: this.tabsController }) {
          ForEach(this.tabList, (_: TabModel, index) => {
            TabContent() {
              if (index == 0) {
                HomePage()
              } else if (index == 1) {
                AIWeb({ controller: this.controller })
              } else {
                Text("设置")
              }
            }
          })
        }
        .onChange((index) => {
          // 统一在 onChange 中更新 currentIndex,保证判断使用的是切换前的旧值
          this.currentIndex = index
          // 切换 Tab 时强制显示底部导航栏(以防未触发 blur 时一直隐藏)
          this.showTabBar = true
        })
        .barHeight(0)
        .width('100%')
        .layoutWeight(1)

        // 底部 Tab 栏
        if (this.showTabBar) {
          Row() {
            ForEach(this.tabList, (item: TabModel, index) => {
              this.barBuilder((() => {
                item.index = index
                return item
              })())
            })
          }
          .justifyContent(FlexAlign.SpaceAround)
          .width('100%')
          .height(this.tabBarHeight + this.safeAreaBottom)
          .padding({ 
            bottom: this.safeAreaBottom,
            top: 12,
            left: 0,
            right: 0
          })
          .backgroundColor($r('app.color.tab_bar_background'))
          .shadow({ 
            offsetY: -1,
            radius: 12, 
            color: $r('app.color.tab_bar_shadow'),
            fill: true
          })
        }
      }
      .width('100%')
      .height('100%')
    }
    .mode(NavigationMode.Stack)
    .hideToolBar(true)
  }

  // ......
}

第二个坑:WebviewController 必须与 Web 组件同时创建

但是好家伙还是不行,这下我又纳闷了。仔细分析一下,其实上面犯了一个关键错误:

WebviewController 必须与 Web 组件同时创建,禁止在组件外部提前 new 后传递使用。

提前 new 好再传进来,会错过 Web 内核的初始化时机,导致 history 栈识别错乱。

为什么会这样?

发生过程:

  1. Web 组件在首次 build 时才会向 C++ 层申请真正的 Webview 实例
  2. 申请过程中会把「当前壳层控制器」绑定到内核
  3. 如果你在 entry 里提前​​new WebviewController()​​,它内部只是一个空壳,还没和内核句柄挂钩
  4. 当你把这个空壳传给 Web 组件时,Web 会重新生成一个新的真实句柄并把自己绑上去,但原来的壳层引用不会自动更新

我的最终解决方案:事件委派

所以正确的姿势还得是在 AIWeb 页面组件中创建 webController 来使用,但 onBackPress 又需要写在 entry 中,onBackPress 又使用到了 WebviewController,这可如何是好?

这下我们得转化下思路,既然 onBackPress 得写在 entry 中,那我们保持不变,但是偷换其内核,通过触发事件并让 AIWeb 组件中监听该事件执行真实的 ​​this.controller.backward()​​ 操作

Entry 层:委派返回事件

​product/entry/src/main/ets/pages/Index.ets​

onBackPress(): boolean | void {
  // 将返回事件委派给当前 Tab 的具体实现(例如 AIWeb)
  try {
    Log.info('Entry: 进入回退判断 (委派)');
    if (this.currentIndex === 2) { // AIWeb Tab
      try { 
        emitter.emit('aiweb:back'); 
      } catch(_) {}
      return true; // 吞掉返回事件,避免应用退出
    }
  } catch(_) {}
  return false; // 其他页按默认行为处理
}

AIWeb 组件:监听并执行真实回退

​feature/AIWeb/src/main/ets/pages/Index.ets​

private handleBack() {
  if (this.webCtrl.accessStep(-1)) {   // 还能后退?
    this.webCtrl.backward();           // 网页回退
    return true;                       // 吃掉返回事件
  }
  return false;                        // 退无可退,正常退出
}

aboutToAppear() {
  try {
    emitter.on('aiweb:back', () => {
      this.handleBack();
    });
    this.backHandlerBound = true;
  } catch(error) { 
    Log.error(error);     
  }
}

总结

这样我们就可以成功实现目标功能啦!

核心思路:

  • ✅ onBackPress 必须放在根组件(Entry)
  • ✅ WebviewController 必须在 Web 组件内创建
  • ✅ 通过事件机制(emitter)将返回行为委派给实际的 Web 组件处理

如果你也遇到相似的问题,希望这次分享对你有帮助吧。




文章来源:​​https://developer.huawei.com/consumer/cn/blog/topic/03195469630150081​

分类
标签
收藏
回复
举报
回复
    相关推荐