在这里插入图片描述

1 -> 概述

在鸿蒙应用开发中,WebView 组件(ArkWeb)一直是承载网页内容呈现的核心能力模块。无论是混合开发场景下的 H5 页面嵌入,还是纯网页内容的加载展示,滚动行为都是用户最频繁的交互动作之一。然而,长期以来,Web 组件的滚动位置获取一直依赖 JavaScript 注入方案——通过 runJavaScript 执行 window.pageYOffsetdocument.documentElement.scrollTop 等脚本,将结果异步返回给 ArkTS 层。这种方式虽然可行,但存在显而易见的短板:异步回调导致时机难以精准把控、脚本执行上下文依赖页面完全加载、以及不同网页 DOM 结构的差异性带来的稳定性隐患。

鸿蒙 6.0.0(API 20)Beta3 的发布,为 ArkWeb 带来了一系列重要更新,其中“新增支持获取网页滚动偏移量”这一能力尤为引人注目[reference:0]。与此同时,该版本还新增了嵌套滚动过程中的快速调度策略支持,允许渲染进程跳过 vsync 调度直接触发绘制,以及 Web 组件手势焦点模式、私有网络访问开关等一系列增强[reference:1]。本文将聚焦于 getPageOffset20 这一新增 API,从技术背景、接口详解、应用场景到代码实践,进行系统性的深入解读。

2 -> 为什么要新增 getPageOffset20?—— 痛点驱动下的能力演进

2.1 -> 传统方案的局限

getPageOffset20 出现之前,鸿蒙开发者获取 Web 页面滚动位置的主流做法是:

// 传统方案:通过 JS 注入获取滚动位置
this.webController.runJavaScript('window.pageYOffset')
    .then((offset: number) => {
        console.log('当前滚动位置:' + offset);
        // 执行后续业务逻辑
    })
    .catch((error: BusinessError) => {
        console.error('获取滚动位置失败:' + error.message);
    });

这一方案虽然在多数场景下能够工作,但存在几个难以回避的问题:

  • 异步响应机制runJavaScript 本质上是异步的,获取滚动位置的时机往往无法与用户的滚动操作实时同步,在需要高频采样的场景(如阅读进度条、滚动联动动画)中表现吃力。

  • 跨语言调用开销:每次获取滚动位置都需要经过 ArkTS → 底层 Web 内核 → JavaScript 引擎 → 执行脚本 → 返回值回传的完整链路,调用链路过长。

  • 页面状态依赖:如果页面尚未完全加载,或页面 DOM 结构发生变化(如 SPA 应用路由切换),注入的 JS 脚本可能无法正确获取到滚动容器。

  • 类型安全性缺失runJavaScript 返回的是 Promise<Object>,类型信息不够明确,开发者需要自行处理类型断言和边界情况。

2.2 -> 滚动位置获取的核心价值

获取网页滚动位置并非一个可有可无的功能,而是诸多应用场景的技术基石:

场景类别 典型用例 技术要求
状态保存与恢复 页面销毁后重建时回到用户上次阅读位置 精确记录滚动偏移量
阅读进度跟踪 长篇文章阅读进度条、章节完成度统计 高频、低延迟的位置获取
跨设备接续 手机切平板时保持同一浏览进度 滚动位置的跨设备同步
滚动联动 Web 内容与原生组件(如悬浮按钮、侧边目录)的协同响应 实时响应滚动事件
数据埋点 分析用户滚动深度、曝光区域统计 可靠的采样数据来源

正是基于这些实际需求,鸿蒙团队在 ArkWeb 中新增了 getPageOffset20 这一原生 API,为开发者提供了更为高效、可靠、精准的滚动偏移量获取方式。

3 -> API 详解:getPageOffset20

3.1 -> 接口定义

getPageOffset20WebviewController 中新增的方法,用于同步获取当前网页的滚动偏移量。该接口从 API 20 开始支持,对应的鸿蒙版本为 6.0.0 Beta3 及以上。

/**
 * 获取当前网页的滚动偏移量。
 * @returns {PageOffset} 包含水平滚动偏移量和垂直滚动偏移量的对象
 * @since 20
 */
getPageOffset20(): PageOffset;

3.2 -> 返回值类型:PageOffset

PageOffset 是一个简单的对象类型,包含两个属性:

interface PageOffset {
    /**
     * 水平滚动偏移量,单位为像素(px)
     */
    x: number;
    
    /**
     * 垂直滚动偏移量,单位为像素(px)
     */
    y: number;
}

3.3 -> 与 runJavaScript 方案的核心差异

对比维度 getPageOffset20 runJavaScript 注入方案
调用方式 同步,直接返回 异步,返回 Promise
调用开销 极低,原生 API 直接获取 较高,需跨语言、跨引擎调用
实时性 可实时响应滚动事件 存在异步延迟
类型安全 强类型,TypeScript 原生支持 需自行处理类型断言
页面依赖性 不依赖页面 DOM 状态 依赖页面完全加载及滚动容器可访问
可靠性 高,内核层直接获取 中,受页面 JS 执行环境影响

需要特别说明的是,同步返回并不意味着该方法会阻塞 UI 线程。getPageOffset20 是从 Web 内核维护的滚动状态中直接读取当前值,不涉及任何跨进程或跨语言的复杂操作,因此调用效率极高。

3.4 -> 注意事项

  • 调用时机:建议在页面加载完成(如 onPageEnd 回调)后调用,此时滚动位置才有实际意义。
  • Web 组件滚动前提:与任何滚动能力一样,Web 页面能滚动的前提是内容尺寸超过 Web 组件的可视区域[reference:2]。如果页面本身不可滚动,getPageOffset20 返回的 x 和 y 均为 0。
  • 嵌套滚动场景:在 Web 组件嵌套于 Scroll 等父容器的场景中,getPageOffset20 返回的是 Web 组件内部内容的滚动偏移量,而非父容器的滚动位置。

4 -> 实战演练:从基础用法到复杂场景

4.1 -> 基础用法示例

以下是一个完整的 Web 组件使用示例,展示如何在页面滚动时实时获取偏移量:

// WebPage.ets
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebPage {
    controller: webview.WebviewController = new webview.WebviewController();
    @State scrollX: number = 0;
    @State scrollY: number = 0;
    private scrollTimer: number | undefined = undefined;

    build() {
        Column() {
            // 状态显示栏
            Row() {
                Text(`滚动偏移: X=${this.scrollX}, Y=${this.scrollY}`)
                    .fontSize(14)
                    .fontColor('#666666')
                    .padding(12)
            }
            .width('100%')
            .backgroundColor('#F5F5F5')
            
            // Web 组件
            Web({ 
                src: 'https://developer.huawei.com/consumer/cn/', 
                controller: this.controller 
            })
            .javaScriptAccess(true)
            .domStorageAccess(true)
            .onPageEnd(() => {
                console.log('页面加载完成');
                // 页面加载完成后,可以获取初始滚动位置(通常为 0)
                const offset = this.controller.getPageOffset20();
                console.log(`初始滚动位置: x=${offset.x}, y=${offset.y}`);
            })
            .onScroll((event) => {
                // 方案一:使用 Web 组件的 onScroll 事件
                // onScroll 提供的是本次滚动的增量,而非绝对位置
                // 若需要绝对位置,仍需使用 getPageOffset20
                
                // 使用防抖优化高频调用
                if (this.scrollTimer) {
                    clearTimeout(this.scrollTimer);
                }
                this.scrollTimer = setTimeout(() => {
                    const currentOffset = this.controller.getPageOffset20();
                    this.scrollX = currentOffset.x;
                    this.scrollY = currentOffset.y;
                }, 16); // 约 60fps 的采样频率
            })
        }
    }
}

4.2 -> 场景一:页面滚动位置的保存与恢复

在 Web 页面中,保存和恢复滚动位置是最常见的需求之一。例如,用户浏览长文章时切换到其他页面,再返回时应该停留在之前阅读的位置。getPageOffset20 的引入使得这一能力的实现更加简洁可靠。

// 使用 Preferences 持久化存储滚动位置
import { preferences } from '@kit.ArkData';

const STORAGE_KEY = 'web_scroll_position';

@Entry
@Component
struct ArticleReaderPage {
    controller: webview.WebviewController = new webview.WebviewController();
    private prefs: preferences.Preferences | null = null;
    private scrollRestored: boolean = false;

    aboutToAppear() {
        // 初始化 Preferences
        preferences.getPreferences(this.context, 'user_prefs')
            .then(pref => {
                this.prefs = pref;
            })
            .catch(err => {
                console.error(`Preferences 初始化失败: ${err.message}`);
            });
    }

    aboutToDisappear() {
        // 页面销毁时保存当前滚动位置
        if (this.controller.accessWebContent()) {
            const offset = this.controller.getPageOffset20();
            this.prefs?.putSync(STORAGE_KEY, offset.y);
            this.prefs?.flush();
            console.log(`滚动位置已保存: y=${offset.y}`);
        }
    }

    build() {
        Stack() {
            Web({ 
                src: 'https://example.com/long-article', 
                controller: this.controller 
            })
            .javaScriptAccess(true)
            .onPageEnd(() => {
                // 页面加载完成后恢复滚动位置
                if (!this.scrollRestored && this.prefs) {
                    const savedY = this.prefs.getSync(STORAGE_KEY, 0) as number;
                    if (savedY > 0) {
                        // 使用 scrollTo 恢复位置
                        this.controller.scrollTo(savedY);
                        console.log(`滚动位置已恢复: y=${savedY}`);
                    }
                    this.scrollRestored = true;
                }
            })
            
            // 回到顶部按钮
            if (this.scrollY > 500) {
                Button('↑')
                    .width(48)
                    .height(48)
                    .borderRadius(24)
                    .backgroundColor('#007DFF')
                    .position({ x: '90%', y: '85%' })
                    .onClick(() => {
                        this.controller.scrollTo(0);
                    })
            }
        }
    }
}

需要补充说明的是,在鸿蒙 Next 中,WebView 组件虽然默认支持多页面历史栈,但页面状态(如滚动位置)的恢复确实需要开发者主动管理[reference:3]。getPageOffset20 为这一主动管理提供了可靠的数据基础。

4.3 -> 场景二:阅读进度指示器

对于长文章类应用,阅读进度条是一个经典的 UI 元素。利用 getPageOffset20 结合网页总高度,可以精确计算用户的阅读进度。

@Entry
@Component
struct ReadingProgressPage {
    controller: webview.WebviewController = new webview.WebviewController();
    @State progressPercent: number = 0;
    private pageTotalHeight: number = 0;
    private scrollTimer: number | undefined = undefined;

    // 获取网页总高度
    private async getPageTotalHeight(): Promise<number> {
        try {
            const height = await this.controller.runJavaScript(
                'document.documentElement.scrollHeight'
            );
            return typeof height === 'number' ? height : 0;
        } catch (err) {
            console.error('获取页面高度失败:', err);
            return 0;
        }
    }

    // 更新阅读进度
    private updateProgress() {
        const offset = this.controller.getPageOffset20();
        if (this.pageTotalHeight > 0 && offset.y >= 0) {
            // 计算进度百分比
            let progress = (offset.y / (this.pageTotalHeight - this.controller.getHeight())) * 100;
            progress = Math.min(100, Math.max(0, progress));
            this.progressPercent = progress;
        }
    }

    build() {
        Stack({ alignContent: Alignment.Top }) {
            Web({ 
                src: 'https://example.com/article', 
                controller: this.controller 
            })
            .javaScriptAccess(true)
            .onPageEnd(() => {
                // 页面加载完成后获取总高度
                this.getPageTotalHeight().then(height => {
                    this.pageTotalHeight = height;
                    console.log(`网页总高度: ${height}px`);
                });
            })
            .onScroll(() => {
                // 滚动时实时更新进度(使用防抖优化)
                if (this.scrollTimer) {
                    clearTimeout(this.scrollTimer);
                }
                this.scrollTimer = setTimeout(() => {
                    this.updateProgress();
                }, 16);
            })
            
            // 顶部进度条
            Column()
                .width(`${this.progressPercent}%`)
                .height(3)
                .backgroundColor('#007DFF')
                .position({ x: 0, y: 0 })
        }
    }
}

4.4 -> 场景三:嵌套滚动中的偏移量统一派发

在复杂 UI 布局中,Web 组件常常嵌套在 Scroll、List 等原生滚动容器内。getPageOffset20 结合 getPageHeight 等接口,可以实现精准的滚动偏移量统一派发[reference:4]。

@Entry
@Component
struct NestedScrollPage {
    webController: webview.WebviewController = new webview.WebviewController();
    private parentScroller: Scroller = new Scroller();
    @State webHeight: number = 0;
    
    build() {
        Scroll(this.parentScroller) {
            Column() {
                // 文章头部原生区域
                Column() {
                    Text('文章标题')
                        .fontSize(24)
                        .fontWeight(FontWeight.Bold)
                        .margin({ bottom: 12 })
                    Text('作者:XXX | 发布时间:2025-01-01')
                        .fontSize(14)
                        .fontColor('#999999')
                }
                .padding(16)
                .width('100%')
                
                // Web 内容区域
                Web({ 
                    src: 'https://example.com/content', 
                    controller: this.webController 
                })
                .height(this.webHeight)
                .nestedScroll({
                    scrollUp: NestedScrollMode.PARENT_FIRST,
                    scrollDown: NestedScrollMode.SELF_FIRST
                })
                .onPageEnd(() => {
                    // 获取网页实际高度,用于自适应展开
                    this.webController.getPageHeight().then(height => {
                        this.webHeight = height;
                    });
                })
                
                // 评论区原生区域
                Column() {
                    Text('精彩评论')
                        .fontSize(18)
                        .fontWeight(FontWeight.Medium)
                        .margin({ bottom: 12 })
                    // 评论区内容...
                }
                .padding(16)
                .width('100%')
                .backgroundColor('#F8F8F8')
            }
        }
        .scrollBar(BarState.Auto)
    }
}

4.5 -> 场景四:跨设备应用接续中的滚动位置同步

在鸿蒙分布式能力中,“应用接续”(Continuation)允许用户在一台设备上浏览到某个位置后,无缝切换到另一台设备继续浏览。getPageOffset20 为 Web 浏览的接续场景提供了精确的位置捕获能力。

核心思路是:在源设备上调用 getPageOffset20 获取滚动位置,通过分布式数据对象同步到目标设备,目标设备页面加载完成后通过 scrollTo 恢复位置[reference:5]。

// 源设备端:保存滚动位置
onContinue(wantParam: Want): AbilityConstant.OnContinueResult {
    const offset = this.webController.getPageOffset20();
    const dataObject = distributedDataObject.create(this.context, { 
        scrollY: offset.y,
        scrollX: offset.x,
        currentUrl: this.webController.getUrl()
    });
    // ... 分布式同步逻辑
    return AbilityConstant.OnContinueResult.AGREE;
}

// 目标设备端:恢复滚动位置
continueRestore(want: Want) {
    // 获取同步过来的滚动位置数据
    const savedOffsetY = dataObject['scrollY'];
    if (savedOffsetY > 0) {
        this.webController.onPageEnd(() => {
            this.webController.scrollTo(savedOffsetY);
        });
    }
}

5 -> getPageOffset20 带来的全新可能性

5.1 -> 更流畅的滚动联动体验

getPageOffset20 出现之前,实现 Web 内容与原生 UI 的滚动联动(如悬浮按钮的显示/隐藏、侧边目录高亮跟随)往往需要借助 onScroll 事件的增量计算,逻辑复杂且容易出错。现在可以直接获取绝对滚动位置,使得联动逻辑的实现变得直观且可靠。

5.2 -> 更精准的数据埋点

对于需要分析用户滚动行为的数据采集场景(如广告曝光检测、内容到达率统计),getPageOffset20 提供了精确的位置数据,且调用成本远低于 JS 注入方案,支持更高频率的采样。

5.3 -> 更简洁的状态管理代码

无论是页面内状态管理还是全局状态持久化,getPageOffset20 的同步特性使得代码逻辑更为线性,减少了异步回调带来的心智负担。

6 -> 总结

getPageOffset20 的引入,标志着鸿蒙 ArkWeb 在 Web 交互能力上的又一次重要演进。从更宏观的视角来看,这一更新体现了鸿蒙系统在 Web 容器能力构建上的清晰思路:

首先,能力完备性。鸿蒙 6.0 为 ArkWeb 新增了获取网页滚动偏移量、嵌套滚动快速调度策略、Web 组件手势焦点模式、私有网络访问开关、自定义错误页面、组件销毁模式等多项能力[reference:6]。getPageOffset20 作为其中的关键一环,填补了滚动偏移量获取在原生 API 层面的空白,使 ArkWeb 的滚动能力体系更加完整。

其次,开发体验优化。从依赖 JS 注入的异步方案,到原生同步 API,getPageOffset20 带来的不仅是性能上的提升,更是开发范式上的简化。开发者不再需要关心 JS 执行时机、DOM 结构稳定性、跨语言调用开销等问题,只需一行代码即可完成滚动偏移量的精准获取。

再次,生态协同。滚动位置的精准获取能力,与鸿蒙分布式技术(应用接续)、持久化存储(Preferences)、UI 框架(ArkUI)形成了良好的协同效应。开发者可以将这一能力与其他系统能力有机结合,构建出体验更流畅、功能更丰富的应用。

展望未来,随着 ArkWeb 能力的持续增强,我们有理由期待更多原生 API 的推出——如滚动边界的精确检测、滚动动画的精细控制、滚动事件的丰富回调等。getPageOffset20 是一个起点,它标志着鸿蒙在 Web 容器能力建设上正在从“可用”向“好用、易用”迈进。对于广大鸿蒙应用开发者而言,及时跟进这些能力更新,将其合理地应用到实际项目中,将是提升应用品质和开发效率的重要途径。


感谢各位大佬支持!!!

互三啦!!!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐