Quickstart

Updated at:

Learn how to integrate the H5 container component into a HarmonyOS NEXT client. Add the H5 container SDK to your project using ohpmrc.

Prerequisites

Dependencies

In your project's .ohpmrc file, add the following repository:

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

Add the SDK

Use the mppm tool to install the H5 container component.

image

Permissions

Configure the required permissions in module.json5.

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

Using the SDK

Initialization

Once the mPaaS framework is initialized, initialize HRiver with the following code:

HRiver.init();

Before using the SDK, you must initialize the mPaaS framework and set the userId and appsecret. The appsecret can be obtained from the AppCenter console. For details, see Get the HarmonyOS NEXT configuration file.

The code example follows:

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";
  }
}

Open an offline package

After initializing HRiver, call startApp to open the offline package.

import { HRiver } from '@mpaas/hriver'

/**
* Opens an offline package.
* @param appId The offline package ID.
* @param startParams Optional. Startup parameters that control elements such as the title bar and the target page.
*/
HRiver.startApp(appId: string, startParams?: Map<string, Object>)

Here is a code sample:

import { HRiver } from '@mpaas/hriver'

// Example 1:
HRiver.startApp('20190517') // Start the offline package directly.

// Example 2: 
let startParams: Map<string, Object> = new Map();
startParams.set('defaultTitle', 'Default Title') // Sets the default title for the loading phase.
HRiver.startApp('20190517', startParams)

Online page

Initialize HRiver, then call startUrl to open a web page.

import { HRiver } from '@mpaas/hriver'

/**
* Opens a web page.
* @param url The URL of the web page.
* @param startParams Optional. Start parameters to control elements such as the title bar and the target page.
*/
HRiver.startUrl(url: string, startParams: Map<string, Object>)

Custom JSAPI

To register a custom JSAPI, initialize HRiver and then call registerPlugin.

import { HRiver } from '@mpaas/hriver'

/**
* Registers a custom JSApi implementation.
* @param pluginClass An array of plugin classes.
* @param scope Optional. The plugin's lifecycle scope. Use `HRiver.SCOPE_APP` for the offline package's app-level lifecycle or `HRiver.SCOPE_PAGE` for the page-level lifecycle. Defaults to `HRiver.SCOPE_PAGE`.
*/
HRiver.registerPlugin(pluginClass: ESObject, scope: string)

Code example:

HRiver.registerPlugin({H5CustomPlugin, H5Custom1Plugin})

The following example shows the plugin implementation:

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 called successfully'
      })
      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 called successfully'
      })
      return true
    }
    return super.handleEvent(event, context);
  }
}

Native calls H5

There are two ways to call H5 from native code.

  • In a custom JSAPI, you can send a message to an H5 page using the H5BridgeContext.sendToWeb method.

    import { H5BridgeContext, H5Event, H5EventFilter, H5SimplePlugin, HRiver } from '@mpaas/hriver';
    class H5CustomPlugin extends H5SimplePlugin {
      handleEvent(event: H5Event, context: H5BridgeContext): Boolean {
        // Send a message to the H5 page.
        context.sendToWeb('customCallWeb', {
            data: 'abc'
        })
    
        ... // Other code
      }
    }
  • From your native code, use AppManager to get the current top page and call its sendToWeb method to send a message to the H5 page. How you retrieve the page depends on how it was opened.

    Method 1

    When using HRiver.startApp to open an offline package, get the top app with findTopApp and the current active page with getActivePage.

    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: '' })
     }
    }

    Method 2

    When using HRiver.startUrl to open an online page, get the top H5Session with findTopSession and the current page with getTopPage.

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

Built-in offline package

Loads all built-in offline packages, including the public ones.

After initializing HRiver, call loadOfflineResource to load the built-in offline package.

import { HRiver } from '@mpaas/hriver'

/**
* Loads a built-in offline package.
* @param jsonFileName The filename of the `h5_json.json` file for the built-in offline package. Place this file in the rawfile directory. For example: `h5_json.json`.
* @param callback A callback function that runs after the built-in offline package loads. The signature is `(result: string) => {}`.
*/
HRiver.loadOfflineResource(jsonFileName: string, callback: Function)

Here is a code example:

  1. In the entry/src/main/resources/rawfile directory, add the h5_json.json file, which can be downloaded from the Appcenter console.

    {
       "config":{
          "updateReqRate":16400,
          "limitReqRate":13600,
          "appPoolLimit":3,
          "versionRefreshRate":86400
       },
       "data":[
          {
             "app_desc":"Offline package 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":"Offline package 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":"Offline package 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":"Offline package 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":"Operation successful",
       "state":"success"
    }
  2. Download the .amr file and rename it to ${appid}_${version}.amr.

    image

  3. Call the API.

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

Update an offline package

import { HRiver } from '@mpaas/hriver'

/**
 * Updates offline packages in a batch.
 * @param appIds The IDs of the offline packages to update.
 * @param updateCallback A callback that is executed after the update completes. The function receives a `result` (boolean) and a `code` (number).
 */
HRiver.updateApp(appIds?: Array<string>, updateCallback?: Function)

/**
 * Updates offline packages in a batch to specific versions.
 * @param appIds A map of `appId`s to their target `version`s.
 * @param updateCallback A callback that is executed after the update completes. The function receives a `result` (boolean) and a `code` (number).
 */
HRiver.updateAppWithVersion(appIds?: Map<string, string>, updateCallback?: Function)

The code example follows:

import { HRiver } from '@mpaas/hriver'

HRiver.updateApp(['90000002'], (result: boolean, code: number) => {
  this.resultText = `Update for 90000002: ${result}`
})

Public offline package

  1. Configure the H5CommonAppProvider.

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * Configures the common offline package provider. Must be called before HRiver.init().
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    Note

    setProvider must be called before HRiver.init().

  2. Implement the getCommonResourceAppList method in H5AppCommonProviderImpl.ets.

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
      getCommonResourceAppList(): Array<string> {
        return ['20220719'] // Returns a list of common offline package IDs.
      }
    }

Fallback logic

On iOS and Android, if an offline package fails to download, the application falls back to the online address.

This feature is disabled by default. To enable it, use the following switch:

In H5CommonAppProvider, implement the configJSON method and add the enableFallback parameter.

export class H5AppCommonProviderImpl extends H5CommonAppProvider {
  ... // Other configurations

  // Specifies configuration settings, such as the update frequency.
  configJSON(): string {
    return JSON.stringify({
      enableFallback: 'YES',
      xxx // Other configurations
    })
  }
}

User-Agent

Note

Calling setUserAgent after HRiver.init() appends the specified useragent to the default user agent.

Offline package default update frequency

  1. Set the H5CommonAppProvider.

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * Configures the common offline package provider, including its update frequency and other settings. This method must be called before HRiver.init().
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    Note

    You must call setProvider before HRiver.init().

  2. Implement the configJSON method of H5CommonAppProvider as shown in the following code example.

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
      ... // Other configuration
    
      // configJSON: Configure the update frequency and other settings.
      configJSON(): string {
        return JSON.stringify({
          h5_nbmngconfig: "{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1800\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}"
        })
      }
    }

    The h5_nbmngconfig parameter contains the configuration string:

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

    ur: 1800 indicates an update frequency of 1800 seconds; change the value of ur to adjust this frequency.

Offline package signature verification

  1. Set the H5CommonAppProvider.

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    /**
    * Configures features such as the common offline package provider, offline package update frequency, and signature verification.
    * Call this method before HRiver.init().
    */
    HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
    Note

    Call setProvider before HRiver.init().

  2. Implement the H5CommonAppProvider methods: shouldVerify and pubKey. The following is a code example:

    import { HRiver, H5CommonAppProvider } from '@mpaas/hriver'
    
    export class H5AppCommonProviderImpl extends H5CommonAppProvider {
     ... // Other configurations 
    
     /**
     * Specifies whether to enable signature verification for offline packages. Disabled by default.
     * @returns {boolean} `true` to enable verification, or `false` to disable it.
     */
     shouldVerify(): boolean {
     return false
     }
    
     // The public key for offline package signature verification. 
     // This key is required only if shouldVerify returns true.
     pubKey(): string {
     return ''
     }
    }

Enable debug mode

Monitor page lifecycle

After initialization, set the 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 deregistration

Implement log out for the Page component in Page.ets.

// Unregisters a plugin by its action.
unregisterPluginByAction(action: string);

// Unregisters a plugin by its name.
unregisterPluginByPluginName(name: string);

Here is an example:

image

Embedded mode

The page embedding mode is based on navigation.

  1. Create the NavPathStack required by the offline package.

    pageInfos: NavPathStack = new NavPathStack()
  2. Add a blank component where you want to embed the page.

    Important

    The mode must be NavigationMode.Stack. Otherwise, you may encounter issues when switching between portrait and landscape orientations or on foldable screens.

    Navigation(this.pageInfos) {
              Column() {
                // Blank component to embed the offline package page. No content is required.
              }.width('100%') // Set the width as needed.
              .backgroundColor(Color.Red) // For example only.
              .height(500)  // Set the height as needed.
            }.navDestination(this.PagesMap)
            .mode(NavigationMode.Stack)
    
    
    // pagesMap implementation:
    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') {
        // Pages for other services.
      }
    }
  3. When starting an offline package or an online page, pass the NavPathStack created in Step 1 as the third parameter. Add the embedPage parameter to specify that the page is embedded.

    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. Intercept the back press event.

    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 customization

Custom navigation bar

  1. After initialization, use a provider to set a custom navigation bar.

    import {HRBuilder, RouterUtils, HRiver, H5CommonAppProvider, H5RouterNavStackProvider,
      CustomUIBuilderProvider,
      H5CacheProvider
    } from '@mpaas/hriver'
    
    HRiver.setProvider(CustomUIBuilderProvider.name, new CustomUIBuilderProviderImpl())
  2. Implement 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 is the global builder for a custom title bar component.

    /**
    *  name: The name of the custom component.
    *  page: The page that corresponds to the custom title bar.
    */
    @Builder
    export function CustomUIBuilder(name: string, p: Page) {
      if (name === 'titleBar') {
        CustomTitleBarComponent({page: p, titleBarData: p.titleBarData})
      }
    }

    CustomTitleBarComponent is the concrete title bar component. titleBarData contains the data for the title bar. The UI updates in real time as this data changes.

    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 an image title is set, the title bar displays the image instead of text.
            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")
    
      }
    
    }

Custom loading and error pages

  1. Set the provider after initialization.

    import {
      CustomLoadingBuilderProvider} from '@mpaas/hriver';
      
    HRiver.setProvider(CustomLoadingBuilderProvider.name,  new CustomLoadingBuilderProviderImpl())
    
  2. Implement the CustomLoadingBuilderProviderImpl class.

    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. Implement the CustomLoadingComponent.ets class. Use loadingStatus to control the loading UI.

    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 }) {
              // back button
              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 ? `The network is unstable. Please try again later. \n(${this.loadingStatus.code} ${this.loadingStatus.msg})` : this.loadingStatus.title)
                .fontSize(18)
                .margin({top: 15})
                .textAlign(TextAlign.Center)
            }
    
          }
          .width('100%')
          .margin({
            top: 48
          })
        }
      }
    
    
    }

Custom URL error page

  1. Set a provider to intercept the web page error callback.

    HRiver.setProvider(H5WebClientProvider.name, new H5WebClientProviderImpl());
  2. In the H5WebClientProviderImpl class, implement the onErrorReceive method.

    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) {
          // For consistency with the iOS implementation, do not show the error page for 404 and 403 errors.
          // log(TAG, "ignoreErrorPage 404 or 403, return ");
          return true;
        }
        let lastUrl = page?.webcontroller?.getUrl()
    
        if (errorUrl == page?.pageUrl || errorUrl == `${page?.pageUrl}/`) {
          // Read the custom error page demo_custom_err.html from the rawfile directory.
          
          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. In the demo_custom_err.html file, write the error page code.

    <!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,iVBORw0KGgoAAAANSUhEUgAAAQ4AAAEOCAMAAABPbwmXAAAAwFBMVEUAAAD///8zi7r//vwClewFue7///90wNwQvOzn39X3x5D///8UvO321KL///8Guu4SvO73x5H///6hyun3x5Clyumlyummyun+1aD////////wu4SjxuKlyuk+wOpZYooAu+7/1qFYYYnwuoOlyukAuu3/1p8AsewAufr/25ZMV4U+Snt62PVKxuZ5wcT/56b6ypP/u3TZz6qCobRscZCAgpmll5TIt5qg5vn////t+P655P/C5//Z8f/J0OOqv8Kmuo/+AAAAIHRSTlMAPkjv/X99EPYjepuk92HIXqPcQE3Ye7PQsMfRX5aRu7xgWQUAAA+TSURBVHhe7JrdiiMhEEZXCpRG8MI7fZP6Mfv+b7VLIDAzziRD1NZKPDeB9FUfvvrU7v6zCpvNZrNxhwnW+pRzjHAlxpyTtzaYw72ViGBTBrwDZG/DG0g5jE2AvwSSNcfrpsLYKhMPgWyNe8FYBA/4JODDS4XEhQTYRgqvkhHjATsA3rxCMDJ2IyuPyOEBuwL+UDwlOABvtoxPKEzI8UHGFmIBhwJW05xEHE40WuYk4SkkFRMTAE8Cwo7Gl4Ds1tDTIBZPZ90lxiWcQHKL1kbEKcQlC8QATgIWLJCAEwkr29g+LE7Grmpj+wi4AGFFG9uHwUVYYr09cBmO+TZcxFaICLsQ3XQdqUlEKXKjlHYpSfESS0WEmeWK8H+kkO7l1rTIYJYvMEvRXKeAT1JuLmohhA3EmTY8PgcJy08wNwXE6xsVYpY7sOgcl9hgY5iPqGxVIZZHsOhbXVy3bNQ09YfT1KPC8guYlLXp0W9Uvv1L2dkl9QkH8+2n37h4teG4bcypMKuLR3tzFP5py0XCetvDYY9ZYaqudZkWcDoeCNLdO+43LUHBhrSaFbl7VdHW1GAHHfU4yCdIzcnFd9FBlY7qso4yjU836Ufoka3niRpmBal8BMfpQKP+JWStQ8m5NuMA6u5Q8lDdwQnhEMEWYEJ1DA2HaHmmbs8IBxctb7A9joCqI1wTXnmTCkvHWcE882VTO1KHQ0mXnrCqcNHzecMJNkTR1y/Da1SY3lkHccOoTF9ph9eooCYdw4uDVH1pOtaGMKEqHYNrtKAuHeNrVL+O+TWqv0rrGmVSrGP+/osul79XLhdSraP9qPKPezNYbRgGgmirQkUp+JBbcjHMcfZo6VC5lv7/r0pKDw2ByNkIeeP3CQ/PstZIS5kCSPmFRJzK8txbqf5Wx1IiRUjiD5IiiCU/7y9cmpU28gQR4gqKMJa+tfWhYVZUgyPHCxdXRkLpWTwdG+rQRGWi4CYiIX8aap30FXadHISoITK6XjqGLWu3IoI1EKOJe2H62bFGxyTEWkZvYpbqG/0aKQruwRkoWtSNflVHCoL7cHt+KhoEMOjDb2MjCmDSx3ELG0Wgwe0zLYmEUR/eUFTqfOwwLVkILTt8eD8J1IxGNzE9CYQe9xzDNF1yMys4Y3Z8vDUvFeallhXDcRkaNU5nIXNdRyBs+zg00LG6cEogYDouQ08dmYTxz+PUU4fgUZy53UOvowgexps89tDrsB6XNwM6TMalvw79MuZcl7jY1wF/tgHXZ/mwr2N03mG00zHo9w60YjS7jKX5gu8bOkA0wtttKJe1xUJqpsPt4mAsEFeQNFg0nDY6DCMAUu5xwn82fpg7v95WcSCKb/40cZNC7lZ9uK32we5qxmBCqLhAsrvpzff/VmsTsy51jOGqCXsqRYpO+5CfzswwBcXL4er5LYyj3FUQVR4E+31VvvZFIpKGxvRuM/8gzkkvbR7upqPw8I8WITBjmRQL9lXx6iciktc/nlar1dPybt4Sp42A+gR08+1/8Y3xn3tpUjIpVC8SSV6VkkgnDVF9DzKGDIOTBcMpMKAMkOnYPOzbLEnBMmYkU5JXhXATSYp9pv8AY9z5kgHm1bbBG5Dly22bR1IEmc6GIcL2LiJClEwlQwcqjk8WDeB+QQPlbsx5a9+TFK+KRiP8SKT83EbUu+KHtFD9NAHZNTRAZQA4p1u/CHANxN9A1repFpuGgYKKSFDPmkSclYii+EGiU4ztX45TiYNqcUj7PYl13IIOlH/ChLeqFiHyDFEj0K+tNiJnTVWWRVGUZfXniURRqmkYxfl8Dg2NtP8Drcf0HBCyHPcQDlMtQuwzkwhLGknDZhdFNMpjZoQ6HpyCpjEopdtzPh7GPtmpTAyNtmw0pkdEFCKCNrf4FFFDY4jSM4/lyKct/EwsGh7pz6xqBS0rP09PTgbnlHA9XfyaXG/iztQaJ5I2DewKShzQCCSOXYzaMDYGQKWAvw/fw+v2QcY+jqNKJI2qTzZMqQCtcTAjg8OUyuBy0cNl1IAEA2lgQKnBYZnKAn78BRxHXS09FV5lh8myStHAYZ3DgYMhIwoH+aWcEoVy0//ZsfXXw8C3LK/Y5XDgpbcYpxEFcOFA6bpr5Xj453A4HJ3VAhQ2Ix5Im72pVGQdGCwLGfkvHXipsSgcW+fVhZLbtXqpT5Pnr6aBjhbhBEJorShFNJSaFx8OAPDgGO385pmi4ZcBZmaHEgSxo7N0fmAvjqG6Xzx+UThYXyHDz+mg0Sm2ucU59eCgXhzDFX5BzcwQHdWBXWBQ9w6ISIC2nUNHK015t2tNlhvWzGOrVtCPwkwW6hq1WHdagMuL/d8UKO1ywbHEXfPCzODwADAOXtrSaHvDRxMe4Ft3NTjdQ+1+G+Ne3ex5cb96s+qg91UpNLPFceFBXFsa0C5X6l4CuGlAZi+LUO3RK3Su8Ebo2llA4iCIeBEH5VtnOLrdzS1u5hoS68Vk2QyoFgr0z9r2Rqt6qV1OqrU4tpYjrzPldk2tXDsgs8fn9SKsSRh9Z5ZwyNZCIcBL2VEWh88r/jtwRYI6XMqVubnmw0Ivi0UYhpPJ5F5ysPXkyISvrdYfuc5HbuNT3pnHod0oQZdKpzu95knooQfk7/54oOtfHlGNI2bowEE5T00E3lP+gQa43IerPk22+Jd68+tx1IaiuBs1a2BAiSseXFGtiCy75CGIYR9mWu0q3/9blQSxJ4uvLVxFmrlHmkxGzAs/nfuPa0Qyj7jA45p1E5HxDGIrHHbKEPby/n36ZeL7+8Uib8w/89Uf09Uf75f2dtUijz7HIOk4REGbIO6NO49TPwEZz/AQVk/Lnd/2Tnb5ZWEOXMU/Ya3wPIOk4xB5oavQTSNZkAZx47WicscvC2t7E7XIx9Xl2gE0nmKQZByouFBKu641sAEHtrQ2RAHra8RQKZ4r+ZKKAynEJTQeUK1XEN2ypwUQMPCRQDjg8XSDpOMo4IUUKVGB1MOadlb3U4sTVubA96wEjKcbJB1HMySMcOjLpfAnHueu336qn7WP67hD0ni+QdJx1MY4igU5/4OJEjWF69WT+EDJl2QcyhhDHmSIqpo4AgVkPEnxEYJB0nBIM2mg7BFFkgtRgF4Mh/hYyZckHLW5ydG7laBdirki+Z7yaDQC+iiDJOBoDHgE5KdMFRiJT8NnwgGDbMaRL55ed58ulEoWGjnhps+GAwbZiKMw4LG1I1XLAwIG7sChoZeEWEmJlyIyDxtPSnwOffmyLZFCw5bu1NViVuVFSgAHIymz4nHPEy488et8AUmyMp4KwUivdz0Aic9vFW5Oew0sMilLHHJ3/Nr3365vBkgAhJLKEWVggE8KRy1Y6Fhm1s4jZ3bp3wyIBDb7usgFpOOxAuWCgXaHFuomZRMRABncqtnQqhaPKsgdTWV8SQY09t6TmJkILGJUoXU1SWul4AuEijfT0KmjYUCjbH3Zhcjj5BV4Vy2vyDGvaljW2ZJ+I8vePdKf3RC/DZHrEyltOBaWvYcBX+dDPW6IFYS68kDAHOwKy87aAIwWJ+9RR9aSKvRoRDcMM6nMWksbYznSs3QSWtXSX8xUwSZNGYaZtKRILFw6Wz0O+s5pVdTLxjuvC30KSyuGqWP3SMKCiY0cnq30TROoqJRh2IRl8AawYBWCtWtMyBho0XXDMFb2VNpAIr1Q5ojzQVnhFyvStjHh/Tai5YxLG4axUlLF1auyyXKYVjjFyq4NC+eq4zs5+pqB+PRgB9RVzxs4CrjhzZboPgFiYA67woC/0YJF755+6sMwkR7IcoKvI2KFokDLgQYzc3itF36hyqbLGabm2FBlU4g4RArjsmL9LIoXEBI4OEQKz7IyizzWGGrB4orQUHzKCpjgo+3Hc3ryMEFJds9HkUzv9mgvV/crERfrPmANhqEiwSBIpeuyu0VAITzBAAa/qgJzxGW7m0XGM4GEhsExcWCURStqgyWm6+ylHx2JBIwGw5mG2IdDBLJ2IdJm/XU8nSc5AsowGN40RBaAYQOM5tXtxKQCDqDgTuMIM2Cir84nqLPLOCwyEMMsCh02kSBnmi4zryZBNYN9vY0xiKvrU2g0OcsVdQKeN+BgHyiosv/PHzYlVpQUHHQkaUTSB2aZhFhRueChw2o+WVXZqHe6vwq1JWcUUjDRzjOBpRdxFJ1sXlSrJsqiFnxU0kGxKYLwyp6sCwJKM6HI/yPnClIchoGYMRg7i8E+5AG56P9f3B42sFjMTN1bFUEPaXPJwEgajZv0TagU9+D+BJ79Ar0koB7nef784aRKfAGmsVZ5y7WjJjEM533LYRONJIZM/hsbnrQkMRRqgaBH8P/mnMTQtt0o1HsF1nMDD+uVTl1AZozqpdsrtUVDCpyatCSGecE4GhcDV1c0HTGwZMc3plqvLC6UjBfcsVbHkrJBj9V1FZoh3SswisBTv6jMVuOBDZlZL7OermwMKfIyO6gj2G7YuUfX05X9dYKuzE63ECEOXV1BFA7qJz8Vm54cUJ5mJ/xakCHVpo7O2xWQ9TQ9GapyDoattT30qCNf21CmjhIf+IETgmRVS8oyC9q70FUT7BVYBt3WHFWHXqgDvMUb1kmvyMeCsL6AfoZe25tZMdaqQJE6JlXBYVH9DL1YxMDAAxz6IN4ke/ok6vDnNu9XTer4LAmDpuvojrLGgBx1BCYjmPWzokNfgHUFC75HlTp8mAdvValjNxGUjkmHTZ5RNaBHHcfNkhyBsm/nHL1VaerwN9X8/RCnDg7F3HrpZx1b/5Scj12/4YL8sfyJSFYIEF7OFpdH4adiQD9eqC9oUgcfcAG8uvy2cz6rDcMwHN6l2IZCBsLSxQ47SLfkFfz+j7V4hqVbmqitwU0g3zUnf4Sf/hw0Ji74CSKKERFDAHCHMmQAAmIkskp+KpE68DrZT8xu3F7NGAdZgvdc4EHJCS1NWGE2E3FPXhxkDfyPNMpmRmh9uuUn8YQB3ivFBJxFLHRsaNAZE7+CjwjmPSqi5w3srEAVIktLQ+KXIYTGMiCygpoZsjQkv15k4Co8uoa5Saxy14A8fLOCq8FmOjxrpFGJCkWI5XpiKx2skkbl0XqSVkPNaqtnjfFOQZWNFaH8zZKhXgc1rDBBE2KVIqswVMtoXFwAiTdY67REXQhJfZISul11YWl46JCLrIz5YjlT34e1x9zr0dMo82uX7179Q6QiST0pJppKCWWAm3WI6MGxXKQ/r8MTYRlX9odxZby/dtZaKUu/RfOlmpFSWNQRv4yyh8AYB/D5een7/tp1nc3cDinr1yhvk3S5AgoBFAcH0VME/XAp9JnrLf3E9AERwwRk3HHWXycnJyffpA9otAf5V/kAAAAASUVORK5CYII="/>
        </div>
        <p>This is a custom error page.</p>
    
    </div>
    </body>
    <script>
    
    
    
    
    </script>
    </html>
    

Synchronized scrolling with native view

// Set startup parameters.
// 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)

Limiting web component width

  • Set the width with a startup parameter.

    params.set('webWidth', xxx); // Accepts a string for a percentage (e.g., '100%') or a number for pixels (e.g., 800).
  • Update the width dynamically.

    page.setWebWidth(webWidth: string | number)

HarmonyOS web behavior customization

H5WebClientProvider

Implement H5WebClientProvider to customize the default web behavior of the HarmonyOS H5 container.

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

The customizable APIs are:

export class H5WebClientProvider {

  // HTTP error callback
  onHttpErrorReceive(page: Page | undefined, request: WebResourceRequest | undefined, response: WebResourceResponse | undefined) {
  }

  // Download callback
  onDownloadStart(page: Page | undefined, url: string | undefined, userAgent: string | undefined, contentDisposition: string | undefined,
    mimetype: string | undefined, contentLength: number | undefined) {
  }

  // SSL error callback
  onSslErrorEventReceive(page: Page | undefined, handler: SslErrorHandler, error: SslError) {

  }

  // Page error callback
  onErrorReceive(page: Page | undefined, request: WebResourceRequest | undefined, error: WebResourceError | undefined): boolean {
    return false
  }

  // Full-screen enter callback
  onFullScreenEnter(page: Page | undefined, handler: FullScreenExitHandler) {

  }
 
  // Full-screen exit callback
  onFullScreenExit(page: Page | undefined) {

  }

  // Permission request callback
  onPermissionRequest(page: Page | undefined, request: PermissionRequest | undefined) {

  }

  // Screen capture request callback
  onScreenCaptureRequest(page: Page | undefined, handler: ScreenCaptureHandler | undefined) {

  }

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

  }

  onAppear(page: Page | undefined) {

  }

  // Scroll callback
  onScroll(x: number, y: number, page: Page | undefined) {

  }

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

  // Context menu callback
  onContextMenuShow(param: WebContextMenuParam | undefined, result: WebContextMenuResult | undefined) {
    return false
  }

  // File selector callback
  onShowFileSelector(fileSelector: FileSelectorParam, result: FileSelectorResult, page: Page | undefined) {
    return false
  }

  // Geolocation show callback
  onGeolocationShow(origin: string | undefined, geoLocation: JsGeolocation | undefined, page: Page | undefined) {

  }

  // Geolocation hide callback
  onGeolocationHide(page: Page | undefined) {

  }
}

H5MixModeSettingProvider

Use H5MixModeSettingProvider to set the MixedMode for handling mixed HTTP and HTTPS web content:

HRiver.setProvider(H5MixModeSettingProvider.name, new H5MixModeSettingProviderImpl()) // Business implementation.
export class H5MixModeSettingProviderImpl extends H5MixModeSettingProvider {
  mixMode(page: Page | undefined): MixedMode {
      // Return the appropriate MixedMode for the given page.
    return MixedMode.Compatible
  }
}
Important

HarmonyOS does not support loading content from an IP address via HTTP on an HTTPS page. Instead, use an HTTPS link to the IP address or an HTTP link to a domain name.

Page navigation

Page routing defaults to router mode. In HarmonyOS, this mode does not support closing a specific page in the stack; you can only return to the previous page. Alternatively, offline packages support navigation mode:

  1. Support for closing pages in the stack.

  2. Support for split-view mode.

Global navigation mode

  1. Set H5RouterNavStackProvider after HRiver is initialized and before launching an offline package.

    HRiver.setProvider(H5RouterNavStackProvider.name, new NavStackProvider(this.pageInfos))
    import { H5RouterNavStackProvider } from '@mpaas/hriver';
    
    export class NavStackProvider extends H5RouterNavStackProvider {
      navStack: NavPathStack // Global NavPathStack.
    
      constructor(navStack: NavPathStack) {
        super();
        this.navStack = navStack;
      }
    
      getNavPathStack(): NavPathStack {
        return this.navStack
      }
    }
  2. In navDestination, configure the builder, and in the builder, load the mPaaS global 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') {
        // Pages for other services.
      }
    }
    
    build() {
        Navigation(this.pageInfos) {
          Row() {
            Column() {
              // Application page.
              MainPage()
            }
            .width('100%')
          }
        }.navDestination(this.PagesMap)
      }
    }

Standalone navigation mode with offline package

  1. Simply pass NavPathStack as the third parameter to HRiver.startApp or HRiver.startUrl.

  2. See Step 2 in Global navigation mode.