From 70a077165324f19452308513dfaa3b86e4e3cb2f Mon Sep 17 00:00:00 2001 From: heweishui Date: Mon, 21 Oct 2024 14:26:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0harmony-hybrid?= =?UTF-8?q?=E6=8C=87=E5=AF=BC=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/harmony-hybrid/index.md | 980 ++++++++++-------- .../version-3.x/harmony-hybrid/index.md | 979 +++++++++-------- 2 files changed, 1074 insertions(+), 885 deletions(-) diff --git a/docs/harmony-hybrid/index.md b/docs/harmony-hybrid/index.md index 1c053a468d21..3797c58438a8 100644 --- a/docs/harmony-hybrid/index.md +++ b/docs/harmony-hybrid/index.md @@ -1,5 +1,6 @@ --- title: Harmony Hybrid + --- :::info @@ -8,7 +9,7 @@ Taro v3.6.24+ 开始支持 ## 简介 -Taro Harmony Hybrid容器是为让Taro小程序代码可以完整的运行在鸿蒙单内核系统里,在Taro H5平台的基础上,基于原生壳工程的jsbridge能力,扩展H5平台不支持的小程序Api能力,让Taro小程序0成本运行在鸿蒙系统。 +Taro Harmony Hybrid容器是为让Taro小程序代码可以完整的运行在鸿蒙单内核系统里,在Taro H5平台的基础上,基于原生壳工程的JSBridge能力,扩展H5平台不支持的小程序Api能力,让Taro小程序0成本运行在鸿蒙系统。 此方案不同于Taro的Harmony原生方案,其运行时还是Webview,重点解决Api切换的问题。 @@ -56,6 +57,15 @@ Options: 通过 Taro CLI 工具基于`默认模板`创建 Taro 工程,使用如下编译命令生成 harmony-hybrid 平台的 Web 资源: ```shell +# 项目根目录下安装依赖 +# yarn +$ yarn install +# npm script +$ npm install +# pnpm script +$ pnpm install + +# 编译项目 # yarn $ yarn build:harmony-hybrid # npm script @@ -74,6 +84,7 @@ Taro `harmony-hybrid` 平台产物需要结合鸿蒙壳工程一起编译运行 - 使用DevEco Studio开发工具( >= 5.0.3.100 )新建应用工程,选择Empty Ability模板,API >= 11, 其他配置使用默认值。 - 在 `entry/oh-package.json5` 文件中添加 `@hybrid/web-container` 模块的依赖并点击Sync进行同步: + ```json { "license": "ISC", @@ -82,39 +93,99 @@ Taro `harmony-hybrid` 平台产物需要结合鸿蒙壳工程一起编译运行 "description": "演示如何使用TaroWebContainer组件", "version": "1.0.0", "dependencies": { - "@hybrid/web-container": "2.0.0-rc.1" + "@hybrid/web-container": "2.0.0-rc.7" } } ``` -- 使用 `@hybrid/web-container` -```typescript -import { TaroWebContainer } from '@hybrid/web-container'; -``` + 具体使用方式请参考下个章节:集成到现有鸿蒙工程 - 最后点击菜单 `Build` -- `Rebuild Project` 编译项目。 ## 集成到现有鸿蒙工程 +### 初始化 + +在src/main/ets/entryability/EntryAbility.ets中的 windowStage.loadContent里初始化和预加载,扩展原生Api可参考进阶教程中部分 + +```ts +import { TaroHybridManager, InjectObject } from '@hybrid/web-container' +import router from '@ohos.router'; + +const nativeObj: InjectObject={} + +// 在windowStage.loadContent中初始化项目 +windowStage.loadContent('pages/Index', (err) => { + // 全局初始化 + TaroHybridManager.init({ + uiAbilityContext: this.context, + domain: 'https://customer.domain.com', // 小程序的域名,注意:此处不添加/结尾 + injectNativeMethod: (indexHtmlPath: string,uiAbilityContext: common.UIAbilityContext) => { // 扩展原生API + return nativeObj + }, + rootIndexHtmlPath: "/index.html", + nativeBack: ()=>{ + router.back() + }, + forceDarkAccess: true, + windowClass: windowStage.getMainWindowSync(), + rawFileName: 'spa', + diskCacheEnable: true + }) + // 预加载,可选,不是必须 + TaroHybridManager.preLoader( + windowStage.getMainWindowSync().getUIContext(), + "/xx/index.html", // html的path路由 + "/pages/index/index" // Taro小程序的Page路径 + ) +}) +``` + +**TaroHybridManager.init 参数说明:** + +init方法的参数类型为 `TaroHybridManagerOptions`, 各字段说明如下: + +| 字段名称 | 类型 | 描述 | 必填 | +| ------------------ | ------------------------------------------ | ------------------------------------------------ | ---- | +| uiAbilityContext | UIAbilityContext | UIAbility的上下文对象 | 是 | +| windowClass | Window | 主窗口 | 是 | +| injectNativeMethod | (string, UIAbilityContext) => InjectObject | 注入对象生成函数 | 否 | +| nativeBack | () => void | 多容器场景回退自定义函数,只作用于TaroHybrid组件 | 否 | +| rootIndexHtmlPath | string | 主资源路径,只作用于TaroHybrid组件 | 否 | +| forceDarkAccess | boolean | 是否强制反色适配深色模式,只作用于TaroHybrid组件 | 否 | +| domain | string | 小程序的域名,此处不添加/结尾 | 否 | +| rawFileName | string | 小程序的bundle目录 | 否 | +| diskCacheEnable | boolean | 小程序磁盘是否缓存,默认true | 否 | + ### 简单集成(TaroWebContainer) `TaroWebContainer` 组件提供加载单页面 Web 应用能力,并提供部分 Taro API 鸿蒙版本。 **使用方法:** + +在src/main/ets/pages/Index.ets中使用TaroWebContainer组件拉起taro项目 + ```typescript -// entry/src/main/ets/pages/Index.ets import Want from '@ohos.app.ability.Want'; import Url from '@ohos.url'; -import { TaroWebContainer, InjectObject, HostPageState, TaroWebController, Logger, LoggerLevel, wbLogger } from '@hybrid/web-container'; -const SAMPLE_INDEX_TAG = 'SampleIndex'; +import { TaroWebContainer, InjectObject, HostPageState, TaroWebController } from '@hybrid/web-container'; + +const WEB_CONTAINER_PAGE_TAG = 'WebContainerPage'; +let storage = LocalStorage.getShared() // 获取共享的本地存储对象 +const TAG = 'WebContainerPage'; + +@Entry(storage) @Component -struct TaroMpharmonySample { +struct WebContainerPage { + @LocalStorageProp('want') want: Want = {}; @State pageState: HostPageState = HostPageState.PageInit; @State taroWebController: TaroWebController = new TaroWebController(); + // 用户可以自定义对象注入到Web环境中,使用native.sayHello格式进行调用 nativeObj: InjectObject = { - sayHello: () => console.log('Hello World'), + sayHello: () => console.log(TAG, 'sayHello %{public}s', 'Hello World'), } + onBackPress() { if (this.taroWebController.accessBackward()) { this.taroWebController.backward(); @@ -122,39 +193,56 @@ struct TaroMpharmonySample { } return false; } - aboutToAppear() { - Logger.setLogLevel(LoggerLevel.LOG_DEBUG); - } + onPageShow() { this.pageState = HostPageState.PageOnShow; } + onPageHide() { this.pageState = HostPageState.PageOnHide; } + webUrl(): string { // 开发阶段可以把网站静态资源文件放置到src/main/resources/rawfile/目录下 - // 生产环境下可以把部分网页静态资源放置到web服务器, 这里填写实际的网站地址url + // 生产环境下把网站静态资源放置到web服务器, 这里填写实际的网站地址url return 'resource://rawfile/index.html'; } + webUrlPrefix() { try { - const url = Url.URL.parseURL(this.webUrl()); - return `${url.protocol}//${url.host}/`; + const url = Url.URL.parseURL(this.webUrl()); + return `${url.protocol}//${url.host}/`; } catch (err) { - wbLogger.error(SAMPLE_INDEX_TAG, `Invalid webUrl: ${this.webUrl()}`); - return ''; + console.error(WEB_CONTAINER_PAGE_TAG, `Invalid webUrl: ${this.webUrl()}`); + return ''; } } + + getUrl(value: LoadCommittedDetails) { + console.log('LoadCommittedDetails:', JSON.stringify(value)) + } + build() { Column() { TaroWebContainer({ + indexHtmlPath: 'index.html', pageState: this.pageState, // 页面状态同步到组件 webUrl: this.webUrl(), // 初始Url webUrlPrefix: this.webUrlPrefix(), - useCache: true, + useCache: false, + want: this.want, // want信息 taroWebController: this.taroWebController, - isFullScreen: true, // 是否全屏显示 + isFullScreen: false, // 是否全屏显示 injectObj: this.nativeObj, // 注入对象 + navigationInitVisible: true, // 导航栏是否显示 + showCapsule:true, + capsulePage:'index.html', + enableWebDebug:true, + forceDarkAccess: true, + userAgent: '', + getLoadCommittedDetails: (value: LoadCommittedDetails) => { + this.getUrl(value) + } }) .width('100%') .height('100%') @@ -165,59 +253,89 @@ struct TaroMpharmonySample { **构造参数说明:** -| 参数名称 | 类型 | 描述 | 必填 | -|-------------------|-------------------|-------------------------------------------------------|-----------------------| -| taroWebController | TaroWebController | TaroWebContainer组件的控制器 | 是 | -| webUrl | string | 资源入口url | 是 | -| webUrlPrefix | string | 资源入口url的前缀,一般是 `${webUrl.protocol}://${webUrl.host}/` | 是 | -| pageState | HostPageState | 传递页面状态 | 是 | -| useCache | boolean | 是否优先使用应用内置的Web资源 | 否,默认值: true | -| want | Want | 传递EntryAbility中`onCreate`和`onNewWant`保存的want信息 | 否,默认值: { } | -| isFullScreen | boolean | 是否全屏显示应用 | 否,默认值: true | -| injectObj | ESObject | 注入ets对象到Web环境 | 否:默认值:undefined | -| showCapsule | boolean | 是否显示胶囊按钮 | 否:默认值:true | -| capsulePage | string | 点击胶囊按钮跳转的页面 | 否:默认值:`pages/Capsule` | -| enableWebDebug | boolean | [开启Web调试功能][Web调试devtools配置] | 否:默认值:true | -| navigationInitVisible | boolean | 控制导航栏初始显示状态 | 否:默认值:true | +| 参数名称 | 类型 | 描述 | 必填 | +| ----------------------- | ----------------- | ------------------------------------------------------------ | --------------------------- | +| taroWebController | TaroWebController | TaroWebContainer组件的控制器 | 是 | +| webUrl | string | 资源入口url | 是 | +| webUrlPrefix | string | 资源入口url的前缀,一般是 `${webUrl.protocol}://${webUrl.host}/` | 是 | +| pageState | HostPageState | 传递页面状态 | 是 | +| useCache | boolean | 是否优先使用应用内置的Web资源 | 否,默认值: true | +| want | Want | 传递EntryAbility中`onCreate`和`onNewWant`保存的want信息 | 否,默认值: { } | +| isFullScreen | boolean | 是否全屏显示应用 | 否,默认值: true | +| injectObj | ESObject | 注入ets对象到Web环境 | 否:默认值:undefined | +| showCapsule | boolean | 是否显示胶囊按钮 | 否:默认值:true | +| capsulePage | string | 点击胶囊按钮跳转的页面 | 否:默认值:`pages/Capsule` | +| enableWebDebug | boolean | [开启Web调试功能][Web调试devtools配置] | 否:默认值:true | +| navigationInitVisible | boolean | 控制导航栏初始显示状态 | 否:默认值:true | +| userAgent | string | 自定义用户代理 | 否 | +| getLoadCommittedDetails | Function | 网页跳转时触发该回调方法 | 否 | +| forceDarkAccess | boolean | 网页强制开启深色模式 | 否:默认值:false | + ### 多容器和容器共用集成(TaroHybrid) +`TaroHybrid` 同`TaroWebContainer`组件功能相似,提供加载单页面 Web 应用能力,并提供部分 Taro API 鸿蒙版本,主要用于项目中有多个webview的场景。 + **使用方法:** + +1.在src/main/ets/pages/SafeArea.ets中定义一个顶部导航栏避让的组件 + +```ts +@Component +export struct SafeArea { + @StorageProp('topRectHeight') topRectHeight: string = '0px' + @BuilderParam child: ()=>void + + build() { + Column() { + Row() + .width('100%') + .height(this.topRectHeight) + .backgroundColor(0xFFFFFF) + Column(){ + this.child() + } + .width('100%') + .layoutWeight(1) + } + .width('100%') + .height('100%') + } +} +``` + +2.在src/main/ets/pages/TaroHybridPage.ets中使用TaroHybrid组件拉起taro项目 + ```typescript -// windowStage.loadContent里初始化 和 预加载 -windowStage.loadContent('xxx', storage, (err, data) => { - // 全局初始化 - TaroHybridManager.init({ - uiAbilityContex: this.context, - domain: 'https://customer.domain.com', // 小程序的域名,注意:此处不添加/结尾 - injectNativeMethod: (uiAbilityContext: common.UIAbilityContext) => { // 扩展原生API - ... - } - }) - // 预加载,可选,不是必须 - TaroHybridManager.preLoader( - windowStage.getMainWindowSync().getUIContext(), - "/xx/index.html", // html的path路由 - "/pages/index/index" // Taro小程序的Page路径 - ) -}) +import { HostPageState, TaroHybrid, TaroHybridManager } from '@hybrid/web-container/Index'; +import router from '@ohos.router'; +import { SafeArea } from './SafeArea'; -// 创建TaroHybrid页面(例子是基于Entry实现) +const TAG = 'TaroHybridPage' + +// 创建TaroHybridPage页面(例子是基于Entry实现) export interface TaroHybridRouterParams { indexHtmlPath: string, taroPath: string } export const RouterToTaroHybrid = (params: TaroHybridRouterParams) => { - // 跳转之前先加载url,目的是为了提升新页面的打开速度 - TaroHybridManager.loadUrl(params.indexHtmlPath, params.taroPath); - + // 跳转之前先加载url,目的是为了提升新页面的打开速度 + const result = TaroHybridManager.loadUrl(params.indexHtmlPath, params.taroPath); + if (result) { + // 容器复用成功,等待loadUrl,不然会出现先看到上一个页面 setTimeout(()=>{ - router.pushUrl({ - url: 'xxx/TaroHybridPage', // 鸿蒙原生Page的路径,不是Taro小程序的Page - params: params - }, router.RouterMode.Standard) - }, 200) + router.pushUrl({ + url: 'pages/TaroHybridPage', + params: params + }, router.RouterMode.Standard) + }, 300) + } else { + router.pushUrl({ + url: 'pages/TaroHybridPage', + params: params + }, router.RouterMode.Standard) + } } @Entry @@ -226,6 +344,13 @@ struct TaroHybridPage { @State pageState: HostPageState = HostPageState.PageInit; private indexHtmlPath: string = (router.getParams() as TaroHybridRouterParams).indexHtmlPath private taroPath: string = (router.getParams() as TaroHybridRouterParams).taroPath + + aboutToAppear(): void { + // 设置UserAgent + TaroHybridManager.setCustomUserAgent('') + // initLoadCommittedDetails(indexHtmlPath,callBack()) web页面跳转时触发callBack() + TaroHybridManager.initLoadCommittedDetails(this.indexHtmlPath,(value: LoadCommittedDetails): void => {}) + } onPageShow(): void { this.pageState = HostPageState.PageOnShow @@ -238,7 +363,9 @@ struct TaroHybridPage { if (!instance) { return false; } - return TaroHybridManager.onBack(instance.builderData.taroWebController) + return TaroHybridManager.onBack( + instance.builderData.indexHtmlPath, + instance.builderData.taroPath) } build() { @@ -256,27 +383,67 @@ struct TaroHybridPage { ``` +3.在src/main/ets/pages/Index.ets中增加一个跳转按钮,可跳转到TaroHybridPage页面 + +```ts +import { RouterToTaroHybrid } from './TaroHybridPage'; +import { SafeArea } from './SafeArea'; + +let storage = LocalStorage.getShared() // 获取共享的本地存储对象 + +@Entry(storage) +@Component +struct Index { + build() { + Stack({ alignContent: Alignment.TopStart }) { + SafeArea() { + Column({ space: 30 }) { + Button('TaroHybrid例子', { type: ButtonType.Normal, stateEffect: true }) + .borderRadius(8) + .backgroundColor(0x317aff) + .height(40) + .onClick(() => { + RouterToTaroHybrid({ + indexHtmlPath: "/spa/main/index.html", + taroPath: "/pages/index/index" + }) + }) + } + .width('100%') + .height('100%') + .alignItems(HorizontalAlign.Center) + } + } + } +} + +``` + **TaroHybrid的设计思路:** + 1. 多容器的判断依据:html的Path路径为判断依赖,相同则共用,不同则新建载体页 2. 容器共用的思路:通过鸿蒙的NodeContainer + NodeController实现 -3. 注意: - 1. 容器共用存在一个问题:相邻两个原生Page之间如果共用容器,页面切换动画时,会有一个页面白屏,进入和退出时都会出现,尽量避免相邻两个原生Page之间共用容器。 +3. 注意:容器共用存在一个问题:相邻两个原生Page之间如果共用容器,页面切换动画时,会有一个页面白屏,进入和退出时都会出现,尽量避免相邻两个原生Page之间共用容器。 ## 进阶教程 ### 混合路由 +#### **实现方式** + 在原生与TaroHybrid混合鸿蒙应用中,如果使用了容器共用,用户的路由栈会比较复杂,当在Taro Hybrid页面时,用户使用原生的物理返回时,需要区分是Web容器的返回,还是原生的返回。 TaroHybrid组件已经解决了此问题,其思路为: -1. 原生跳转打开的taro页面,添加query参数from=native -2. 原生的onBackPress逻辑里,获取当前url,判断有没有参数from=native,如果有则走原生路由返回,如果没有则走Web组件的backward逻辑 + +1. 原生跳转打开的taro页面,添加query参数tarofrom=native +2. 原生的onBackPress逻辑里,获取当前url,判断有没有参数tarofrom=native,如果有则走原生路由返回,如果没有则走Web组件的backward逻辑 ```typescript +// @hybrid/web-container 内部已实现 static onBack(taroWebController: TaroWebController): boolean { let curUrl = taroWebController.webController.getUrl(); - if (curUrl?.endsWith('?from=native')) { //web回退到原生 + if (curUrl?.endsWith('?tarofrom=native')) { //web回退到原生 // Web先返回 if (taroWebController.accessBackward()) { setTimeout(()=>{ @@ -296,10 +463,76 @@ static onBack(taroWebController: TaroWebController): boolean { } ``` +#### **使用方式** + +1. TaroWebContainer组件设置混合路由返回方式 + + ```tsx + ... + struct WebContainerPage { + // 判断原生返回或taro返回 + onBackPress() { + if (this.taroWebController.accessBackward()) { + this.taroWebController.backward(); + return true; + } + return false; + } + ... + + build() { + Column() { + TaroWebContainer({...}) + .width('100%') + .height('100%') + } + } + } + ``` + +2. TaroHybrid组件设置混合路由返回方式 + + ```tsx + ... + + @Entry + @Component + struct TaroHybridPage { + ...... + + // 通过indexHtmlPath区分webview容器,设置返回方式 + onBackPress(): boolean | void { + const instance = TaroHybridManager.getCoreInstance(this.indexHtmlPath) + if (!instance) { + return false; + } + return TaroHybridManager.onBack( + instance.builderData.indexHtmlPath, + instance.builderData.taroPath) + } + + build() { + Stack({alignContent: Alignment.TopStart}){ + SafeArea(){ + TaroHybrid({ + ... + }) + } + } + } + } + ``` + + + ### 小程序内置及热更新 + #### 内置及热更新的使用方式 + ##### 小程序内置规则 + 在业务较为复杂的场景时,项目通常分为多个`bundle`,内置到应用时遵循以下目录结构规则: + ``` └──rawfile # 应用rawfile目录 └──spa # 多bundle内置目录,支持配置 @@ -318,150 +551,66 @@ static onBack(taroWebController: TaroWebController): boolean { ├──static # 静态资源目录 └──index.html # 首页文件 ``` -以上目录结构的解释说明 -1. rawfile目录下的spa,为多bundle的内置目录,容器会在此目录下读取bundle加载,支持以下方式配置(该目录要与配置一致): -```typescript -GlobalThis.getInstance().setRawFile('spa') -``` -2. bundle包名称:`一级目录`+`_`+`二级目录`+`@`+`VersionCode`,例如:spa_main@100000,以`@`符分割为两部分: -2.1 `@`符分前置: 为bundle的打包目录层级,一般为index.html(首页文件)的path路径,用下划线`_`分割,作用是为内置文件路径拦截匹配查找; -2.2 VersionCode: 为当前bundle版本号,作用为版本控制及磁盘热更新 -##### 小程序磁盘缓存 -1. 读取磁盘缓存,磁盘更新操作入口(如下),读取应用磁盘`${context.filesDir}/update`下bundle缓存版本信息: -```typescript -LocalUpdateManager.updateMiniOnLaunch() -``` -2. 内置更新缓存至磁盘,若磁盘缓存bundle版本 < rawfile内置版本,则将内置bundle更新至磁盘,具体实现参见以下方法: -```typescript -LocalUpdateManager.updateAllMini() -``` -3. 更新至磁盘的bundle在容器加载时会被自动查找加载; -4. 磁盘缓存默认开启,支持以下方式关闭: -```typescript -GlobalThis.getInstance().setDiskUpdateEnable(false) -``` -##### 热更新 -目前正在开发阶段,暂不支持。 -#### 整体方案简介 - -动态下开发台吗,在不发布新版本的情况下修复`bug`和发布新功能,绕开应用商店的审核机制,避免长时间审核以及多次被拒绝造成的成本问题,缩短用户取得新版本的流程,改善用户体验。 - -#### 具体实现 - -##### 1、资源拦截并加载本地资源 - -在业务较为复杂的场景时,项目通常分为多个`bundle`,热更新模块能够支持更新单独某个`bundle`服务。在`web`组件的`onInterceptRequest`中,可以进行资源拦截。通过`url`解析出`path`后,通过`path`获取对应的`bundle`信息,进而构建相应的响应返回给`web`组件。`MiniRawFileDataSource.getBizByPath`可以获取本地缓存中的`bundle`信息,`MiniLocalFileDataSource.fileExist`可以根据位置判断资源是否存在。 - -```ts -path = path.split('#')[0] -wbLogger.debug('TaroWeb', 'intercepter path=%{public}s', path) -let biz = MiniRawFileDataSource.getBizByPath(path) //获取业务标识 -const lastVerInfo = LocalUpdateManagerInstance.getLastVersionInfo(biz) -if (lastVerInfo?.diskCachePath) { // 走本地缓存 - if (!path || !MiniLocalFileDataSource.fileExist(lastVerInfo?.diskCachePath, path)) { - return null; - } - wbLogger.debug('TaroWeb', 'useLocalCacheResource path=%{public}s', path) - return MiniLocalFileDataSource.getFileOnResponseWeb(lastVerInfo?.diskCachePath, path) - } -``` -##### 2、入口函数--`updateMiniOnLaunch` +以上目录结构的解释说明 -入口函数`updateMiniOnLaunch`接收一个`context`,首先初始化了基类`UpdateManager`,然后获取了`rawfile`目录下的文件内容,并传入到了`updateAllMini`中进行逻辑处理。 +1. rawfile目录下的spa,为多bundle的内置目录,容器会在此目录下读取bundle加载,支持以下方式配置(该目录要与配置一致): -```ts -updateMiniOnLaunch(context: AppContext.UIAbilityContext) { - this.reset(context) - wbLogger.debug('LocalUpdateManager',`update start`); - this.updateAllMini(context,this.miniRepository.getMiniRawList(context)).then((result: Array) => { - if (result) { - wbLogger.debug('LocalUpdateManager',`update success ${result}`); - } - }).catch((error: Error) => { - wbLogger.error('LocalUpdateManager','update Error :', error); +```typescript +// src/main/ets/entryability/EntryAbility.ets文件中写入 +windowStage.loadContent('pages/Index', (err) => { + // 全局初始化 + TaroHybridManager.init({ + ...... + rawFileName: 'spa', + diskCacheEnable: true }) -} ``` +2. bundle包名称:`一级目录`+`_`+`二级目录`+`@`+`VersionCode`,例如:spa_main@100000,以`@`符分割为两部分: + 2.1 `@`符分前置: 为bundle的打包目录层级,一般为index.html(首页文件)的path路径,用下划线`_`分割,作用是为内置文件路径拦截匹配查找; + 2.2 VersionCode: 为当前bundle版本号,作用为版本控制及磁盘热更新 +#### 热更新 -##### 3、获取`rawfile`文件信息--`getMiniRawList` +##### 整体方案简介 -从全局存储中拿出初始化时存储的`rawfile`文件名,根据该文件名称,获取该文件夹下所有的文件信息。 +动态下开发代码,在不发布新版本的情况下修复`bug`和发布新功能,绕开应用商店的审核机制,避免长时间审核以及多次被拒绝造成的成本问题,缩短用户取得新版本的流程,改善用户体验。 -```ts -getRawMiniList(context: common.Context): Array{ - let rawFile = GlobalThis.getInstance().getRawFile(); - return context.resourceManager.getRawFileListSync(rawFile); -} -``` +##### 使用说明 -##### 4、更新所有的资源包信息---`updateAllMini` +###### `arkts`端配置 -该函数接收一个`context`和`rawfile`文件信息,返回一个`Promise`对象。通过`rawfile`文件信息,构建`LocalVersionInfo`实例对象,该对象存储了对应文件的版本号、业务标识`biz`, 以及`rawfile`路径,将其存储到`UpdateConfig`单例的`Map`对象中,这样在资源拦截时可以从该`Map`中通过相应的业务标识获取到对应资源的信息。`rawfile`目录下的资源可能不是最新的,需要与本地缓存进行对比,如果本地缓存中没有任何资源,则需要将`rawfile`的资源复制到本地沙箱相应位置中,这样可以通过字节码优化,能提高性能。由于`rawfile`文件复制到了沙箱,这样沙箱资源一直都是最新的,所以需要更新存储在`UpdateConfig`单例的`Map`对象的资源信息。 +热更新配置分为三步,首先需要配置`rawfile`内资源顶层文件夹名称,以壳工程资源为例`rawfile/spa/`,需要配置`spa`名称;其次,需要配置云端资源信息的`url`生成函数,这样当资源需要更新时可以从该地址获取相应信息; -```ts - async updateAllMini(context: AppContext.UIAbilityContext,miniList:Array) : Promise>{ - return new Promise((resolve, reject) => { - let miniArray = new Array() - try { - if (miniList) { - for (let index = 0; index < miniList.length; index++) { - const element = miniList[index]; - let splits = FileUtils.checkMiniFileNameFormat(element) - let miniInfo: LocalVersionInfo | null = null - if (splits) { - miniInfo = new LocalVersionInfo(splits[1], splits[0],'',this.miniRepository.getRawCachePath(element)) - UpdateConfig.getInstance().setOrReplaceMiniInfo(miniInfo.biz, miniInfo) - } - if (miniInfo) { - if (GlobalThis.getInstance().getDiskUpdateEnable()) { - this.updateDiskSingleMini(context, miniInfo) - } - wbLogger.debug('LocalUpdateManager',`VersionInfo miniInfo:${miniInfo.localVersion},biz:${miniInfo.biz},diskCachePath:${miniInfo.diskCachePath},rawCachePath:${miniInfo.rawCachePath}`); - miniArray.push(miniInfo) - } - } - //TODO-JasonZhang 热更新接口请求 - } - resolve(miniArray) - } catch (err) { - reject(err) - } - }) - } - updateDiskSingleMini(context: AppContext.UIAbilityContext,versionInfo:LocalVersionInfo){ - // 获取本地最新的版本号 - let localVersionInfo = this.miniRepository.readDiskLastMiniBizInfo(context,versionInfo.biz) - // 本地无内置版本,则迁移RawFile小程序资源到磁盘缓存目录为初始版本 - if (localVersionInfo && (localVersionInfo?.diskCachePath==''|| localVersionInfo?.localVersion < versionInfo.localVersion)) { - let result = this.miniRepository.copyMiniRawUnzipDisk(context,versionInfo) - if (result) { - versionInfo.localVersion = result.localVersion - versionInfo.diskCachePath = result.diskCachePath - } - wbLogger.debug('LocalUpdateManager',`copied success ${result}`); +```tsx +// 在src/main/ets/entryability/EntryAbility.ets中 +import { TaroHybridManager,LocalMiniInfo,NetworkMiniInfo } from '@hybrid/web-container'; + +// 请求更新版本的信息和下载路径 +function getNeedUpdateNetworkInfos(localVersions: LocalMiniInfo[]): Promise { + return new Promise((resolve)=>{ + // 使用接口请求服务端的版本信息 NetworkMiniInfo + let httpRequest = http.createHttp() + httpRequest.request("http://www.xxx.com",(error:Error,data:http.HttpResponse)=>{ + if(data.result){ + let NetworkMiniInfos:NetworkMiniInfo[]=data.result as NetworkMiniInfo[] + resolve(NetworkMiniInfos) }else{ - versionInfo.localVersion = localVersionInfo.localVersion - versionInfo.diskCachePath = localVersionInfo.diskCachePath + console.error(`getNeedUpdateNetworkInfos fail! errMsg ${error}`) } - } -``` -#### 使用说明 - -##### `arkts`端配置 - -热更新配置分为三步,首先需要配置`rawfile`内资源顶层文件夹名称,以壳工程资源为例`rawfile/spa/`,需要配置`spa`名称;其次,需要配置云端资源信息的`url`生成函数,这样当资源需要更新时可以从该地址获取相应信息;最后,启动热更新模块。 + }) + }) +} -```tsx -onCreate(want: Want) { - // ... other code - GlobalThis.getInstance().setRawFile('spa') - UpdateConfig.getInstance(). - LocalUpdateManagerInstance.updateMiniOnLaunch(this.context) - } +export default class EntryAbility extends UIAbility { + onCreate(want: Want) { + // ... other code + TaroHybridManager.startUpdateDiskCacheFromNetwork(this.context.getApplicationContext(), (localVersions:LocalMiniInfo[])=>getNeedUpdateNetworkInfos(localVersions)) + } + ......other code +} ``` ###### 1、配置位置 @@ -470,76 +619,18 @@ onCreate(want: Want) { ###### 2、导入包 -需要导入`LocalUpdateManagerInstance`实例,以及配置热更新模块的对象`UpdateConfig` +需要导入`TaroHybridManager`实例,以及配置热更新模块的参数类型`LocalMiniInfo, NetworkMiniInfo` ``` tsx -import {LocalUpdateManagerInstance, UpdateConfig} from '@ohos/web-container' +import { TaroHybridManager, LocalMiniInfo, NetworkMiniInfo } from '@hybrid/web-container'; ``` ###### 3、热更新初始配置 -在执行入口函数前,需要对热更新模块进行配置初始化。`UpdateConfig`对象可以配置四个参数,**第一个参数和第二个参数为必须配置参数,第三个和第四个参数为选配**。其中第一个参数为最新离线资源配置的地址,需要用户按照字段配置好;第二个参数为初始的版本号(可以是`rawfile`路径下资源的版本),该版本号将会与第一个参数进行拼接,形成完成的`url`。原生端根据形成的`url`发起请求以获取服务端离线资源配置信息,服务端依据相关规则将字段返回,分两种情况,**需要更新离线资源和本地资源是最新的**。**后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** +在执行入口函数前,需要对热更新模块进行配置初始化。`TaroHybridManager.startUpdateDiskCacheFromNetwork`方法接受2个参数*。其中第一个参数为应用的context,可以直接调用this.context.getApplicationContext()获取;第二个参数为一个方法,该方法接收 一个参数,为本地热更新资源的信息,返回通过网络请求获取服务端要更新的资源信息。 ```tsx -const updateConfig = new UpdateConfig('http://127.0.0.1:8080:/apis/config?local_version=', '0') -LocalUpdateManagerInstance.setUpdateConfig(updateConfig) -``` - -**1、需要更新离线资源** - -假设初始版本号为`0`,服务端的最新离线资源版本为`1`,则需要更新。服务端收到原生端的请求后,获取`local_version`字段进行对比,根据相关规则将字段传回原生端。服务端配置字段可以参照如下所示: - -```json -{"code": 200, "message": "request OK", "data": {"mini_download_url": "http://127.0.0.1/mpharmony/mpharmony.zip", "mini_version": 1}} -``` - -服务端需要配置上述四个字段,其中,`data`字段中的`mini_download_url`字段表示需要下载离线资源路径,热更新模块依据这个路径进行下载最新配置。 - -**注意:这里的`0`只是用例,后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** - -**2、本地资源是最新的** - -假设初始版本号为`1`,服务端的最新离线资源版本为`1`,则不需要更新。服务端收到原生端的请求后,获取`local_version`字段进行对比,根据相关规则将字段传回原生端,服务端配置字段可以参照如下所示: - -```json -{"code": "300", "message": "latest version"} -``` - -**注意:这里的`1`只是用例,后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** - -###### 4、排序算法配置(选配) - -此外,热更新模块会保留两个历史版本,会在下次启动时删除多余版本。此时需要对离线版本进行排序,默认支持的排序算法是`2.1.0 < 11.0.1`。用户可以自己实现排序算法用以适配不同的版本号,伪代码如下所示: - -```ts -/** - * - * @param v1 - * @param v2 - * @returns {boolean} is v1 bigger than v2 - */ - -function versionCompare(v1: string, v2: string) { - if (v1 > v2){ - return true - } - return false -} -``` - -热更新模块配置如下所示: - -```tsx -const updateConfig = new UpdateConfig('http://127.0.0.1:8080:/apis/config?local_version=', '0', true, versionCompare) -LocalUpdateManagerInstance.setUpdateConfig(updateConfig) -``` - -第三个参数为是否开启自定义排序算法配置,默认为`false`,`false`会执行默认的排序算法 - -4、启动,启动热更新模块入口函数时需要传递当前页面的上下文,下载的离线资源会依据上下文存储。 - -```tsx -LocalUpdateManagerInstance.updateMiniOnLaunch(this.context) + TaroHybridManager.startUpdateDiskCacheFromNetwork(this.context.getApplicationContext(),(localVersions:LocalMiniInfo[])=>getNeedUpdateNetworkInfos(localVersions)) ``` ##### `js`端配置 @@ -587,12 +678,13 @@ updateManager.onUpdateFailed(function listener(){ 示例代码如下: ```typescript +// src/main/ets/pages/TaroMethods.ets中定义对象和方法 +import { InjectObject } from '@hybrid/web-container/Index'; interface LoginOptions { username: string password: string success: (t: string | null | undefined) => void - error: (error: BusinessError) => void } export const nativeObj:InjectObject = { @@ -606,12 +698,14 @@ export const nativeObj:InjectObject = { ##### 初始化注入InjectObject ```typescript +// src/main/ets/entryability/EntryAbility.ets +import nativeObj from '../pages/TaroMethods' TaroHybridManager.init({ uiAbilityContex: this.context, domain: 'https://xxx.xxx.com', // 注意:此处不添加/结尾 - injectNativeMethod: (uiAbilityContext: common.UIAbilityContext) => { + injectNativeMethod: (indexHtmlPath: string, uiAbilityContext: common.UIAbilityContext) => { return nativeObj - } + }, }) ``` @@ -651,7 +745,6 @@ autoRelease:如果option中有回调方法且回调方法可能会调用多次 ##### 方法调用 ```typescript - import { native } from './NativeApi'; native.customLogin({ @@ -674,10 +767,12 @@ native.customLogin({ 默认情况下,设计尺寸是750px,Taro适配不同手机分辨率的方式是等比缩放,在鸿蒙折叠屏展开的状态下,等比放大的效果不满足华为应用商店上架要求。 **建议的适配方案:** + 1. 最大放大比例不超过1.2倍 2. 宽度不基于750px来布局,基于flex自适应 最大放大比例不超过1.1倍的配置: + ```javascript // 修改config/index.js export default defineConfig(async (merge, { command, mode }) => { @@ -695,7 +790,8 @@ export default defineConfig(async (merge, { command, mode }) => { } }, // .... - } + } + }, // .... } @@ -710,13 +806,26 @@ export default defineConfig(async (merge, { command, mode }) => { 建议使用“窗口全屏布局方案”,默认全屏模式,再处理状态栏及导航条的避让。 -**全屏模式下的状态样的避让:** 通过原生创建一个SafeArea组件实现,而不通过JS实现 -```typescript -// 在UIAbility里,全局保存状态栏的安全高度 -const systemAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); -const topRectHeight = systemAvoidArea.topRect.height -AppStorage.setOrCreate('topRectHeight', `${topRectHeight}px`); +**全屏模式下的状态栏的避让:** 通过原生创建一个SafeArea组件实现,而不通过JS实现 + +1.在在src/main/ets/entryability/EntryAbility.ets的onWindowStageCreate中设置全局保存状态栏的安全高度 + +```javascript +onWindowStageCreate(windowStage: window.WindowStage) { + // 设置全局保存状态栏的安全高度 + const systemAvoidArea = windowStage.getMainWindowSync().getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); + const topRectHeight = systemAvoidArea.topRect.height + AppStorage.setOrCreate('topRectHeight', `${topRectHeight}px`); + + windowStage.loadContent('pages/Index', storage, (err, data) => { + ... + } +} +``` +2.自定义SafeArea组件 + +```typescript // 自定义SafeArea组件 @Component export struct SafeArea { @@ -741,76 +850,39 @@ export struct SafeArea { } } +``` + +3.在TaroHybridPage页面中引用SafeArea组件 + +```tsx // Taro Hybrid的布局 +import { HostPageState, TaroHybrid } from '@hybrid/web-container'; +import { SafeArea } from './SafeArea' + @Entry @Component struct TaroHybridPage { + @State pageState: HostPageState = HostPageState.PageInit; + onPageShow(): void { + this.pageState = HostPageState.PageOnShow + } + onPageHide(): void { + this.pageState = HostPageState.PageOnHide + } build() { Stack(){ SafeArea(){ - TaroHybrid() + TaroHybrid({ + indexHtmlPath: 'index.html', + taroPath: 'pages/index/index', + pageState: this.pageState + }) } } } } ``` -**全屏模式下的导航条的避让:** 通过鸿蒙Web组件支持安全边界,给H5的底部bar设置margin -```css -padding-bottom: env( safe-area-inset-bottom); -``` - -`注意:` 支持安全区域的鸿蒙单内核系统暂未发布 - - -### 访问控制权限二次弹窗指导 - -### 性能优化:同步调用走缓存 -taro的NativeApi,是taro暴露给鸿蒙实现的原生方法。使得Taro具备调用原生的能力。 - -对于NativeApi中的同步方法,每次调用都要执行原生代码逻辑,频繁调用必然导致系统开销增加和耗时增加。所以taro内部对同步方法增加了缓存机制。比如"getSystemSetting"、"getWindowInfo"等。 -如果应用层想扩展,对额外的同步方法使用缓存机制以提高执行效率,则可以通过如下步骤扩展使用缓存的方法: - -#### 明确使用缓存的方法名 -```typescript -let list = new ArrayList(); -list.add("youMethodName1") -list.add("youMethodName2") - -TaroHybridManager.setCacheSyncMethods(list) -``` - -list是一个方法名的列表,用于标识哪些NativeApi中的方法使用缓存。这里的youMethodName1、youMethodName2为要使用缓存的方法名。 - -#### 监听数据变化更新缓存 -同步方法使用缓存机制可以极大加快api响应速度,但是需要及时更新缓存中的数据。开发者需要在合适的时机监听数据变化,并通过如下方法更新数据 - - -```typescript -nativeUpdater.update(p: NativeApiPair, v?: any) -``` -举例说明: - -1. youMethodName1方法执行全量数据更新(无入参的情况): - -```typescript -nativeUpdater.update(new NativeApiPair("YouMethodName1",[])) -``` - - -2. youMethodName1方法执行全量数据更新(有入参的情况): - -```typescript -nativeUpdater.update(new NativeApiPair("youMethodName1",["param1",123])) -``` -3. youMethodName1方法执行局部数据更新(仅更新其中某个字段:myField): - -```typescript -nativeUpdater.update(new NativeApiPair("youMethodName1",["param1",123],"myField"),"newData") -``` -#### 注意 -1. 使用缓存机制的方法,必须是同步方法。 -2. 使用缓存机制的方法,在数据发生变化时必须及时更新缓存,并且对数据发生变化的场景做全方位覆盖。否则会带来获取到的数据不是最新的问题。 ### Taro.request请求方式选择(原生/js) @@ -818,7 +890,6 @@ nativeUpdater.update(new NativeApiPair("youMethodName1",["param1",123],"myField" #### 一、Taro.request 原生实现(native桥接)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/network/request/index.ts ```typescript - let task!: Taro.RequestTask const result: ReturnType = new Promise((resolve, reject) => { const upperMethod = method ? method.toUpperCase() : method @@ -844,7 +915,6 @@ let task!: Taro.RequestTask #### 二、Taro.request js实现(axios)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/request.ts ```typescript - this.httpRequest = axios.create({ responseType: responseType || 'text', headers: headers, @@ -980,62 +1050,50 @@ this.httpRequest = axios.create({ }) ``` -#### 三、原生、js方式转换实现(proxy代理)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/NativeApi.ts +#### 三、原生、js方式转换实现 ```typescript - -class HybridProxy { - private readonly useAxios: boolean - private readonly useOsChannel: boolean - private readonly cacheProxy: any - private readonly requestApi = 'request' - - constructor (useAxios: boolean, useOsChannel: boolean, nativeApi: NativeApi) { - this.useAxios = useAxios - this.useOsChannel = useOsChannel - this.cacheProxy = new Proxy(nativeApi, new CacheStorageProxy(nativeApi)) - } - - get (_target: any, prop: string) { - return (...args: any) => { - if (this.useAxios && prop === this.requestApi) { - judgeUseAxios = this.useAxios - // @ts-ignore - return new RequestTask(...args) - } - if (this.useOsChannel && osChannelApi.hasOwnProperty(prop)) { - return osChannelApi[prop](...args) - } - return this.cacheProxy[prop](...args) - } - } +// 调用时通过第二个参数控制是原生还是js实现,默认为原生实现方式 +export function request (options: any, useNativeRequest: boolean = true) { + return useNativeRequest ? nativeReuqest(options) : h5Request(options) } -const nativeApi = new NativeApi() -const native = new Proxy(nativeApi, new HybridProxy(false, false, nativeApi)) // 第一个false是默认走jsb,true是走纯js, 第二个false是不走osChannel -export default native +// 具体使用方式 + const task = Taro.request({ + ...data, + success: (res) => { + ... + }, + fail: (res) => { + ... + }, + complete: (res) => { + ... + }, + },true/false) //第二个参数可忽略 + ``` -#### 注意 -1、转换需要手动修改 const native = new Proxy(nativeApi, new HybridProxy(false, false, nativeApi)) 中 new HybridProxy的第一个参数,改为false为原生实现,若改为true,则为js实现。 -### 同层渲染 -预计后续530支持 +#### + +### 同层渲染 `@hybrid/web-container` 提供 `sameLayerManager.registerNativeComponentBuilders` 方法来注册同层渲染组件。 **参数说明:** -| 参数名称 | 类型 | 描述 | 必填 | -|---------------|----------------------------------------------------|----------------------|---------------------| -| componentName | string | 对应embed标签type属性,去掉 `native/` 前缀 | 是 | -| builder | (...args: ESObject[]) => void | 使用 `@Builder` 注解的原生组件builder函数 | 是 | - +| 参数名称 | 类型 | 描述 | 必填 | +| ------------- | ----------------------------- | ------------------------------------------ | ---- | +| componentName | string | 对应embed标签type属性,去掉 `native/` 前缀 | 是 | +| builder | (...args: ESObject[]) => void | 使用 `@Builder` 注解的原生组件builder函数 | 是 | **使用方法:** + - 壳工程中,注册自定义组件: ```typescript import { sameLayerManager } from '@hybrid/web-container' +import { NativeVideoBuilder } from '../pages/HosVideo' //具体路径根据项目实现路径引入 export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { @@ -1045,6 +1103,7 @@ export default class EntryAbility extends UIAbility { } ``` + - Video 自定义组件实现参考 ```typescript @@ -1115,83 +1174,121 @@ export function NativeVideoBuilder(params: VideoParams) { ``` - 前端代码中,使用 `react` 框架实现的代码如下: -```typescript -import React from 'react' -import classNames from 'classnames' - -import './index.scss' - -let videoId = 0 -interface IProps extends React.HTMLAttributes { - src: string - controls?: boolean - autoplay?: boolean - loop?: boolean - muted?: boolean - onPlay?: (res: any) => void - onPause?: (res: any) => void -} - -export default class HosVideo extends React.Component { - private componentId: string - - constructor (props: IProps | Readonly) { - super(props) - this.componentId = `video_${videoId++}` - } - - componentDidMount () { - this.transferVideoProps() - } - - componentDidUpdate (_prevProps: IProps, _prevState: any) { - // 组件更新时重新传输数据 - this.transferVideoProps() - } - - transferVideoProps () { - // 同层渲染video组件数据 - const { - src, - controls, - autoplay, - loop, - muted, - onPlay, - onPause - } = this.props - - const properties = { - componentId: this.componentId, - src: src, - controls: controls, - autoPlay: autoplay, - loop: loop, - muted: muted, - onPlay: onPlay, - onPause: onPause - } - - // @ts-ignore 调用JSB方法传递原生组件数据 - window.JSBridge && window.JSBridge.transferSameLayerArgs(properties) - } + 1. sameLayerRender实现如下 - render () { - const { - style, - className - } = this.props - - return ( -
- -
- ) - } -} -``` + ```ts + // @ts-ignore + const asyncAndNotRelease = window.MethodChannel && window.MethodChannel.jsBridgeMode({ isAsync: true, autoRelease: false }) || (target => target) + + class SameLayerRender { + @asyncAndNotRelease + transferSameLayerArgs (_options: object):void {} + } + const sameLayerRender = new SameLayerRender() + export default sameLayerRender + ``` + + 2. HosVideo.ts组件实现如下 + + ```tsx + import React from 'react' + import classNames from 'classnames' + import sameLayerRender from './SameLayerRender' + + interface IProps extends React.HTMLAttributes { + src: string + controls?: boolean + autoplay?: boolean + loop?: boolean + muted?: boolean + onPlay?: (res: any) => void + onPause?: (res: any) => void + } + + export default class HosVideo extends React.Component { + // eslint-disable-next-line react/sort-comp + private componentId: string + + constructor(props: IProps | Readonly) { + super(props) + this.componentId = `video_${Math.floor(Math.random() * 100000)}_${Date.now()}` + } + + componentDidMount() { + this.transferVideoProps() + } + + componentDidUpdate(_prevProps: IProps, _prevState: any) { + // 组件更新时重新传输数据 + this.transferVideoProps() + } + + transferVideoProps() { + // 同层渲染video组件数据 + const { + src, + controls, + autoplay, + loop, + muted, + onPlay, + onPause + } = this.props + + const properties = { + componentId: this.componentId, + src: src, + controls: controls, + autoPlay: autoplay, + loop: loop, + muted: muted, + onPlay: onPlay, + onPause: onPause, + width:'300px', + height:'300px' + } + sameLayerRender.transferSameLayerArgs(properties) + } + + render() { + const { + style, + className + } = this.props + + return ( +
+ +
+ ) + } + } + ``` + + + + 3. 具体使用方式 + + ```tsx + import { View, Button } from '@tarojs/components' + import './index.scss' + import HosVideo from '../nativeCompinents/hosvideodemo'; + + export default function Index() { + return ( + + + + ) + } + + ``` + ## FAQ @@ -1202,12 +1299,9 @@ export default class HosVideo extends React.Component { ```typescript // Web组件加载时,原生会给js侧注入的代码: var navigationHeight = ${this.navigationBarHeight}; - -// 使用方法 -window.navigationHeight ``` -当存在原生导航栏时,fixed布局需要添加一个top值,其值为window.navigationHeight +当存在原生导航栏时,fixed布局需要添加一个top值,其值为window.navigationHeight,由原生测注入,taro自动设置 [Web调试devtools配置]: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/web/web-debugging-with-devtools.md diff --git a/versioned_docs/version-3.x/harmony-hybrid/index.md b/versioned_docs/version-3.x/harmony-hybrid/index.md index 56817a7f47a5..714c75ab60b3 100644 --- a/versioned_docs/version-3.x/harmony-hybrid/index.md +++ b/versioned_docs/version-3.x/harmony-hybrid/index.md @@ -1,5 +1,6 @@ --- title: Harmony Hybrid + --- :::info @@ -8,7 +9,7 @@ Taro v3.6.24+ 开始支持 ## 简介 -Taro Harmony Hybrid容器是为让Taro小程序代码可以完整的运行在鸿蒙单内核系统里,在Taro H5平台的基础上,基于原生壳工程的jsbridge能力,扩展H5平台不支持的小程序Api能力,让Taro小程序0成本运行在鸿蒙系统。 +Taro Harmony Hybrid容器是为让Taro小程序代码可以完整的运行在鸿蒙单内核系统里,在Taro H5平台的基础上,基于原生壳工程的JSBridge能力,扩展H5平台不支持的小程序Api能力,让Taro小程序0成本运行在鸿蒙系统。 此方案不同于Taro的Harmony原生方案,其运行时还是Webview,重点解决Api切换的问题。 @@ -56,6 +57,15 @@ Options: 通过 Taro CLI 工具基于`默认模板`创建 Taro 工程,使用如下编译命令生成 harmony-hybrid 平台的 Web 资源: ```shell +# 项目根目录下安装依赖 +# yarn +$ yarn install +# npm script +$ npm install +# pnpm script +$ pnpm install + +# 编译项目 # yarn $ yarn build:harmony-hybrid # npm script @@ -74,6 +84,7 @@ Taro `harmony-hybrid` 平台产物需要结合鸿蒙壳工程一起编译运行 - 使用DevEco Studio开发工具( >= 5.0.3.100 )新建应用工程,选择Empty Ability模板,API >= 11, 其他配置使用默认值。 - 在 `entry/oh-package.json5` 文件中添加 `@hybrid/web-container` 模块的依赖并点击Sync进行同步: + ```json { "license": "ISC", @@ -82,39 +93,99 @@ Taro `harmony-hybrid` 平台产物需要结合鸿蒙壳工程一起编译运行 "description": "演示如何使用TaroWebContainer组件", "version": "1.0.0", "dependencies": { - "@hybrid/web-container": "2.0.0-rc.1" + "@hybrid/web-container": "2.0.0-rc.7" } } ``` -- 使用 `@hybrid/web-container` -```typescript -import { TaroWebContainer } from '@hybrid/web-container'; -``` + 具体使用方式请参考下个章节:集成到现有鸿蒙工程 - 最后点击菜单 `Build` -- `Rebuild Project` 编译项目。 ## 集成到现有鸿蒙工程 +### 初始化 + +在src/main/ets/entryability/EntryAbility.ets中的 windowStage.loadContent里初始化和预加载,扩展原生Api可参考进阶教程中部分 + +```ts +import { TaroHybridManager, InjectObject } from '@hybrid/web-container' +import router from '@ohos.router'; + +const nativeObj: InjectObject={} + +// 在windowStage.loadContent中初始化项目 +windowStage.loadContent('pages/Index', (err) => { + // 全局初始化 + TaroHybridManager.init({ + uiAbilityContext: this.context, + domain: 'https://customer.domain.com', // 小程序的域名,注意:此处不添加/结尾 + injectNativeMethod: (indexHtmlPath: string,uiAbilityContext: common.UIAbilityContext) => { // 扩展原生API + return nativeObj + }, + rootIndexHtmlPath: "/index.html", + nativeBack: ()=>{ + router.back() + }, + forceDarkAccess: true, + windowClass: windowStage.getMainWindowSync(), + rawFileName: 'spa', + diskCacheEnable: true + }) + // 预加载,可选,不是必须 + TaroHybridManager.preLoader( + windowStage.getMainWindowSync().getUIContext(), + "/xx/index.html", // html的path路由 + "/pages/index/index" // Taro小程序的Page路径 + ) +}) +``` + +**TaroHybridManager.init 参数说明:** + +init方法的参数类型为 `TaroHybridManagerOptions`, 各字段说明如下: + +| 字段名称 | 类型 | 描述 | 必填 | +| ------------------ | ------------------------------------------ | ------------------------------------------------ | ---- | +| uiAbilityContext | UIAbilityContext | UIAbility的上下文对象 | 是 | +| windowClass | Window | 主窗口 | 是 | +| injectNativeMethod | (string, UIAbilityContext) => InjectObject | 注入对象生成函数 | 否 | +| nativeBack | () => void | 多容器场景回退自定义函数,只作用于TaroHybrid组件 | 否 | +| rootIndexHtmlPath | string | 主资源路径,只作用于TaroHybrid组件 | 否 | +| forceDarkAccess | boolean | 是否强制反色适配深色模式,只作用于TaroHybrid组件 | 否 | +| domain | string | 小程序的域名,此处不添加/结尾 | 否 | +| rawFileName | string | 小程序的bundle目录 | 否 | +| diskCacheEnable | boolean | 小程序磁盘是否缓存,默认true | 否 | + ### 简单集成(TaroWebContainer) `TaroWebContainer` 组件提供加载单页面 Web 应用能力,并提供部分 Taro API 鸿蒙版本。 **使用方法:** + +在src/main/ets/pages/Index.ets中使用TaroWebContainer组件拉起taro项目 + ```typescript -// entry/src/main/ets/pages/Index.ets import Want from '@ohos.app.ability.Want'; import Url from '@ohos.url'; -import { TaroWebContainer, InjectObject, HostPageState, TaroWebController, Logger, LoggerLevel, wbLogger } from '@hybrid/web-container'; -const SAMPLE_INDEX_TAG = 'SampleIndex'; +import { TaroWebContainer, InjectObject, HostPageState, TaroWebController } from '@hybrid/web-container'; + +const WEB_CONTAINER_PAGE_TAG = 'WebContainerPage'; +let storage = LocalStorage.getShared() // 获取共享的本地存储对象 +const TAG = 'WebContainerPage'; + +@Entry(storage) @Component -struct TaroMpharmonySample { +struct WebContainerPage { + @LocalStorageProp('want') want: Want = {}; @State pageState: HostPageState = HostPageState.PageInit; @State taroWebController: TaroWebController = new TaroWebController(); + // 用户可以自定义对象注入到Web环境中,使用native.sayHello格式进行调用 nativeObj: InjectObject = { - sayHello: () => console.log('Hello World'), + sayHello: () => console.log(TAG, 'sayHello %{public}s', 'Hello World'), } + onBackPress() { if (this.taroWebController.accessBackward()) { this.taroWebController.backward(); @@ -122,39 +193,56 @@ struct TaroMpharmonySample { } return false; } - aboutToAppear() { - Logger.setLogLevel(LoggerLevel.LOG_DEBUG); - } + onPageShow() { this.pageState = HostPageState.PageOnShow; } + onPageHide() { this.pageState = HostPageState.PageOnHide; } + webUrl(): string { // 开发阶段可以把网站静态资源文件放置到src/main/resources/rawfile/目录下 - // 生产环境下可以把部分网页静态资源放置到web服务器, 这里填写实际的网站地址url + // 生产环境下把网站静态资源放置到web服务器, 这里填写实际的网站地址url return 'resource://rawfile/index.html'; } + webUrlPrefix() { try { - const url = Url.URL.parseURL(this.webUrl()); - return `${url.protocol}//${url.host}/`; + const url = Url.URL.parseURL(this.webUrl()); + return `${url.protocol}//${url.host}/`; } catch (err) { - wbLogger.error(SAMPLE_INDEX_TAG, `Invalid webUrl: ${this.webUrl()}`); - return ''; + console.error(WEB_CONTAINER_PAGE_TAG, `Invalid webUrl: ${this.webUrl()}`); + return ''; } } + + getUrl(value: LoadCommittedDetails) { + console.log('LoadCommittedDetails:', JSON.stringify(value)) + } + build() { Column() { TaroWebContainer({ + indexHtmlPath: 'index.html', pageState: this.pageState, // 页面状态同步到组件 webUrl: this.webUrl(), // 初始Url webUrlPrefix: this.webUrlPrefix(), - useCache: true, + useCache: false, + want: this.want, // want信息 taroWebController: this.taroWebController, - isFullScreen: true, // 是否全屏显示 + isFullScreen: false, // 是否全屏显示 injectObj: this.nativeObj, // 注入对象 + navigationInitVisible: true, // 导航栏是否显示 + showCapsule:true, + capsulePage:'index.html', + enableWebDebug:true, + forceDarkAccess: true, + userAgent: '', + getLoadCommittedDetails: (value: LoadCommittedDetails) => { + this.getUrl(value) + } }) .width('100%') .height('100%') @@ -165,59 +253,89 @@ struct TaroMpharmonySample { **构造参数说明:** -| 参数名称 | 类型 | 描述 | 必填 | -|-------------------|-------------------|-------------------------------------------------------|-----------------------| -| taroWebController | TaroWebController | TaroWebContainer组件的控制器 | 是 | -| webUrl | string | 资源入口url | 是 | -| webUrlPrefix | string | 资源入口url的前缀,一般是 `${webUrl.protocol}://${webUrl.host}/` | 是 | -| pageState | HostPageState | 传递页面状态 | 是 | -| useCache | boolean | 是否优先使用应用内置的Web资源 | 否,默认值: true | -| want | Want | 传递EntryAbility中`onCreate`和`onNewWant`保存的want信息 | 否,默认值: { } | -| isFullScreen | boolean | 是否全屏显示应用 | 否,默认值: true | -| injectObj | ESObject | 注入ets对象到Web环境 | 否:默认值:undefined | -| showCapsule | boolean | 是否显示胶囊按钮 | 否:默认值:true | -| capsulePage | string | 点击胶囊按钮跳转的页面 | 否:默认值:`pages/Capsule` | -| enableWebDebug | boolean | [开启Web调试功能][Web调试devtools配置] | 否:默认值:true | -| navigationInitVisible | boolean | 控制导航栏初始显示状态 | 否:默认值:true | +| 参数名称 | 类型 | 描述 | 必填 | +| ----------------------- | ----------------- | ------------------------------------------------------------ | --------------------------- | +| taroWebController | TaroWebController | TaroWebContainer组件的控制器 | 是 | +| webUrl | string | 资源入口url | 是 | +| webUrlPrefix | string | 资源入口url的前缀,一般是 `${webUrl.protocol}://${webUrl.host}/` | 是 | +| pageState | HostPageState | 传递页面状态 | 是 | +| useCache | boolean | 是否优先使用应用内置的Web资源 | 否,默认值: true | +| want | Want | 传递EntryAbility中`onCreate`和`onNewWant`保存的want信息 | 否,默认值: { } | +| isFullScreen | boolean | 是否全屏显示应用 | 否,默认值: true | +| injectObj | ESObject | 注入ets对象到Web环境 | 否:默认值:undefined | +| showCapsule | boolean | 是否显示胶囊按钮 | 否:默认值:true | +| capsulePage | string | 点击胶囊按钮跳转的页面 | 否:默认值:`pages/Capsule` | +| enableWebDebug | boolean | [开启Web调试功能][Web调试devtools配置] | 否:默认值:true | +| navigationInitVisible | boolean | 控制导航栏初始显示状态 | 否:默认值:true | +| userAgent | string | 自定义用户代理 | 否 | +| getLoadCommittedDetails | Function | 网页跳转时触发该回调方法 | 否 | +| forceDarkAccess | boolean | 网页强制开启深色模式 | 否:默认值:false | + ### 多容器和容器共用集成(TaroHybrid) +`TaroHybrid` 同`TaroWebContainer`组件功能相似,提供加载单页面 Web 应用能力,并提供部分 Taro API 鸿蒙版本,主要用于项目中有多个webview的场景。 + **使用方法:** + +1.在src/main/ets/pages/SafeArea.ets中定义一个顶部导航栏避让的组件 + +```ts +@Component +export struct SafeArea { + @StorageProp('topRectHeight') topRectHeight: string = '0px' + @BuilderParam child: ()=>void + + build() { + Column() { + Row() + .width('100%') + .height(this.topRectHeight) + .backgroundColor(0xFFFFFF) + Column(){ + this.child() + } + .width('100%') + .layoutWeight(1) + } + .width('100%') + .height('100%') + } +} +``` + +2.在src/main/ets/pages/TaroHybridPage.ets中使用TaroHybrid组件拉起taro项目 + ```typescript -// windowStage.loadContent里初始化 和 预加载 -windowStage.loadContent('xxx', storage, (err, data) => { - // 全局初始化 - TaroHybridManager.init({ - uiAbilityContex: this.context, - domain: 'https://customer.domain.com', // 小程序的域名,注意:此处不添加/结尾 - injectNativeMethod: (uiAbilityContext: common.UIAbilityContext) => { // 扩展原生API - ... - } - }) - // 预加载,可选,不是必须 - TaroHybridManager.preLoader( - windowStage.getMainWindowSync().getUIContext(), - "/xx/index.html", // html的path路由 - "/pages/index/index" // Taro小程序的Page路径 - ) -}) +import { HostPageState, TaroHybrid, TaroHybridManager } from '@hybrid/web-container/Index'; +import router from '@ohos.router'; +import { SafeArea } from './SafeArea'; -// 创建TaroHybrid页面(例子是基于Entry实现) +const TAG = 'TaroHybridPage' + +// 创建TaroHybridPage页面(例子是基于Entry实现) export interface TaroHybridRouterParams { indexHtmlPath: string, taroPath: string } export const RouterToTaroHybrid = (params: TaroHybridRouterParams) => { - // 跳转之前先加载url,目的是为了提升新页面的打开速度 - TaroHybridManager.loadUrl(params.indexHtmlPath, params.taroPath); - + // 跳转之前先加载url,目的是为了提升新页面的打开速度 + const result = TaroHybridManager.loadUrl(params.indexHtmlPath, params.taroPath); + if (result) { + // 容器复用成功,等待loadUrl,不然会出现先看到上一个页面 setTimeout(()=>{ - router.pushUrl({ - url: 'xxx/TaroHybridPage', // 鸿蒙原生Page的路径,不是Taro小程序的Page - params: params - }, router.RouterMode.Standard) - }, 200) + router.pushUrl({ + url: 'pages/TaroHybridPage', + params: params + }, router.RouterMode.Standard) + }, 300) + } else { + router.pushUrl({ + url: 'pages/TaroHybridPage', + params: params + }, router.RouterMode.Standard) + } } @Entry @@ -226,6 +344,13 @@ struct TaroHybridPage { @State pageState: HostPageState = HostPageState.PageInit; private indexHtmlPath: string = (router.getParams() as TaroHybridRouterParams).indexHtmlPath private taroPath: string = (router.getParams() as TaroHybridRouterParams).taroPath + + aboutToAppear(): void { + // 设置UserAgent + TaroHybridManager.setCustomUserAgent('') + // initLoadCommittedDetails(indexHtmlPath,callBack()) web页面跳转时触发callBack() + TaroHybridManager.initLoadCommittedDetails(this.indexHtmlPath,(value: LoadCommittedDetails): void => {}) + } onPageShow(): void { this.pageState = HostPageState.PageOnShow @@ -238,7 +363,9 @@ struct TaroHybridPage { if (!instance) { return false; } - return TaroHybridManager.onBack(instance.builderData.taroWebController) + return TaroHybridManager.onBack( + instance.builderData.indexHtmlPath, + instance.builderData.taroPath) } build() { @@ -256,27 +383,67 @@ struct TaroHybridPage { ``` +3.在src/main/ets/pages/Index.ets中增加一个跳转按钮,可跳转到TaroHybridPage页面 + +```ts +import { RouterToTaroHybrid } from './TaroHybridPage'; +import { SafeArea } from './SafeArea'; + +let storage = LocalStorage.getShared() // 获取共享的本地存储对象 + +@Entry(storage) +@Component +struct Index { + build() { + Stack({ alignContent: Alignment.TopStart }) { + SafeArea() { + Column({ space: 30 }) { + Button('TaroHybrid例子', { type: ButtonType.Normal, stateEffect: true }) + .borderRadius(8) + .backgroundColor(0x317aff) + .height(40) + .onClick(() => { + RouterToTaroHybrid({ + indexHtmlPath: "/spa/main/index.html", + taroPath: "/pages/index/index" + }) + }) + } + .width('100%') + .height('100%') + .alignItems(HorizontalAlign.Center) + } + } + } +} + +``` + **TaroHybrid的设计思路:** + 1. 多容器的判断依据:html的Path路径为判断依赖,相同则共用,不同则新建载体页 2. 容器共用的思路:通过鸿蒙的NodeContainer + NodeController实现 -3. 注意: - 1. 容器共用存在一个问题:相邻两个原生Page之间如果共用容器,页面切换动画时,会有一个页面白屏,进入和退出时都会出现,尽量避免相邻两个原生Page之间共用容器。 +3. 注意:容器共用存在一个问题:相邻两个原生Page之间如果共用容器,页面切换动画时,会有一个页面白屏,进入和退出时都会出现,尽量避免相邻两个原生Page之间共用容器。 ## 进阶教程 ### 混合路由 +#### **实现方式** + 在原生与TaroHybrid混合鸿蒙应用中,如果使用了容器共用,用户的路由栈会比较复杂,当在Taro Hybrid页面时,用户使用原生的物理返回时,需要区分是Web容器的返回,还是原生的返回。 TaroHybrid组件已经解决了此问题,其思路为: -1. 原生跳转打开的taro页面,添加query参数from=native -2. 原生的onBackPress逻辑里,获取当前url,判断有没有参数from=native,如果有则走原生路由返回,如果没有则走Web组件的backward逻辑 + +1. 原生跳转打开的taro页面,添加query参数tarofrom=native +2. 原生的onBackPress逻辑里,获取当前url,判断有没有参数tarofrom=native,如果有则走原生路由返回,如果没有则走Web组件的backward逻辑 ```typescript +// @hybrid/web-container 内部已实现 static onBack(taroWebController: TaroWebController): boolean { let curUrl = taroWebController.webController.getUrl(); - if (curUrl?.endsWith('?from=native')) { //web回退到原生 + if (curUrl?.endsWith('?tarofrom=native')) { //web回退到原生 // Web先返回 if (taroWebController.accessBackward()) { setTimeout(()=>{ @@ -296,10 +463,76 @@ static onBack(taroWebController: TaroWebController): boolean { } ``` +#### **使用方式** + +1. TaroWebContainer组件设置混合路由返回方式 + + ```tsx + ... + struct WebContainerPage { + // 判断原生返回或taro返回 + onBackPress() { + if (this.taroWebController.accessBackward()) { + this.taroWebController.backward(); + return true; + } + return false; + } + ... + + build() { + Column() { + TaroWebContainer({...}) + .width('100%') + .height('100%') + } + } + } + ``` + +2. TaroHybrid组件设置混合路由返回方式 + + ```tsx + ... + + @Entry + @Component + struct TaroHybridPage { + ...... + + // 通过indexHtmlPath区分webview容器,设置返回方式 + onBackPress(): boolean | void { + const instance = TaroHybridManager.getCoreInstance(this.indexHtmlPath) + if (!instance) { + return false; + } + return TaroHybridManager.onBack( + instance.builderData.indexHtmlPath, + instance.builderData.taroPath) + } + + build() { + Stack({alignContent: Alignment.TopStart}){ + SafeArea(){ + TaroHybrid({ + ... + }) + } + } + } + } + ``` + + + ### 小程序内置及热更新 + #### 内置及热更新的使用方式 + ##### 小程序内置规则 + 在业务较为复杂的场景时,项目通常分为多个`bundle`,内置到应用时遵循以下目录结构规则: + ``` └──rawfile # 应用rawfile目录 └──spa # 多bundle内置目录,支持配置 @@ -318,150 +551,66 @@ static onBack(taroWebController: TaroWebController): boolean { ├──static # 静态资源目录 └──index.html # 首页文件 ``` -以上目录结构的解释说明 -1. rawfile目录下的spa,为多bundle的内置目录,容器会在此目录下读取bundle加载,支持以下方式配置(该目录要与配置一致): -```typescript -GlobalThis.getInstance().setRawFile('spa') -``` -2. bundle包名称:`一级目录`+`_`+`二级目录`+`@`+`VersionCode`,例如:spa_main@100000,以`@`符分割为两部分: -2.1 `@`符分前置: 为bundle的打包目录层级,一般为index.html(首页文件)的path路径,用下划线`_`分割,作用是为内置文件路径拦截匹配查找; -2.2 VersionCode: 为当前bundle版本号,作用为版本控制及磁盘热更新 -##### 小程序磁盘缓存 -1. 读取磁盘缓存,磁盘更新操作入口(如下),读取应用磁盘`${context.filesDir}/update`下bundle缓存版本信息: -```typescript -LocalUpdateManager.updateMiniOnLaunch() -``` -2. 内置更新缓存至磁盘,若磁盘缓存bundle版本 < rawfile内置版本,则将内置bundle更新至磁盘,具体实现参见以下方法: -```typescript -LocalUpdateManager.updateAllMini() -``` -3. 更新至磁盘的bundle在容器加载时会被自动查找加载; -4. 磁盘缓存默认开启,支持以下方式关闭: -```typescript -GlobalThis.getInstance().setDiskUpdateEnable(false) -``` -##### 热更新 -目前正在开发阶段,暂不支持。 -#### 整体方案简介 - -动态下开发台吗,在不发布新版本的情况下修复`bug`和发布新功能,绕开应用商店的审核机制,避免长时间审核以及多次被拒绝造成的成本问题,缩短用户取得新版本的流程,改善用户体验。 - -#### 具体实现 - -##### 1、资源拦截并加载本地资源 - -在业务较为复杂的场景时,项目通常分为多个`bundle`,热更新模块能够支持更新单独某个`bundle`服务。在`web`组件的`onInterceptRequest`中,可以进行资源拦截。通过`url`解析出`path`后,通过`path`获取对应的`bundle`信息,进而构建相应的响应返回给`web`组件。`MiniRawFileDataSource.getBizByPath`可以获取本地缓存中的`bundle`信息,`MiniLocalFileDataSource.fileExist`可以根据位置判断资源是否存在。 - -```ts -path = path.split('#')[0] -wbLogger.debug('TaroWeb', 'intercepter path=%{public}s', path) -let biz = MiniRawFileDataSource.getBizByPath(path) //获取业务标识 -const lastVerInfo = LocalUpdateManagerInstance.getLastVersionInfo(biz) -if (lastVerInfo?.diskCachePath) { // 走本地缓存 - if (!path || !MiniLocalFileDataSource.fileExist(lastVerInfo?.diskCachePath, path)) { - return null; - } - wbLogger.debug('TaroWeb', 'useLocalCacheResource path=%{public}s', path) - return MiniLocalFileDataSource.getFileOnResponseWeb(lastVerInfo?.diskCachePath, path) - } -``` -##### 2、入口函数--`updateMiniOnLaunch` +以上目录结构的解释说明 -入口函数`updateMiniOnLaunch`接收一个`context`,首先初始化了基类`UpdateManager`,然后获取了`rawfile`目录下的文件内容,并传入到了`updateAllMini`中进行逻辑处理。 +1. rawfile目录下的spa,为多bundle的内置目录,容器会在此目录下读取bundle加载,支持以下方式配置(该目录要与配置一致): -```ts -updateMiniOnLaunch(context: AppContext.UIAbilityContext) { - this.reset(context) - wbLogger.debug('LocalUpdateManager',`update start`); - this.updateAllMini(context,this.miniRepository.getMiniRawList(context)).then((result: Array) => { - if (result) { - wbLogger.debug('LocalUpdateManager',`update success ${result}`); - } - }).catch((error: Error) => { - wbLogger.error('LocalUpdateManager','update Error :', error); +```typescript +// src/main/ets/entryability/EntryAbility.ets文件中写入 +windowStage.loadContent('pages/Index', (err) => { + // 全局初始化 + TaroHybridManager.init({ + ...... + rawFileName: 'spa', + diskCacheEnable: true }) -} ``` +2. bundle包名称:`一级目录`+`_`+`二级目录`+`@`+`VersionCode`,例如:spa_main@100000,以`@`符分割为两部分: + 2.1 `@`符分前置: 为bundle的打包目录层级,一般为index.html(首页文件)的path路径,用下划线`_`分割,作用是为内置文件路径拦截匹配查找; + 2.2 VersionCode: 为当前bundle版本号,作用为版本控制及磁盘热更新 +#### 热更新 -##### 3、获取`rawfile`文件信息--`getMiniRawList` +##### 整体方案简介 -从全局存储中拿出初始化时存储的`rawfile`文件名,根据该文件名称,获取该文件夹下所有的文件信息。 +动态下开发代码,在不发布新版本的情况下修复`bug`和发布新功能,绕开应用商店的审核机制,避免长时间审核以及多次被拒绝造成的成本问题,缩短用户取得新版本的流程,改善用户体验。 -```ts -getRawMiniList(context: common.Context): Array{ - let rawFile = GlobalThis.getInstance().getRawFile(); - return context.resourceManager.getRawFileListSync(rawFile); -} -``` +##### 使用说明 -##### 4、更新所有的资源包信息---`updateAllMini` +###### `arkts`端配置 -该函数接收一个`context`和`rawfile`文件信息,返回一个`Promise`对象。通过`rawfile`文件信息,构建`LocalVersionInfo`实例对象,该对象存储了对应文件的版本号、业务标识`biz`, 以及`rawfile`路径,将其存储到`UpdateConfig`单例的`Map`对象中,这样在资源拦截时可以从该`Map`中通过相应的业务标识获取到对应资源的信息。`rawfile`目录下的资源可能不是最新的,需要与本地缓存进行对比,如果本地缓存中没有任何资源,则需要将`rawfile`的资源复制到本地沙箱相应位置中,这样可以通过字节码优化,能提高性能。由于`rawfile`文件复制到了沙箱,这样沙箱资源一直都是最新的,所以需要更新存储在`UpdateConfig`单例的`Map`对象的资源信息。 +热更新配置分为三步,首先需要配置`rawfile`内资源顶层文件夹名称,以壳工程资源为例`rawfile/spa/`,需要配置`spa`名称;其次,需要配置云端资源信息的`url`生成函数,这样当资源需要更新时可以从该地址获取相应信息; -```ts - async updateAllMini(context: AppContext.UIAbilityContext,miniList:Array) : Promise>{ - return new Promise((resolve, reject) => { - let miniArray = new Array() - try { - if (miniList) { - for (let index = 0; index < miniList.length; index++) { - const element = miniList[index]; - let splits = FileUtils.checkMiniFileNameFormat(element) - let miniInfo: LocalVersionInfo | null = null - if (splits) { - miniInfo = new LocalVersionInfo(splits[1], splits[0],'',this.miniRepository.getRawCachePath(element)) - UpdateConfig.getInstance().setOrReplaceMiniInfo(miniInfo.biz, miniInfo) - } - if (miniInfo) { - if (GlobalThis.getInstance().getDiskUpdateEnable()) { - this.updateDiskSingleMini(context, miniInfo) - } - wbLogger.debug('LocalUpdateManager',`VersionInfo miniInfo:${miniInfo.localVersion},biz:${miniInfo.biz},diskCachePath:${miniInfo.diskCachePath},rawCachePath:${miniInfo.rawCachePath}`); - miniArray.push(miniInfo) - } - } - //TODO-JasonZhang 热更新接口请求 - } - resolve(miniArray) - } catch (err) { - reject(err) - } - }) - } - updateDiskSingleMini(context: AppContext.UIAbilityContext,versionInfo:LocalVersionInfo){ - // 获取本地最新的版本号 - let localVersionInfo = this.miniRepository.readDiskLastMiniBizInfo(context,versionInfo.biz) - // 本地无内置版本,则迁移RawFile小程序资源到磁盘缓存目录为初始版本 - if (localVersionInfo && (localVersionInfo?.diskCachePath==''|| localVersionInfo?.localVersion < versionInfo.localVersion)) { - let result = this.miniRepository.copyMiniRawUnzipDisk(context,versionInfo) - if (result) { - versionInfo.localVersion = result.localVersion - versionInfo.diskCachePath = result.diskCachePath - } - wbLogger.debug('LocalUpdateManager',`copied success ${result}`); +```tsx +// 在src/main/ets/entryability/EntryAbility.ets中 +import { TaroHybridManager,LocalMiniInfo,NetworkMiniInfo } from '@hybrid/web-container'; + +// 请求更新版本的信息和下载路径 +function getNeedUpdateNetworkInfos(localVersions: LocalMiniInfo[]): Promise { + return new Promise((resolve)=>{ + // 使用接口请求服务端的版本信息 NetworkMiniInfo + let httpRequest = http.createHttp() + httpRequest.request("http://www.xxx.com",(error:Error,data:http.HttpResponse)=>{ + if(data.result){ + let NetworkMiniInfos:NetworkMiniInfo[]=data.result as NetworkMiniInfo[] + resolve(NetworkMiniInfos) }else{ - versionInfo.localVersion = localVersionInfo.localVersion - versionInfo.diskCachePath = localVersionInfo.diskCachePath + console.error(`getNeedUpdateNetworkInfos fail! errMsg ${error}`) } - } -``` -#### 使用说明 - -##### `arkts`端配置 - -热更新配置分为三步,首先需要配置`rawfile`内资源顶层文件夹名称,以壳工程资源为例`rawfile/spa/`,需要配置`spa`名称;其次,需要配置云端资源信息的`url`生成函数,这样当资源需要更新时可以从该地址获取相应信息;最后,启动热更新模块。 + }) + }) +} -```tsx -onCreate(want: Want) { - // ... other code - GlobalThis.getInstance().setRawFile('spa') - UpdateConfig.getInstance(). - LocalUpdateManagerInstance.updateMiniOnLaunch(this.context) - } +export default class EntryAbility extends UIAbility { + onCreate(want: Want) { + // ... other code + TaroHybridManager.startUpdateDiskCacheFromNetwork(this.context.getApplicationContext(), (localVersions:LocalMiniInfo[])=>getNeedUpdateNetworkInfos(localVersions)) + } + ......other code +} ``` ###### 1、配置位置 @@ -470,76 +619,18 @@ onCreate(want: Want) { ###### 2、导入包 -需要导入`LocalUpdateManagerInstance`实例,以及配置热更新模块的对象`UpdateConfig` +需要导入`TaroHybridManager`实例,以及配置热更新模块的参数类型`LocalMiniInfo, NetworkMiniInfo` ``` tsx -import {LocalUpdateManagerInstance, UpdateConfig} from '@ohos/web-container' +import { TaroHybridManager, LocalMiniInfo, NetworkMiniInfo } from '@hybrid/web-container'; ``` ###### 3、热更新初始配置 -在执行入口函数前,需要对热更新模块进行配置初始化。`UpdateConfig`对象可以配置四个参数,**第一个参数和第二个参数为必须配置参数,第三个和第四个参数为选配**。其中第一个参数为最新离线资源配置的地址,需要用户按照字段配置好;第二个参数为初始的版本号(可以是`rawfile`路径下资源的版本),该版本号将会与第一个参数进行拼接,形成完成的`url`。原生端根据形成的`url`发起请求以获取服务端离线资源配置信息,服务端依据相关规则将字段返回,分两种情况,**需要更新离线资源和本地资源是最新的**。**后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** +在执行入口函数前,需要对热更新模块进行配置初始化。`TaroHybridManager.startUpdateDiskCacheFromNetwork`方法接受2个参数*。其中第一个参数为应用的context,可以直接调用this.context.getApplicationContext()获取;第二个参数为一个方法,该方法接收 一个参数,为本地热更新资源的信息,返回通过网络请求获取服务端要更新的资源信息。 ```tsx -const updateConfig = new UpdateConfig('http://127.0.0.1:8080:/apis/config?local_version=', '0') -LocalUpdateManagerInstance.setUpdateConfig(updateConfig) -``` - -**1、需要更新离线资源** - -假设初始版本号为`0`,服务端的最新离线资源版本为`1`,则需要更新。服务端收到原生端的请求后,获取`local_version`字段进行对比,根据相关规则将字段传回原生端。服务端配置字段可以参照如下所示: - -```json -{"code": 200, "message": "request OK", "data": {"mini_download_url": "http://127.0.0.1/mpharmony/mpharmony.zip", "mini_version": 1}} -``` - -服务端需要配置上述四个字段,其中,`data`字段中的`mini_download_url`字段表示需要下载离线资源路径,热更新模块依据这个路径进行下载最新配置。 - -**注意:这里的`0`只是用例,后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** - -**2、本地资源是最新的** - -假设初始版本号为`1`,服务端的最新离线资源版本为`1`,则不需要更新。服务端收到原生端的请求后,获取`local_version`字段进行对比,根据相关规则将字段传回原生端,服务端配置字段可以参照如下所示: - -```json -{"code": "300", "message": "latest version"} -``` - -**注意:这里的`1`只是用例,后续的更新与该版本号无关,只与服务端最新离线资源配置相关。** - -###### 4、排序算法配置(选配) - -此外,热更新模块会保留两个历史版本,会在下次启动时删除多余版本。此时需要对离线版本进行排序,默认支持的排序算法是`2.1.0 < 11.0.1`。用户可以自己实现排序算法用以适配不同的版本号,伪代码如下所示: - -```ts -/** - * - * @param v1 - * @param v2 - * @returns {boolean} is v1 bigger than v2 - */ - -function versionCompare(v1: string, v2: string) { - if (v1 > v2){ - return true - } - return false -} -``` - -热更新模块配置如下所示: - -```tsx -const updateConfig = new UpdateConfig('http://127.0.0.1:8080:/apis/config?local_version=', '0', true, versionCompare) -LocalUpdateManagerInstance.setUpdateConfig(updateConfig) -``` - -第三个参数为是否开启自定义排序算法配置,默认为`false`,`false`会执行默认的排序算法 - -4、启动,启动热更新模块入口函数时需要传递当前页面的上下文,下载的离线资源会依据上下文存储。 - -```tsx -LocalUpdateManagerInstance.updateMiniOnLaunch(this.context) + TaroHybridManager.startUpdateDiskCacheFromNetwork(this.context.getApplicationContext(),(localVersions:LocalMiniInfo[])=>getNeedUpdateNetworkInfos(localVersions)) ``` ##### `js`端配置 @@ -587,12 +678,13 @@ updateManager.onUpdateFailed(function listener(){ 示例代码如下: ```typescript +// src/main/ets/pages/TaroMethods.ets中定义对象和方法 +import { InjectObject } from '@hybrid/web-container/Index'; interface LoginOptions { username: string password: string success: (t: string | null | undefined) => void - error: (error: BusinessError) => void } export const nativeObj:InjectObject = { @@ -606,12 +698,14 @@ export const nativeObj:InjectObject = { ##### 初始化注入InjectObject ```typescript +// src/main/ets/entryability/EntryAbility.ets +import nativeObj from '../pages/TaroMethods' TaroHybridManager.init({ uiAbilityContex: this.context, domain: 'https://xxx.xxx.com', // 注意:此处不添加/结尾 - injectNativeMethod: (uiAbilityContext: common.UIAbilityContext) => { + injectNativeMethod: (indexHtmlPath: string, uiAbilityContext: common.UIAbilityContext) => { return nativeObj - } + }, }) ``` @@ -651,7 +745,6 @@ autoRelease:如果option中有回调方法且回调方法可能会调用多次 ##### 方法调用 ```typescript - import { native } from './NativeApi'; native.customLogin({ @@ -674,10 +767,12 @@ native.customLogin({ 默认情况下,设计尺寸是750px,Taro适配不同手机分辨率的方式是等比缩放,在鸿蒙折叠屏展开的状态下,等比放大的效果不满足华为应用商店上架要求。 **建议的适配方案:** + 1. 最大放大比例不超过1.2倍 2. 宽度不基于750px来布局,基于flex自适应 最大放大比例不超过1.1倍的配置: + ```javascript // 修改config/index.js export default defineConfig(async (merge, { command, mode }) => { @@ -695,7 +790,8 @@ export default defineConfig(async (merge, { command, mode }) => { } }, // .... - } + } + }, // .... } @@ -710,13 +806,26 @@ export default defineConfig(async (merge, { command, mode }) => { 建议使用“窗口全屏布局方案”,默认全屏模式,再处理状态栏及导航条的避让。 -**全屏模式下的状态样的避让:** 通过原生创建一个SafeArea组件实现,而不通过JS实现 -```typescript -// 在UIAbility里,全局保存状态栏的安全高度 -const systemAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); -const topRectHeight = systemAvoidArea.topRect.height -AppStorage.setOrCreate('topRectHeight', `${topRectHeight}px`); +**全屏模式下的状态栏的避让:** 通过原生创建一个SafeArea组件实现,而不通过JS实现 + +1.在在src/main/ets/entryability/EntryAbility.ets的onWindowStageCreate中设置全局保存状态栏的安全高度 + +```javascript +onWindowStageCreate(windowStage: window.WindowStage) { + // 设置全局保存状态栏的安全高度 + const systemAvoidArea = windowStage.getMainWindowSync().getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); + const topRectHeight = systemAvoidArea.topRect.height + AppStorage.setOrCreate('topRectHeight', `${topRectHeight}px`); + + windowStage.loadContent('pages/Index', storage, (err, data) => { + ... + } +} +``` +2.自定义SafeArea组件 + +```typescript // 自定义SafeArea组件 @Component export struct SafeArea { @@ -741,84 +850,47 @@ export struct SafeArea { } } +``` + +3.在TaroHybridPage页面中引用SafeArea组件 + +```tsx // Taro Hybrid的布局 +import { HostPageState, TaroHybrid } from '@hybrid/web-container'; +import { SafeArea } from './SafeArea' + @Entry @Component struct TaroHybridPage { + @State pageState: HostPageState = HostPageState.PageInit; + onPageShow(): void { + this.pageState = HostPageState.PageOnShow + } + onPageHide(): void { + this.pageState = HostPageState.PageOnHide + } build() { Stack(){ SafeArea(){ - TaroHybrid() + TaroHybrid({ + indexHtmlPath: 'index.html', + taroPath: 'pages/index/index', + pageState: this.pageState + }) } } } } ``` -**全屏模式下的导航条的避让:** 通过鸿蒙Web组件支持安全边界,给H5的底部bar设置margin -```css -padding-bottom: env( safe-area-inset-bottom); -``` - -`注意:` 支持安全区域的鸿蒙单内核系统暂未发布 - - -### 访问控制权限二次弹窗指导 - -### 性能优化:同步调用走缓存 -taro的NativeApi,是taro暴露给鸿蒙实现的原生方法。使得Taro具备调用原生的能力。 - -对于NativeApi中的同步方法,每次调用都要执行原生代码逻辑,频繁调用必然导致系统开销增加和耗时增加。所以taro内部对同步方法增加了缓存机制。比如"getSystemSetting"、"getWindowInfo"等。 -如果应用层想扩展,对额外的同步方法使用缓存机制以提高执行效率,则可以通过如下步骤扩展使用缓存的方法: - -#### 明确使用缓存的方法名 -```typescript -let list = new ArrayList(); -list.add("youMethodName1") -list.add("youMethodName2") - -TaroHybridManager.setCacheSyncMethods(list) -``` - -list是一个方法名的列表,用于标识哪些NativeApi中的方法使用缓存。这里的youMethodName1、youMethodName2为要使用缓存的方法名。 - -#### 监听数据变化更新缓存 -同步方法使用缓存机制可以极大加快api响应速度,但是需要及时更新缓存中的数据。开发者需要在合适的时机监听数据变化,并通过如下方法更新数据 - - -```typescript -nativeUpdater.update(p: NativeApiPair, v?: any) -``` -举例说明: - -1. youMethodName1方法执行全量数据更新(无入参的情况): - -```typescript -nativeUpdater.update(new NativeApiPair("YouMethodName1",[])) -``` -2. youMethodName1方法执行全量数据更新(有入参的情况): - -```typescript -nativeUpdater.update(new NativeApiPair("youMethodName1",["param1",123])) -``` -3. youMethodName1方法执行局部数据更新(仅更新其中某个字段:myField): - -```typescript -nativeUpdater.update(new NativeApiPair("youMethodName1",["param1",123],"myField"),"newData") -``` -#### 注意 -1. 使用缓存机制的方法,必须是同步方法。 -2. 使用缓存机制的方法,在数据发生变化时必须及时更新缓存,并且对数据发生变化的场景做全方位覆盖。否则会带来获取到的数据不是最新的问题。 - ### Taro.request请求方式选择(原生/js) #### 一、Taro.request 原生实现(native桥接)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/network/request/index.ts ```typescript - let task!: Taro.RequestTask const result: ReturnType = new Promise((resolve, reject) => { const upperMethod = method ? method.toUpperCase() : method @@ -844,7 +916,6 @@ let task!: Taro.RequestTask #### 二、Taro.request js实现(axios)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/request.ts ```typescript - this.httpRequest = axios.create({ responseType: responseType || 'text', headers: headers, @@ -980,62 +1051,50 @@ this.httpRequest = axios.create({ }) ``` -#### 三、原生、js方式转换实现(proxy代理)代码位置:taro/packages/taro-platform-harmony-hybrid/src/api/apis/NativeApi.ts +#### 三、原生、js方式转换实现 ```typescript - -class HybridProxy { - private readonly useAxios: boolean - private readonly useOsChannel: boolean - private readonly cacheProxy: any - private readonly requestApi = 'request' - - constructor (useAxios: boolean, useOsChannel: boolean, nativeApi: NativeApi) { - this.useAxios = useAxios - this.useOsChannel = useOsChannel - this.cacheProxy = new Proxy(nativeApi, new CacheStorageProxy(nativeApi)) - } - - get (_target: any, prop: string) { - return (...args: any) => { - if (this.useAxios && prop === this.requestApi) { - judgeUseAxios = this.useAxios - // @ts-ignore - return new RequestTask(...args) - } - if (this.useOsChannel && osChannelApi.hasOwnProperty(prop)) { - return osChannelApi[prop](...args) - } - return this.cacheProxy[prop](...args) - } - } +// 调用时通过第二个参数控制是原生还是js实现,默认为原生实现方式 +export function request (options: any, useNativeRequest: boolean = true) { + return useNativeRequest ? nativeReuqest(options) : h5Request(options) } -const nativeApi = new NativeApi() -const native = new Proxy(nativeApi, new HybridProxy(false, false, nativeApi)) // 第一个false是默认走jsb,true是走纯js, 第二个false是不走osChannel -export default native +// 具体使用方式 + const task = Taro.request({ + ...data, + success: (res) => { + ... + }, + fail: (res) => { + ... + }, + complete: (res) => { + ... + }, + },true/false) //第二个参数可忽略 + ``` -#### 注意 -1、转换需要手动修改 const native = new Proxy(nativeApi, new HybridProxy(false, false, nativeApi)) 中 new HybridProxy的第一个参数,改为false为原生实现,若改为true,则为js实现。 -### 同层渲染 -预计后续530支持 +#### + +### 同层渲染 `@hybrid/web-container` 提供 `sameLayerManager.registerNativeComponentBuilders` 方法来注册同层渲染组件。 **参数说明:** -| 参数名称 | 类型 | 描述 | 必填 | -|---------------|----------------------------------------------------|----------------------|---------------------| -| componentName | string | 对应embed标签type属性,去掉 `native/` 前缀 | 是 | -| builder | (...args: ESObject[]) => void | 使用 `@Builder` 注解的原生组件builder函数 | 是 | - +| 参数名称 | 类型 | 描述 | 必填 | +| ------------- | ----------------------------- | ------------------------------------------ | ---- | +| componentName | string | 对应embed标签type属性,去掉 `native/` 前缀 | 是 | +| builder | (...args: ESObject[]) => void | 使用 `@Builder` 注解的原生组件builder函数 | 是 | **使用方法:** + - 壳工程中,注册自定义组件: ```typescript import { sameLayerManager } from '@hybrid/web-container' +import { NativeVideoBuilder } from '../pages/HosVideo' //具体路径根据项目实现路径引入 export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { @@ -1045,6 +1104,7 @@ export default class EntryAbility extends UIAbility { } ``` + - Video 自定义组件实现参考 ```typescript @@ -1115,83 +1175,121 @@ export function NativeVideoBuilder(params: VideoParams) { ``` - 前端代码中,使用 `react` 框架实现的代码如下: -```typescript -import React from 'react' -import classNames from 'classnames' - -import './index.scss' - -let videoId = 0 -interface IProps extends React.HTMLAttributes { - src: string - controls?: boolean - autoplay?: boolean - loop?: boolean - muted?: boolean - onPlay?: (res: any) => void - onPause?: (res: any) => void -} - -export default class HosVideo extends React.Component { - private componentId: string - - constructor (props: IProps | Readonly) { - super(props) - this.componentId = `video_${videoId++}` - } - - componentDidMount () { - this.transferVideoProps() - } - - componentDidUpdate (_prevProps: IProps, _prevState: any) { - // 组件更新时重新传输数据 - this.transferVideoProps() - } - - transferVideoProps () { - // 同层渲染video组件数据 - const { - src, - controls, - autoplay, - loop, - muted, - onPlay, - onPause - } = this.props - - const properties = { - componentId: this.componentId, - src: src, - controls: controls, - autoPlay: autoplay, - loop: loop, - muted: muted, - onPlay: onPlay, - onPause: onPause - } - - // @ts-ignore 调用JSB方法传递原生组件数据 - window.JSBridge && window.JSBridge.transferSameLayerArgs(properties) - } + 1. sameLayerRender实现如下 - render () { - const { - style, - className - } = this.props - - return ( -
- -
- ) - } -} -``` + ```ts + // @ts-ignore + const asyncAndNotRelease = window.MethodChannel && window.MethodChannel.jsBridgeMode({ isAsync: true, autoRelease: false }) || (target => target) + + class SameLayerRender { + @asyncAndNotRelease + transferSameLayerArgs (_options: object):void {} + } + const sameLayerRender = new SameLayerRender() + export default sameLayerRender + ``` + + 2. HosVideo.ts组件实现如下 + + ```tsx + import React from 'react' + import classNames from 'classnames' + import sameLayerRender from './SameLayerRender' + + interface IProps extends React.HTMLAttributes { + src: string + controls?: boolean + autoplay?: boolean + loop?: boolean + muted?: boolean + onPlay?: (res: any) => void + onPause?: (res: any) => void + } + + export default class HosVideo extends React.Component { + // eslint-disable-next-line react/sort-comp + private componentId: string + + constructor(props: IProps | Readonly) { + super(props) + this.componentId = `video_${Math.floor(Math.random() * 100000)}_${Date.now()}` + } + + componentDidMount() { + this.transferVideoProps() + } + + componentDidUpdate(_prevProps: IProps, _prevState: any) { + // 组件更新时重新传输数据 + this.transferVideoProps() + } + + transferVideoProps() { + // 同层渲染video组件数据 + const { + src, + controls, + autoplay, + loop, + muted, + onPlay, + onPause + } = this.props + + const properties = { + componentId: this.componentId, + src: src, + controls: controls, + autoPlay: autoplay, + loop: loop, + muted: muted, + onPlay: onPlay, + onPause: onPause, + width:'300px', + height:'300px' + } + sameLayerRender.transferSameLayerArgs(properties) + } + + render() { + const { + style, + className + } = this.props + + return ( +
+ +
+ ) + } + } + ``` + + + + 3. 具体使用方式 + + ```tsx + import { View, Button } from '@tarojs/components' + import './index.scss' + import HosVideo from '../nativeCompinents/hosvideodemo'; + + export default function Index() { + return ( + + + + ) + } + + ``` + ## FAQ @@ -1202,12 +1300,9 @@ export default class HosVideo extends React.Component { ```typescript // Web组件加载时,原生会给js侧注入的代码: var navigationHeight = ${this.navigationBarHeight}; - -// 使用方法 -window.navigationHeight ``` -当存在原生导航栏时,fixed布局需要添加一个top值,其值为window.navigationHeight +当存在原生导航栏时,fixed布局需要添加一个top值,其值为window.navigationHeight,由原生测注入,taro自动设置 [Web调试devtools配置]: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/web/web-debugging-with-devtools.md