本文介绍如何将 H5 容器组件接入到 HarmonyOS NEXT 客户端。您可以基于已有工程使用 ohpmrc 方式接入 H5 容器 SDK 到客户端。
前置条件
添加 H5 容器 SDK 之前,请您确保已经将工程接入到 mPaaS。更多信息请参见 基于已有工程使用 ohpmrc 接入。
引入依赖
在项目的
.ohpmrc
文件中添加如下仓库:@mpaas:registry=https://mpaas-ohpm.oss-cn-hangzhou.aliyuncs.com/meta
使用
ohpm install @mpaas/hriver
安装 H5 容器的依赖。
添加 SDK
在 oh-package.json5
中配置所需依赖,具体版本号请参考开发指南手册基于已有工程使用 ohpmrc 接入中“添加 mPaaS 组件依赖”的组件列表。
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {
"@mpaas/hriver": "0.0.5-2406300000"
}
}
配置权限
在 module.json5
中配置所需权限。
"requestPermissions":[
{
"name" : "ohos.permission.GET_NETWORK_INFO",
},
{
"name" : "ohos.permission.INTERNET",
}
]
使用 SDK
初始化
在 mPaaS 框架初始化完成之后,初始化 HRiver
。代码如下:
HRiver.init();
使用 SDK 之前必须初始化 mPaaS 框架,设置 userId 和 appsecret,其中 appsecret 从 AppCenter 后台获取,具体查看开发指南手册获取 Harmony 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 代码中获取
TopApp
的activityPage
,获得最新的页面,通过页面调用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)
代码示例如下:
在
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" }
下载 amr 并命名为
${appid}_${version}.amr
。调用 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}`
})
配置公共离线包
设置
H5CommonAppProvider
。import { HRiver, H5CommonAppProvider } from '@mpaas/hriver' /** * 配置公共离线包 Provider,必须在 HRiver.init() 之前调用 */ HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
说明setProvider
必须在HRiver.init()
之前调用。实现
H5AppCommonProviderImpl.ets
的getCommonResourceAppList
方法。import { HRiver, H5CommonAppProvider } from '@mpaas/hriver' export class H5AppCommonProviderImpl extends H5CommonAppProvider { getCommonResourceAppList(): Array<string> { return ['20220719'] // 返回公共离线包id列表 } }
设置 UserAgent
import { HRiver } from '@mpaas/hriver'
HRiver.setUserAgent(userAgent)
setUserAgent
在 HRiver.init()
之后调用,会在默认 UserAgent
之后拼接设置的 useragent
。
设置离线包默认更新频率
设置
H5CommonAppProvider
。import { HRiver, H5CommonAppProvider } from '@mpaas/hriver' /** * 配置公共离线包Provider、离线包更新频率等功能,必须在HRiver.init()之前调用 */ HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
说明setProvider
必须在HRiver.init()
之前调用。实现
H5CommonAppProvider
的configJSON
方法,代码示例如下。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
的值即可。
配置离线包签名校验
设置
H5CommonAppProvider
。import { HRiver, H5CommonAppProvider } from '@mpaas/hriver' /** * 配置公共离线包Provider、离线包更新频率、签名校验等功能,必须在HRiver.init()之前调用 */ HRiver.setProvider(H5CommonAppProvider.name, new H5AppCommonProviderImpl())
说明setProvider
必须在HRiver.init()
之前调用。实现
H5CommonAppProvider
的shouldVerify
和pubKey
方法,代码示例如下: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 {HRBuilder, RouterUtils, HRiver, H5CommonAppProvider, H5RouterNavStackProvider, CustomUIBuilderProvider, H5CacheProvider } from '@mpaas/hriver' HRiver.setProvider(CustomUIBuilderProvider.name, new CustomUIBuilderProviderImpl())
实现
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") } }
页面路由支持 Navigation
默认页面路由使用 router 方式,鸿蒙 router 方式不支持关闭栈中某个页面,只能一级级回退。离线包支持 Navigation 模式:
支持关闭栈中页面
支持分栏模式
全局 Navigation 模式
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 } }
在
navDestination
中配置builder
,builder
中加载 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 模式
HRiver.startApp
/HRiver.startUrl
的第三个参数传入 NavPathStack 即可。参考 全局 Navigation 模式 中的步骤 2。