快速开始

本文介绍如何将 H5 容器组件接入到 HarmonyOS NEXT 客户端。您可以基于已有工程使用 ohpmrc 方式接入 H5 容器 SDK 到客户端。

前置条件

添加 H5 容器 SDK 之前,请您确保已经将工程接入到 mPaaS。更多信息请参见 接入 mPaaS 能力

引入依赖

在项目的.ohpmrc文件中添加如下仓库:

@mpaas:registry=https://mpaas-ohpm.oss-cn-hangzhou.aliyuncs.com/meta

添加 SDK

通过 使用 mppm 工具 安装 H5 容器组件。

image

配置权限

module.json5 中配置所需权限。

"requestPermissions":[
  {
  "name" : "ohos.permission.GET_NETWORK_INFO",
  },
  {
  "name" : "ohos.permission.INTERNET",
  }
]

使用 SDK

初始化

在 mPaaS 框架初始化完成之后,初始化 HRiver。代码如下:

HRiver.init();

使用 SDK 之前必须初始化 mPaaS 框架,设置 userId 和 appsecret,其中 appsecret 从 AppCenter 后台获取,具体查看 获取 HarmonyOS NEXT config 配置文件(beta)

代码示例如下:

import AbilityStage from '@ohos.app.ability.AbilityStage';
import { MPFramework } from '@mpaas/framework';

export default class ModuleEntry extends AbilityStage {
  async onCreate() {
    MPFramework.create(this.context);
    
    MPFramework.instance.userId = 'MPTestCase'
    MPFramework.instance.appSecret = "12a711d78980f661aca5401788fdf09a";
  }
}

打开离线包

初始化 HRiver后调用 startApp 打开离线包。

import { HRiver } from '@mpaas/hriver'

/**
* 打开离线包
* @param appId: 离线包id
* @param startParams: 启动参数,可以不传。控制TitleBar、指定页面等
*/
HRiver.startApp(appId: string, startParams?: Map<string, Object>)

代码示例如下:

import { HRiver } from '@mpaas/hriver'

// 示例1:
HRiver.startApp('20190517') // 直接启动离线包

// 示例2: 
let startParams: Map<string, Object> = new Map();
startParams.set('defaultTitle', '默认标题') //加载阶段显示默认标题
HRiver.startApp('20190517', startParams)

打开在线页面

初始化 HRiver 之后调用 startUrl 打开在线页面。

import { HRiver } from '@mpaas/hriver'

/**
* 打开在线页面
* @param url: 在线地址
* @param startParams: 启动参数。可以不传,控制TitleBar、指定页面等
*/
HRiver.startUrl(url: string, startParams: Map<string, Object>)

注册自定义 JSAPI

初始化 HRiver 之后调用 registerPlugin 注册自定义 JSAPI。

import { HRiver } from '@mpaas/hriver'

/**
* 注册自定义 JSApi 实现
* @param pluginClass: pluginClass列表,如 registerPlugin({CustomPlugin1, CustomPlugin2}, HRiver.SCOPE_PAGE)
* @param scope: 可以不传。默认HRiver.SCOPE_PAGE。HRiver.SCOPE_APP表示离线包App级别生命周期;HRiver.SCOPE_PAGE表示页面级别生命周期
*/
HRiver.registerPlugin(pluginClass: ESObject, scope: string)

代码示例:

HRiver.registerPlugin({H5CustomPlugin, H5Custom1Plugin})

plugin 实现代码示例如下:

import { HRiver, H5SimplePlugin, H5EventFilter, H5Event, H5BridgeContext } from '@mpaas/hriver'

class H5CustomPlugin extends H5SimplePlugin {
  onPrepare(filter: H5EventFilter): void {
    filter.addAction('myapi1')
  }

  handleEvent(event: H5Event, context: H5BridgeContext): Boolean {
    if ('myapi1' == event.action) {
      context.sendBridgeResult({
        success: true,
        data: 'myapi1调用成功'
      })
      return true
    }
    return super.handleEvent(event, context);
  }
}

class H5Custom1Plugin extends H5SimplePlugin {
  onPrepare(filter: H5EventFilter): void {
    filter.addAction('myapi2')
  }

  handleEvent(event: H5Event, context: H5BridgeContext): Boolean {
    if ('myapi2' == event.action) {
      context.sendBridgeResult({
        success: true,
        data: 'myapi2调用成功'
      })
      return true
    }
    return super.handleEvent(event, context);
  }
}

Native 调用 H5

Native 调用 H5 有以下两种方法。

  • 在自定义 JSAPI 中通过 H5BridgeContext.sendToWeb 方法调用 H5。

    import { H5BridgeContext, H5Event, H5EventFilter, H5SimplePlugin, HRiver } from '@mpaas/hriver';
    class H5CustomPlugin extends H5SimplePlugin {
      handleEvent(event: H5Event, context: H5BridgeContext): Boolean {
        // native 调用 h5
        context.sendToWeb('customCallWeb', {
            data: 'abc'
        })
    
        ... // 其他代码
      }
    }
  • 在 Native 代码中获取 TopAppactivityPage,获得最新的页面,通过页面调用 sendToWeb 方法。

    import { HRiver,
      XRiverProxy,
      getProxy,
      AppManager,
      AppNode,
      Page
    } from '@mpaas/hriver';
    {
      let appManager = getProxy(XRiverProxy.AppManager) as AppManager
      let appNode: AppNode | null = appManager.findTopApp()
      if (appNode != null) {
        let page: Page | null = appNode.getActivePage()
        if (page != null) {
          page.sendToWeb('testAction', {data: ''})
        }
      }
    }

加载内置离线包

加载所有内置离线包,包括内置的公共离线包。

初始化 HRiver 之后调用 loadOfflineResource 加载内置离线包。

import { HRiver } from '@mpaas/hriver'

/**
* 加载内置离线包
* @param jsonFileName: 内置离线包的 h5_json.json 的文件名,放到rawfile目录中。如:h5_json.json。
* @param callback: 格式 (result: string) => {}。内置离线包加载完成回调的 Function
*/
HRiver.loadOfflineResource(jsonFileName: string, callback: Function)

代码示例如下:

  1. entry/src/main/resources/rawfile 下添加 h5_json.json(文件从 Appcenter 后台下载即可)。

    {
       "config":{
          "updateReqRate":16400,
          "limitReqRate":13600,
          "appPoolLimit":3,
          "versionRefreshRate":86400
       },
       "data":[
          {
             "app_desc":"离线包1",
             "app_id":"20180910",
             "auto_install":1,
             "fallback_base_url":"https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/570DA89281533-default/20180910/1.0.0.3_all/nebula/fallback/",
             "global_pack_url":"",
             "icon_url":"",
             "installType":1,
             "main_url":"/www/index.html",
             "name":"离线包1",
             "online":1,
             "package_url":"https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/570DA89281533-default/20180910/1.0.0.3_all/nebula/20180910_1.0.0.3.amr",
             "patch":"",
             "sub_url":"",
             "version":"1.0.0.3",
             "vhost":"https://20180910.h5app.com"
          },
            {
                "app_desc":"离线包2",
                "app_id":"20190517",
                "auto_install":1,
                "fallback_base_url":"https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/570DA89281533-default/20190517/1.0.0.0_all/nebula/fallback/",
                "global_pack_url":"",
                "icon_url":"",
                "installType":1,
                "main_url":"/www/index.html",
                "name":"离线包2",
                "online":1,
                "package_url":"https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/570DA89281533-default/20190517/1.0.0.0_all/nebula/20190517_1.0.0.0.amr",
                "patch":"",
                "sub_url":"",
                "version":"1.0.0.0",
                "vhost":"https://20190517.h5app.com"
            }
       ],
       "resultCode":100,
       "resultMsg":"操作成功",
       "state":"success"
    }
  2. 下载 amr 并命名为 ${appid}_${version}.amr

    image

  3. 调用 API。

    import { HRiver } from '@mpaas/hriver'
    
    HRiver.loadOfflineResource('h5_json.json', (result: string) => {
      this.resultText = this.resultText + `\n${result} 预加载成功`
    })

更新离线包

import { HRiver } from '@mpaas/hriver'

/**
 * 批量更新离线包
 * @param appIds: 需要更新离线包的appId列表
 * @param updateCallback: 格式必须 (result: boolean, code: number) => {}。更新接口返回后回调的Function
 */
HRiver.updateApp(appIds?: Array<string>, updateCallback?: Function)

/**
 * 更新所有离线包。
 * @param updateCallback: 格式必须 (result: boolean, code: number) => {}。更新接口返回后回调的Function
 */
HRiver.updateAll(updateCallback: Function)

/**
 * 批量更新离线包
 * @param appIds: 离线包的appId和version的map
 * @param updateCallback: 格式必须 (result: boolean, code: number) => {}。更新接口返回后回调的Function
 */
HRiver.updateAppWithVersion(appIds?: Map<string, string>, updateCallback?: Function)

代码示例如下:

import { HRiver } from '@mpaas/hriver'

HRiver.updateApp(['90000002'], (result: boolean, code: number) => {
  this.resultText = `90000002更新结果: ${result}`
})

配置公共离线包

  1. 设置 H5CommonAppProvider

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * 配置公共离线包 Provider,必须在 HRiver.init() 之前调用
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    说明

    setProvider 必须在 HRiver.init() 之前调用。

  2. 实现 H5AppCommonProviderImpl.etsgetCommonResourceAppList 方法。

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
      getCommonResourceAppList(): Array<string> {
        return ['20220719'] // 返回公共离线包id列表
      }
    }

支持 fallback 逻辑

保持和 iOS/Android 一致,离线包没下载成功情况下优先打开 fallback 在线地址。

该功能默认关闭,可以通过以下开关打开:

实现 H5CommonAppProvider 的 configJSON 方法,增加 enableFallback 参数。

export class H5AppCommonProviderImpl extends H5CommonAppProvider {
  ... // 其他配置

  // configJson,配置更新频率等
  configJSON(): string {
    return JSON.stringify({
      enableFallback: 'YES',
      xxx // 其他配置
    })
  }
}

设置 UserAgent

import { HRiver } from '@mpaas/hriver'

HRiver.setUserAgent(userAgent)
说明

setUserAgentHRiver.init() 之后调用,会在默认 UserAgent 之后拼接设置的 useragent

设置离线包默认更新频率

  1. 设置 H5CommonAppProvider

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * 配置公共离线包Provider、离线包更新频率等功能,必须在HRiver.init()之前调用
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    说明

    setProvider 必须在 HRiver.init() 之前调用。

  2. 实现 H5CommonAppProviderconfigJSON 方法,代码示例如下。

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
      ... // 其他配置
    
      // configJson,配置更新频率等
      configJSON(): string {
        return JSON.stringify({
          h5_nbmngconfig: "{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}"
        })
      }
    }

    具体参数如下:

    h5_nbmngconfig: "{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1800\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}"

    其中的 ur: 1800 表示更新频率为 1800 秒,使用时修改 ur 的值即可。

配置离线包签名校验

  1. 设置 H5CommonAppProvider

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * 配置公共离线包Provider、离线包更新频率、签名校验等功能,必须在HRiver.init()之前调用
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    说明

    setProvider 必须在 HRiver.init() 之前调用。

  2. 实现 H5CommonAppProvidershouldVerifypubKey 方法,代码示例如下:

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
     ... // 其他配置 
    
     /**
     * 是否开启离线包校验,默认关闭
     * return: true表示开启,false表示关闭
     */
     shouldVerify(): boolean {
     return false
     }
    
     // 离线包校验的公钥,如果shouldVerify为false 则无需设置,否则必须设置公钥
     pubKey(): string {
     return ''
     }
    }

开启调试模式

HRiver.enableDebug(true)

监听页面生命周期

初始化完成后设置 provider。

import {H5PageLifeCycleProvider} from '@mpaas/hriver';
  
HRiver.setProvider(H5PageLifeCycleProvider.name, new H5PageLifeCycle())

H5PageLifeCycle

import { H5PageLifeCycleProvider, Page } from '@mpaas/hriver';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class H5PageLifeCycle extends H5PageLifeCycleProvider {
  onPageShow(routerName: string, page?: Page | undefined): void {
    super.onPageShow(routerName, page);
    hilog.debug(1, 'H5PageLifeCycle', "pageshow: " + page?.pageUrl)
  }

  onPageHide(routerName: string, page?: Page | undefined): void {
    super.onPageHide(routerName, page);
    hilog.debug(1, 'H5PageLifeCycle', "onPageHide: " + page?.pageUrl)
  }

  onPageCreate(page?: Page | undefined): void {
    super.onPageCreate(page);
    hilog.debug(1, 'H5PageLifeCycle', "onPageCreate: " + page?.pageUrl)
  }

  onPageExit(page?: Page | undefined): void {
    super.onPageExit(page);
    hilog.debug(1, 'H5PageLifeCycle', "onPageExit: " + page?.pageUrl)
  }

  onBackPress(page?: Page | undefined): boolean {
    hilog.debug(1, 'H5PageLifeCycle', "onBackPress: " + page?.pageUrl)
    return super.onBackPress(page);

  }
}

支持注销 Plugin

通过 Page 的 Page.ets 实现注销。

// 根据action注销
unregisterPluginByAction(action: string);

// 根据pluginName注销
unregisterPluginByPluginName(name: string);

示例如下:

image

支持页面嵌入模式

页面嵌入模式基于 Navigation 实现。

  1. 创建离线包需要的 NavPathStack。

    pageInfos: NavPathStack = new NavPathStack()
  2. 在页面需要嵌入的位置添加空白组件,以下为示例:

    重要

    mode 必须使用 NavigationMode.Stack,否则横竖屏/折叠屏切换有问题。

    Navigation(this.pageInfos) {
              Column() {
                // 空白页面用于嵌入离线包页面, 不用填任何内容
              }.width('100%') // 宽度根据需要
              .backgroundColor(Color.Red) // 只是示例
              .height(500)  // 高度根据实际
            }.navDestination(this.PagesMap)
            .mode(NavigationMode.Stack)
    
    
    // pagesMap实现:
    import {HRBuilder, RouterUtils, HRiver, H5RouterNavStackProvider} from '@mpaas/hriver'
    
    let mPaaSHRiverBuilder: WrappedBuilder<[string, ESObject]> = wrapBuilder(HRBuilder);
    
    @Builder
    PagesMap(name: string, params: ESObject) {
      if (RouterUtils.isMPHRiverPage(name)) {
        mPaaSHRiverBuilder.builder(name, params)
      } else if (name == 'xxx') {
        // 其他业务的页面
      }
    }
  3. 启动离线包和在线页面时,需要添加第三个参数并传入步骤 1 中创建的 NavPathStack,增加 embedPage 参数用来表示内嵌页面。

    let param: Map<string, Object> = new Map<string, Object>()
    param.set('embedPage', 'YES')
    HRiver.startUrl('https://www.baidu.com', param, this.pageInfos)
    // HRiver.startApp('90000000', param, this.pageInfos)
  4. 添加返回事件拦截。

    HRiver.setProvider(H5PageLifeCycleProvider.name, new H5PageLifeCycle())

    import { H5PageLifeCycleProvider, Page } from '@mpaas/hriver';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { router } from '@kit.ArkUI';
    
    export class H5PageLifeCycle extends H5PageLifeCycleProvider {
    
      onPageShow(routerName: string, page?: Page | undefined): void {
        super.onPageShow(routerName, page);
        hilog.debug(1, 'H5PageLifeCycle', "onPageShow: " + page?.pageUrl)
    
      }
      onPageHide(routerName: string, page?: Page | undefined): void {
    
        super.onPageHide(routerName, page);
        hilog.debug(1, 'H5PageLifeCycle', "onPageHide: " + page?.pageUrl)
      }
      onPageCreate(page?: Page | undefined): void {
    
        super.onPageCreate(page);
        hilog.debug(1, 'H5PageLifeCycle', "onPageCreate: " + page?.pageUrl)
      }
      onPageExit(page?: Page | undefined): void {
    
        super.onPageExit(page);
        hilog.debug(1, 'H5PageLifeCycle', "onPageExit: " + page?.pageUrl)
      }
      onBackPress(page?: Page | undefined): boolean {
        hilog.debug(1, 'H5PageLifeCycle', "onBackPress: " + page?.pageUrl)
        let navPathStack: NavPathStack|undefined = page?.getSession()?.getRouter()?.getNavPathStack()
        if (navPathStack && page && page.embedPage && navPathStack.size() == 1) {
          router.back()
          return true;
        }
        return super.onBackPress(page);
      }
    }

UI 定制

自定义导航栏

  1. 初始化完成后通过 provider 设置自定义导航栏。

    import {HRBuilder, RouterUtils, HRiver, H5CommonAppProvider, H5RouterNavStackProvider,
      CustomUIBuilderProvider,
      H5CacheProvider
    } from '@mpaas/hriver'
    
    HRiver.setProvider(CustomUIBuilderProvider.name, new CustomUIBuilderProviderImpl())
  2. 实现 CustomUIBuilderProviderImpl

    import { CustomUIBuilderProvider, Page } from '@mpaas/hriver';
    import { CustomUIBuilder } from '../pages/CustomTitleBarComponent';
    
    export class CustomUIBuilderProviderImpl extends CustomUIBuilderProvider {
      getCustomUIBuilder(): WrappedBuilder<[string, Page]> {
        return wrapBuilder(CustomUIBuilder);
      }
    }

    其中 CustomUIBuilder 为业务自定义 titlebar 组件的全局 Builder。实现参考如下:

    /**
    *  name: 自定义组件的名称
    *  page: 自定义titlebar对应的页面Page
    */
    @Builder
    export function CustomUIBuilder(name: string, p: Page) {
      if (name === 'titleBar') {
        CustomTitleBarComponent({page: p, titleBarData: p.titleBarData})
      }
    }

    CustomTitleBarComponent 为具体的标题组件,titleBarData 为标题栏所需要的数据,数据发生变化会实时刷新。示例参考如下:

    import { H5NavMenuItemData, HRiverUtil, Page, TitleBarData } from '@mpaas/hriver'
    
    const TAG: string = "CustomTitleBarComponent"
    
    
    const MENU_MARGIN: number = 5
    const DEFAULT_MARGIN: number = 12
    
    @Builder
    export function CustomUIBuilder(name: string, p: Page) {
      if (name === 'titleBar') {
        CustomTitleBarComponent({page: p, titleBarData: p.titleBarData})
      }
    }
    
    @Component
    export struct CustomTitleBarComponent {
      page: Page | null = null
      @Prop titleBarData: TitleBarData
    
      aboutToAppear(): void {
      }
    
      build() {
        RelativeContainer() {
          Button() {
            Image(this.getBackIconImage())
              .id('titlebar_back_img')
              .width(12)
              .height(20)
          }
          .width(48)
          .height('100%')
          .borderRadius(0)
          .backgroundColor(Color.Transparent)
          .align(Alignment.Center)
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top }
          })
          .id("h5_nav_close")
          .onClick((event) => {
            if (this.page != null) {
              this.page.backClickEvent()
            }
          })
    
          Flex({ justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
            //如果设置了图片标题就只显示图片
            if (this.titleBarData.titleImage) {
              Image(this.titleBarData.titleImage)
                .id('titlebar_title_image')
                .height(36).onClick(() => {
                this.onTitleClick()
              })
            } else {
              Flex({ justifyContent: FlexAlign.Start,direction: FlexDirection.Row }){
                Text(this.titleBarData.title)
                  .fontSize(18)
                  .textAlign(TextAlign.Start)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .maxLines(1)
                  .fontColor(this.titleBarData.titleColor)
                  .onClick(() => {
                    this.onTitleClick()
                  })
                if(this.titleBarData.showTitleLoading ){
    
                  Image(this.getTitleBarLoadingIcon()).width(18).height(18)
                    .id('titlebar_progress_img')
                    .margin({left:5,top:1})
                    .rotate({ angle: this.titleBarData.loadingRotateAngle })
                    .animation({
                      duration:3000,
                      curve: Curve.Linear,
                      delay: 0,
                      iterations: -1,
                      playMode: PlayMode.Normal,
                    }).onAppear(()=>{
                    this.titleBarData.loadingRotateAngle = 360
                  })
    
                }
    
              }
    
              if (this.titleBarData.subtitle) {
                Text(this.titleBarData.subtitle)
                  .textAlign(TextAlign.Start)
                  .fontColor(this.titleBarData.titleColor)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .maxLines(1)
                  .fontSize(18)
                  .onClick(() => {
                    this.onTitleSubtitleClick()
                  })
              }
            }
    
          }.id("h5_tv_title")
          .height("100%")
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top },
            left: { anchor: 'h5_nav_close', align: HorizontalAlign.End },
            right: { anchor: "h5_nav_options", align: HorizontalAlign.Start }
          })
    
          if (this.titleBarData.optionMenuState) {
            if (this.titleBarData.menuType == TitleBarData.MENU_TYPE_TITLE) {
              Text(this.titleBarData.menuTitle)
                .fontSize(16)
                .align(Alignment.Center)
                .textAlign(TextAlign.Center)
                .fontColor(this.titleBarData.menuColor)
                .height('100%')
                .id("h5_nav_options")
                .alignRules({
                  top: { anchor: '__container__', align: VerticalAlign.Top },
                  right: { anchor: '__container__', align: HorizontalAlign.End }
                })
                .onClick(() => {
                  this.onMoreClick(false, 0)
                })
                .margin({
                  right: DEFAULT_MARGIN
                })
            } else if (this.titleBarData.menuType == TitleBarData.MENU_TYPE_ICON) {
              Button() {
                Image(HRiverUtil.getIconImage(this.titleBarData.menuIcon))
                  .id('titlebar_right_icon')
                  .width(22)
                  .height(22)
                  .objectFit(ImageFit.Contain)
              }
              .width(30)
              .borderRadius(0)
              .backgroundColor(Color.Transparent)
              .align(Alignment.Center)
              .id("h5_nav_options")
              .height("100%")
              .onClick(() => {
                this.onMoreClick(false, 0)
              })
              .alignRules({
                top: { anchor: '__container__', align: VerticalAlign.Top },
                right: { anchor: '__container__', align: HorizontalAlign.End }
              })
              .margin({
                right: DEFAULT_MARGIN
              })
            } else if (this.titleBarData.menuType == TitleBarData.MENU_TYPE_MORE) {
              Button() {
                Image(HRiverUtil.getIconImage('more'))
                  .id('titlebar_right_more')
                  .width(22)
                  .height(22)
                  .objectFit(ImageFit.Contain)
              }
              .width(48)
              .borderRadius(0)
              .backgroundColor(Color.Transparent)
              .align(Alignment.Center)
              .id("h5_nav_options")
              .height("100%")
              .margin({
                right: DEFAULT_MARGIN
              })
              .alignRules({
                top: { anchor: '__container__', align: VerticalAlign.Top },
                right: { anchor: '__container__', align: HorizontalAlign.End }
              }).onClick(() => {
                if (!this.titleBarData.preventDefault) {
                  this.titleBarData.customPopup = !this.titleBarData.customPopup
                }
    
                this.onMoreClick(true, 0)
              })
              .bindPopup(this.titleBarData.customPopup , {
                builder: this.MenuBuilder,
                placement:Placement.BottomLeft,
                popupColor:"#fff",
                onStateChange: (e) => {
                  console.info(JSON.stringify(e.isVisible))
                  if (!e.isVisible) {
                    this.titleBarData.customPopup = false
                  }
                }
              })
            }
    
            if (this.titleBarData.menuType1 == TitleBarData.MENU_TYPE_TITLE) {
              Text(this.titleBarData.menuTitle1)
                .fontSize(16)
                .align(Alignment.Center)
                .textAlign(TextAlign.Center)
                .fontColor(this.titleBarData.menuColor1)
                .height('100%')
                .id("h5_nav_options1")
                .alignRules({
                  top: { anchor: '__container__', align: VerticalAlign.Top },
                  right: { anchor: 'h5_nav_options', align: HorizontalAlign.Start }
                })
                .margin({
                  right: MENU_MARGIN
                })
                .onClick(()=> {
                  this.onMoreClick(false, 1)
                })
            } else if (this.titleBarData.menuType1 == TitleBarData.MENU_TYPE_ICON) {
              Button() {
                Image(HRiverUtil.getIconImage(this.titleBarData.menuIcon1))
                  .id('titlebar_right_icon1')
                  .width(22)
                  .height(22)
                  .objectFit(ImageFit.Contain)
              }
              .width(30)
              .borderRadius(0)
              .backgroundColor(Color.Transparent)
              .align(Alignment.Center)
              .id("h5_nav_options1")
              .height("100%")
              .onClick(()=> {
                this.onMoreClick(false, 1)
              })
              .alignRules({
                top: { anchor: '__container__', align: VerticalAlign.Top },
                right: { anchor: 'h5_nav_options', align: HorizontalAlign.Start }
              })
              .margin({
                right: MENU_MARGIN
              })
            } else if (this.titleBarData.menuType1 == TitleBarData.MENU_TYPE_MORE) {
              Button() {
                Image(HRiverUtil.getIconImage('more'))
                  .id('titlebar_right_more1')
                  .width(22)
                  .height(22)
                  .objectFit(ImageFit.Contain)
              }
              .width(48)
              .borderRadius(0)
              .backgroundColor(Color.Transparent)
              .align(Alignment.Center)
              .id("h5_nav_options1")
              .height("100%")
              .margin({
                right: MENU_MARGIN
              })
              .alignRules({
                top: { anchor: '__container__', align: VerticalAlign.Top },
                right: { anchor: 'h5_nav_options', align: HorizontalAlign.Start }
              }).onClick(() => {
                if (!this.titleBarData.preventDefault) {
                  this.titleBarData.customPopup1 = !this.titleBarData.customPopup1
                }
    
                this.onMoreClick(true, 1)
              })
              .bindPopup(this.titleBarData.customPopup1 , {
                builder: this.MenuBuilder,
                placement:Placement.BottomLeft,
                popupColor:"#fff",
                onStateChange: (e) => {
                  console.info(JSON.stringify(e.isVisible))
                  if (!e.isVisible) {
                    this.titleBarData.customPopup1 = false
                  }
                }
              })
            }
    
          }
    
    
    
        }.height(this.titleBarData.showTitleBar ? 48 : 0)
      }
    
      getTitleBarLoadingIcon(): Resource | string {
        return $rawfile(`icon/h5_title_bar_progress_bg.webp`)
      }
    
      getIconImage(icon: string): Resource | string {
        return HRiverUtil.getIconImage(icon)
      }
    
      getBackIconImage(): Resource | string {
        return $rawfile(`icon/hriverback.webp`)
      }
    
      onTitleClick() {
        if (this.page != null) {
          this.page.titleBarClickEvent()
        }
    
      }
    
      onTitleSubtitleClick() {
        if (this.page != null) {
          this.page.subTitleBarClickEvent()
        }
    
      }
    
      onMoreClick(fromMenu:boolean, index: number) {
    
        if (this.page != null) {
          this.page.onMoreClick(fromMenu, index)
        }
    
      }
      onMoreItemClick(tag: string, name: string,isShowPopMenu:boolean) {
        if (this.page != null) {
          this.page.onMoreItemClick(tag, name, isShowPopMenu)
        }
    
      }
    
      @Builder
      MenuBuilder() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
          ForEach(this.titleBarData.h5NavMenuItemList, (item: H5NavMenuItemData, index) => {
            Column() {
              Row() {
                Image(item.icon).width(20).height(20).margin({ right: 5 })
                Text(item.name).fontSize(16)
              }
              .width('100%')
              .height(30)
              .padding({ left: 10 })
              .justifyContent(FlexAlign.Start)
              .align(Alignment.Center)
              .onClick(() => {
                if (item) {
                  this.onMoreItemClick(item.tag || '', item.name || '',false)
                  this.titleBarData.customPopup = false
                  this.titleBarData.customPopup1 = false
                }
    
              })
    
              if (index != this.titleBarData.h5NavMenuItemList.length - 1) {
                Divider().height(10).width('90%').color('#ccc')
              }
            }.padding(5).height(40)
          })
        }.width(150).height(165).backgroundColor("#fff")
    
      }
    
    }

自定义离线包加载页/错误页

  1. 初始化完成之后设置 Provider。

    import {
      CustomLoadingBuilderProvider} from '@mpaas/hriver';
      
    HRiver.setProvider(CustomLoadingBuilderProvider.name,  new CustomLoadingBuilderProviderImpl())
    
  2. 实现 CustomLoadingBuilderProviderImpl 类。

    import { CustomLoadingBuilderProvider, H5Router, HRLoadingData } from '@mpaas/hriver';
    import { CustomUIBuilder } from './CustomLoadingComponent';
    
    export class CustomLoadingBuilderProviderImpl extends CustomLoadingBuilderProvider {
      getCustomUIBuilder(): WrappedBuilder<[string, HRLoadingData, H5Router]> {
        return wrapBuilder(CustomUIBuilder);
      }
    }
  3. 实现 CustomLoadingComponent.ets 类,其中通过 loadingStatus 控制加载状态。

    import { H5Router, HRLoadingData, LoadingStatus } from '@mpaas/hriver'
    
    const TAG: string = "CustomLoadingComponent"
    
    
    export const Loading_STATE_Init = 0
    export const Loading_STATE_Start = 1
    export const Loading_STATE_End = 2
    export const Loading_STATE_Err = 3
    @Builder
    export function CustomUIBuilder(name: string, loadingData: HRLoadingData, h5Router: H5Router) {
      if (name === 'loading') {
        CustomLoadingComponent({loadingStatus: loadingData.loadingStatus, h5Router: h5Router})
      }
    }
    
    @Component
    export struct CustomLoadingComponent {
      @ObjectLink loadingStatus: LoadingStatus
      h5Router?: H5Router
    
      aboutToAppear(): void {
      }
    
      build() {
        Row() {
          Column() {
            Flex({ direction: FlexDirection.Row }) {
              //返回按钮
              Button() {
                Image($rawfile("icon/hriverback.webp"))
                  .width(12)
                  .height(20)
              }
              .width(48)
              .height('100%')
              .borderRadius(0)
              .backgroundColor(Color.Transparent)
              .align(Alignment.Center)
              .onClick((event) => {
                this.h5Router?.routerBack()
              })
    
            }.width('100%')
            .height(48)
            Image(this.loadingStatus.icon ? this.loadingStatus.icon : $rawfile("icon/hriverloading.webp"))
              .width(40)
              .height(40)
              .margin({top: 38})
            if (this.loadingStatus.state == Loading_STATE_Err ||
              this.loadingStatus.state == Loading_STATE_Start){
              Text(this.loadingStatus.state == Loading_STATE_Err ? `网络不给力,请稍后再试 \n(${this.loadingStatus.code} ${this.loadingStatus.msg})` : this.loadingStatus.title)
                .fontSize(18)
                .margin({top: 15})
                .textAlign(TextAlign.Center)
            }
    
          }
          .width('100%')
          .margin({
            top: 48
          })
        }
      }
    
    
    }

自定义在线 URL 加载失败的错误页

  1. 设置 Provider 拦截网页错误回调。

    HRiver.setProvider(H5WebClientProvider.name, new H5WebClientProviderImpl());
  2. H5WebClientProviderImpl 中实现 onErrorReceive 方法。

    import { MPFramework } from '@mpaas/framework';
    import {  H5WebClientProvider, Page } from '@mpaas/hriver';
    import { util } from '@kit.ArkTS';
    
    export class H5WebClientProviderImpl  extends H5WebClientProvider{
    
      onErrorReceive(page: Page | undefined, request: WebResourceRequest | undefined, err: WebResourceError | undefined): boolean {
        let errorUrl = request?.getRequestUrl()
        let errorCode = err?.getErrorCode()
    
        if (errorCode == 403 || errorCode == 404) {
          // keep same with ios,not show errorPage for 404 and 403
          // log(TAG, "ignoreErrorPage 404 or 403, return ");
          return true;
        }
        let lastUrl = page?.webcontroller?.getUrl()
    
        if (errorUrl == page?.pageUrl || errorUrl == `${page?.pageUrl}/`) {
          // 从rawfile中读取自定义错误页面demo_custom_err.html
          
          let dataBytes = MPFramework.instance.context.resourceManager.getRawFileContentSync('demo_custom_err.html')
          let textDecoder = new util.TextDecoder("utf-8", { fatal: false, ignoreBOM: false })
          let html = textDecoder.decodeWithStream(new Uint8Array(dataBytes), { stream: true })
    
          page?.webcontroller?.loadData(html, "text/html", "utf-8", lastUrl)
          return true;
        }
        return false
      }
    }
  3. demo_custom_err.html 文件中编写错误页代码。

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="apple-mobile-web-app-capable" content="yes"/>
        <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
        <meta name="format-detection" content="telephone=no"/>
        <meta name="format-detection" content="email=no"/>
        <meta name="viewport"
              content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=0"/>
        <title>!!!!</title>
        <style type="text/css">
            body {
            background-color: #FFF;
            }
    
            .am-page-result {
            text-align: center;
            }
    
            .am-page-result .am-page-result-pic {
            width: 135px;
            height: 135px;
            margin: 40px auto;
            }
    
            .am-page-result .am-page-result-pic img {
            width: 100%;
            height: 100%;
            }
    
            .am-page-result p {
            margin: 0;
            font-size: 16px;
            color: #999;
            }
    
            .am-page-result-button {
            margin-top: 25px;
            display: -webkit-box;
            display: -webkit-flex;
            display: flex;
            }
    
            .am-button {
            display: block;
            margin: 0 10px;
            padding: 0 10px;
            height: 42px;
            text-align: center;
            font-size: 18px;
            line-height: 42px;
            border-radius: 4px;
            outline: 0;
            -webkit-appearance: none;
            -webkit-box-flex: 1;
            -webkit-flex: 1;
            flex: 1;
            width: 50%;
            }
    
            .am-button[am-mode~=white] {
            border: 1px solid #DDD;
            color: #666;
            background-color: #FFF;
            }
    
            .am-button[am-mode~=white]:active {
            border-color: #D8D8D8;
            background-color: #F8F8F8;
            }
    
            .am-button[am-mode~=blue] {
            border: 1px solid #28F;
            color: #FFF;
            background-color: #39F;
            }
    
            .am-button[am-mode~=blue]:active {
            border-color: #17F;
            background-color: #28F;
            }
    
            .am-button[am-mode~=light], .am-button[am-mode~=light]:active {
            border: none;
            color: #39F;
            background-color: #FFF;
            }
    
            @media screen and (min-device-width: 375px) {
            .am-page-result .am-page-result-pic {
            width: 160px;
            height: 160px;
            margin: 45px auto;
            }
    
            .am-page-result-button {
            margin-top: 30px;
            }
    
            .am-button {
            margin: 0 20px;
            height: 44px;
            line-height: 44px;
            }
            }
    
            @media screen and (min-device-width: 414px) {
            .am-page-result .am-page-result-pic {
            width: 180px;
            height: 180px;
            margin: 50px auto;
            }
    
            .am-button {
            margin: 0 24px;
            height: 50px;
            line-height: 50px;
            }
            }
        </style>
    <body>
    <div class="am-page-result">
        <div class="am-page-result-pic">
            <img src="data:image/png;base64,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"/>
        </div>
        <p>这是自定义错误页面</p>
    
    </div>
    </body>
    <script>
    
    
    
    
    </script>
    </html>
    

设置和原生 View 一起滚动

//在启动参数里添加
// scrollForward: 0: SELF_ONLY, 1: SELF_FIRST, 2: PARENT_FIRST, 3 : PARALLEL
// scrollBackward: 0: SELF_ONLY, 1: SELF_FIRST, 2: PARENT_FIRST, 3 : PARALLEL
param.set("scrollForward", 2)
param.set("scrollBackward", 1)

支持限制 Web 组件宽度

  • 通过启动参数控制。

    params.set('webWidth', xxx)。 // 参数支持string ('100%'百分比方式) 或者number (例如800表示800px)
  • 动态控制。

    page.setWebWidth(webWidth: string | number)

鸿蒙 Web 行为定制

H5WebClientProvider

可以通过实现 H5WebClientProvider 修改鸿蒙 H5 容器 Web 的一些默认行为 API 。

HRiver.setProvider(H5WebClientProvider.name, new H5WebClientProviderImpl())

可定制的 API 如下:

export class H5WebClientProvider {

  // 页面http错误回调
  onHttpErrorReceive(page: Page | undefined, request: WebResourceRequest | undefined, response: WebResourceResponse | undefined) {
  }

  // 页面下载回调
  onDownloadStart(page: Page | undefined, url: string | undefined, userAgent: string | undefined, contentDisposition: string | undefined,
    mimetype: string | undefined, contentLength: number | undefined) {
  }

  // 页面ssl错误回调
  onSslErrorEventReceive(page: Page | undefined, handler: SslErrorHandler, error: SslError) {

  }

  // 页面错误回调
  onErrorReceive(page: Page | undefined, request: WebResourceRequest | undefined, error: WebResourceError | undefined): boolean {
    return false
  }

  // 全屏回调
  onFullScreenEnter(page: Page | undefined, handler: FullScreenExitHandler) {

  }
 
  // 退出全屏回调
  onFullScreenExit(page: Page | undefined) {

  }

  // web权限申请回调
  onPermissionRequest(page: Page | undefined, request: PermissionRequest | undefined) {

  }

  // web screencapturerequest回调
  onScreenCaptureRequest(page: Page | undefined, handler: ScreenCaptureHandler | undefined) {

  }

  onPageBegin(page: Page | undefined, url: string | undefined) {

  }

  onAppear(page: Page | undefined) {

  }

  // web scroll回调
  onScroll(x: number, y: number, page: Page | undefined) {

  }

  getWindow(context: Context, page: Page | undefined): Promise<window.Window> | undefined {
    return undefined
  }

  // web 长按菜单
  onContextMenuShow(param: WebContextMenuParam | undefined, result: WebContextMenuResult | undefined) {
    return false
  }

  // web 文件选择回调
  onShowFileSelector(fileSelector: FileSelectorParam, result: FileSelectorResult, page: Page | undefined) {
    return false
  }

  // web 定位相关回调
  onGeolocationShow(origin: string | undefined, geoLocation: JsGeolocation | undefined, page: Page | undefined) {

  }

  // web 定位相关回调
  onGeolocationHide(page: Page | undefined) {

  }
}

H5MixModeSettingProvider

可以通过 H5MixModeSettingProvider 设置 Web 支持 HTTP/HTTPS 的 MixedMode,具体使用如下:

HRiver.setProvider(H5MixModeSettingProvider.name, new H5MixModeSettingProviderImpl()) // 业务实现H5MixModeSettingProviderImpl
export class H5MixModeSettingProviderImpl extends H5MixModeSettingProvider {
  mixMode(page: Page | undefined): MixedMode {
      // 根据业务实际情况返回对应的MixedMode
    return MixedMode.Compatible
  }
}
重要

鸿蒙系统不支持在 HTTPS 页面加载地址为 HTTP 的 IP 链接,要么加载 HTTPS 的 IP 的链接,要么加载 HTTP 的非 IP 链接。

页面路由支持 Navigation

默认页面路由使用 router 方式,鸿蒙 router 方式不支持关闭栈中某个页面,只能一级级回退。离线包支持 Navigation 模式:

  1. 支持关闭栈中页面

  2. 支持分栏模式

全局 Navigation 模式

  1. HRiver 初始化完成后,在启动离线包之前,需设置 H5RouterNavStackProvider

    HRiver.setProvider(H5RouterNavStackProvider.name, new NavStackProvider(this.pageInfos))
    import { H5RouterNavStackProvider } from '@mpaas/hriver';
    
    export class NavStackProvider extends H5RouterNavStackProvider {
      navStack: NavPathStack // 全局的navPathStack栈
    
      constructor(navStack: NavPathStack) {
        super();
        this.navStack = navStack;
      }
    
      getNavPathStack(): NavPathStack {
        return this.navStack
      }
    }
  2. navDestination 中配置 builderbuilder 中加载 mPaaS 全局 Builder。

    import {HRBuilder, RouterUtils, HRiver, H5RouterNavStackProvider} from '@mpaas/hriver'
    
    let mPaaSHRiverBuilder: WrappedBuilder<[string, ESObject]> = wrapBuilder(HRBuilder);
    
    @Builder
    PagesMap(name: string, params: ESObject) {
      if (RouterUtils.isMPHRiverPage(name)) {
        mPaaSHRiverBuilder.builder(name, params)
      } else if (name == 'xxx') {
        // 其他业务的页面
      }
    }
    
    build() {
        Navigation(this.pageInfos) {
          Row() {
            Column() {
              // 业务页面
              MainPage()
            }
            .width('100%')
          }
        }.navDestination(this.PagesMap)
      }

离线包独立使用 Navigation 模式

  1. HRiver.startApp/HRiver.startUrl 的第三个参数传入 NavPathStack 即可。

  2. 参考 全局 Navigation 模式 中的步骤 2。