鸿蒙中应用包结构常见问题

302 阅读9分钟

问题1:如何跳转到共享包中的指定页面

在使用方通过router.pushUrl方法传递正确url地址信息进行跳转。其中url地址模板内容为:

'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'

问题2:HSP/HAR包中如何引用外部编译的so库文件

  1. libxxx.so库文件放入HAR或HSP的libs/arm64-v8a目录。设备类型不同时,需添加对应子目录。新版的arm64为libs/arm64-v8a,老版的arm64为libs/armeabi-v7a,x86模拟器为libs/x86_64。

0000000000011111111.20250222123810.80301743676179584466162519658219.png

  1. 在src/main/cpp/CMakeLists.txt文件中链接so库文件
// CMakeLists.txt链接so库文件
target_link_libraries(entry PUBLIC libxxx)

问题3:业务模块HAR如何获取宿主HAP的数据

把需要获取的信息当做参数传入,HAR提供方法给HAP,HAP调用HAR的接口把需要的HAP中的信息传入到HAR里面,这样HAR可以获取到HAP里面的数据。

问题4:如何通过路由跳转到一个只有页面没有UIAbility的模块

问题现象

现在有模块A,B。A模块为Entry类型,其中有个UIAbility,有pages。B模块是Feature类型,没有UIAbility,但是有pages。整个App只想使用一个UIAbility的情况下,A模块怎么跳转到B模块的页面。

解决措施

  • Module分为“Ability”和“Library”两种类型:“Ability”类型的Module对应于编译后的HAP;“Library”类型的Module对应于HAR或者HSP
  • HAP可分为Entry和Feature两种类型:Entry类型的HAP是应用的主模块,通常用于实现应用的入口界面、入口图标、主特性功能等;Feature类型的HAP是应用的动态特性模块,通常用于实现应用的特性功能,可以配置成按需下载安装。
  • HAR静态共享包,和HSP动态共享包,都是为了实现代码和资源的共享,都可以包含代码、C++库、资源和配置文件。其中HAR不支持在配置文件中声明pages页面,HSP支持配置pages页面。

对于没有UIAbility但依然提供可跳转页面的模块,应该考虑使用Library类型的HSP实现相应的功能,从UIAbility跳转HSP中的页面可参考下列方式:

import { router } from '@kit.ArkUI'; 
import { BusinessError } from '@kit.BasicServicesKit'; 

@Entry 
  @Component 
  struct Index { 
    @State message: string = '跳转到HSP页面'; 

    build() { 
      Row() { 
        Column() { 
          Button() { 
            Text(this.message) 
              .fontSize(24) 
          } 
          .onClick(() => { 
            router.pushUrl({ 
              url: '@bundle:com.example.gotohsppage/library/ets/pages/Index' 
            }).then(() => { 
              console.info("Go to hSP page success."); 
            }).catch((err: BusinessError) => { 
              console.error(`Go to hSP page failed, code is ${err.code}, message is ${err.message}.`); 
            }) 
          }) 
            .width(200) 
        } 
        .width('100%') 
      } 
      .height('100%') 
    } 
  }

其中router.pushUrl方法的入参中url的内容模板为:

'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'

问题5:如何查询应用包的名称、供应商、版本号、版本文本、安装时间、更新时间描述信息

首先通过bundleManager.getBundleInfoForSelf()接口获取应用包的名称、供应商、版本号、版本文本、安装时间、更新时间描述信息。具体可参考示例代码:

import { bundleManager } from '@kit.AbilityKit'; 
 
// 申请获取bundleInfo和applicationInfo 
let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION; 
 
try { 
  bundleManager.getBundleInfoForSelf(bundleFlags, (err, data) => { 
    // 获取应用自身的bundleName 
    const bundleName = data.name; 
    // 获取应用的版本号(versionCode) 
    const versionCode = data.versionCode; 
    // 获取应用的版本名(versionName) 
    const versionName = data.versionName; 
 
    if (err) { 
      console.error(`getBundleInfoForSelf failed: ${err.message}`); 
    } else { 
      console.info(`get bundleName successfully: ${bundleName}`); 
      console.info(`get versionCode successfully: ${versionCode}`); 
      console.info(`get versionName successfully: ${versionName}`); 
      console.info(`getBundleInfoForSelf successfully: ${JSON.stringify(data)}`); 
    } 
  }); 
} catch (err) { 
  console.error(`getBundleInfoForSelf failed: ${JSON.stringify(err)}`); 
}

问题6:通过resourceManager.getStringResource接口获取HSP资源文件报“Resource id invalid”错误

问题现象

通过this.resourceManager.getStringResource($r('app.string.PlayCount').id)获取hsp资源文件报错:

Error message:Resource id invalid

Error code:9001001

SourceCode:returnResource = this.context.resourceManager.getStringSync(id);

可能原因

未创建对应的context,传入的是一个不存在的id值。

解决措施

根据模块名创建上下文Context,然后通过getStringByNameSync获取指定资源名称对应的字符串,具体请参考示例代码:

import { common, application } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { JSON } from '@kit.ArkTS';

@Entry
@Component
struct Index {
  private context = getContext(this) as common.UIAbilityContext;

  build() {
    Column() {
      Button()
        .onClick(() => {
          // 根据模块名创建上下文Context
          let moduleName: string = 'library';
          application.createModuleContext(this.context, moduleName)
            .then((data: common.Context) => {
              console.info(`CreateModuleContext success, data: ${JSON.stringify(data)}`);
              if (data !== null) {
                promptAction.showToast({
                  message: ('成功获取Context')
                });
              }

              // 然后通过getStringByNameSync获取指定资源名称对应的字符串
              try {
                let str = data.resourceManager.getStringByNameSync('shared_desc');
                console.info(`getStringByNameSync, data: ${JSON.stringify(str)}`);
              } catch (error) {
                let code = (error as BusinessError).code;
                let message = (error as BusinessError).message;
                console.error(`getStringByNameSync failed, error code: ${code}, message: ${message}.`);
              }
            })
            .catch((err: BusinessError) => {
              console.error(`CeateMudleContext failed, err code:${err.code}, err msg: ${err.message}`);
            });
        })
    }
  }
}

问题7:如何跨模块访问HSP/HAR包中resources目录的element目录、media目录和rawfile目录资源文件

可以通过以下几种方式访问HSP/HAR里面的资源:

  • 通过createModuleContext(moduleName)接口创建同应用中不同module的上下文,获取resourceManager对象后,调用不同接口访问不同资源。

例如:getContext.createModuleContext(moduleName).resourceManager.getStringByNameSync('app.string.xxx')。

  • 通过"r""r"或"rawfile"引用资源,例如:Text($r('[hsp].string.test_string')),其中“hsp”为HSP包模块/HAR包模块的名称。

说明

在HarmonyOS NEXT Developer Beta1及以上版本支持直接通过"r""r"或"rawfile"引用HSP或者HAR包的资源。

  • 通过HSP包下实现一个资源管理类,以封装对外导出的资源。

HAP中访问HAR包中resources目录的rawfile原始文件资源。

例如在HAR包(不妨设名称为library)的“\library\src\main\resources\rawfile”目录中有“iconHar.png”文件。

在HAR包中将rawfile目录下的“iconHar.png”文件封装成一个方法,例如在“\library\src\main\ets\components\mainpage\MainPage.ets”文件中封装一个方法。

export function rawFileIconHarPng() {
  return $rawfile('iconHar.png');
}

在“\library\Index.ets”文件中导出rawFileIconHarPng()方法。

export { rawFileIconHarPng } from './src/main/ets/components/mainpage/MainPage';

在HAP中的“entry\src\main\ets\pages\Index.ets”文件中通过导入rawFileIconHarPng()方法后直接使用即可。

import { rawFileIconHarPng } from 'library';


@Entry
  @Component
  struct Index {
    build() {
      Row() {
        Column() {
          Image(rawFileIconHarPng())
            .width(100)
            .height(100)
        }
        .width('100%')
      }
      .height('100%')
    }
  }

问题8:如何获取当前HAP的BundleName

通过bundleManager模块的getBundleInfoForSelf接口获取所有信息:

GET_BUNDLE_INFO_DEFAULT:接口默认的参数,返回结果的name字段对应BundleName。

GET_BUNDLE_INFO_WITH_APPLICATION:除基本字段外,能够额外获取到ApplicationInfo字段,ApplicationInfo的name字段也对应BundleName。

下面代码以GET_BUNDLE_INFO_DEFAULT为例:

import { bundleManager } from '@kit.AbilityKit'; 
import { hilog } from '@kit.PerformanceAnalysisKit'; 
import { BusinessError } from '@kit.BasicServicesKit'; 
let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT; 
try { 
    bundleManager.getBundleInfoForSelf(bundleFlags).then((data) => { 
        hilog.info(0x0000, 'testTag', 'getBundleInfoForSelf successfully. Data: %{public}s', JSON.stringify(data)); 
    }).catch((err: BusinessError) => { 
        hilog.error(0x0000, 'testTag', 'getBundleInfoForSelf failed. Cause: %{public}s', err.message); 
    }); 
} catch (err) { 
    let message = (err as BusinessError).message; 
    hilog.error(0x0000, 'testTag', 'getBundleInfoForSelf failed: %{public}s', message); 
}

问题9:如何实现在不使用UIAbility的情况下,能够模块化管理代码,并且各个模块之间可以相互路由跳转

采用HSP进行模块管理,可以实现页面之间的跳转,无需导入导包即可跳转,跳转方式如下:

方式一:所有跳转到HSP内的页面需要使用特定的格式跳转:’@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)’。

方式二:正常entry内模块路由跳转:‘pages/页面所在的文件名(不加.ets后缀)’。

  • entry跳转到HSP页面:使用方式一。
  • HSP跳转到entry页面:使用方式二。
  • HSP跳转到HSP页面:使用方式一。

问题10:如何实现跨模块的页面跳转功能

在业务体系庞大或复杂的情况下,通常会将业务拆分成多个子业务模块,单个子业务模块为一个har/hsp。在该场景下,通常存在从主业务入口跳转到不同子页面模块,或从一个子业务模块A页面跳转到另一个子业务模块B页面的需求。如,从应用首页跳转到登录子业务模块页面。 针对该场景,有以下三种解决方案:

  • 方案一:使用router的命名路由接口router.pushNamedRoute()跳转。
  • 方案二:使用navigation组件跳转。

以从应用入口模块的页面NavigationPage跳转到Login子业务模块页面LoginPage为例。主要包含以下步骤:

在Login模块中开发自定义组件LoginPage(路由跳转目的地),并对外导出。

  @Component 
export struct LoginPage { 
  @Consume('pathStack') pathStack: NavPathStack; 
  @State message: string = 'Login Page'; 
 
  build() { 
    NavDestination() { 
      Column() { 
        Text(this.message) 
          .fontSize(50) 
          .fontWeight(FontWeight.Bold) 
      } 
      .width('100%') 
      .height('100%') 
    } 
    .onBackPressed(() => { 
      this.pathStack.pop(); 
      return true; 
    }) 
  } 
}

在Login模块的入口文件Index.ets中导出自定义组件。

export { LoginPage } from './src/main/ets/pages/loginPage';

在入口模块的oh-package.json5文件中添加对Login模块的依赖。

{ 
  // ... 
  "dependencies": { 
    "@ohos/login": "file:../login" 
  } 
}

入口模块LoginPage页面导入Login模块的自定义组件,并添加到Navigation组件的路由表中。

// 导入Login模块自定义组件 
import { LoginPage } from '@ohos/login'; 

@Entry 
  @Component 
  struct NavigationPage { 
    @Provide('pathStack') pathStack: NavPathStack = new NavPathStack(); 

    @Builder 
    pageMap(name: string) { 
      if (name === 'loginPage') { 
        LoginPage() 
      } 
    } 

    build() { 
      Navigation(this.pathStack) { 
        Button('jump to login page') 
          .onClick(() => { 
            // NavPathInfo第二个参数为自定义参数,可用于信息传递 
            let pathInfo: NavPathInfo = new NavPathInfo('loginPage', new Object()); 
            this.pathStack.pushDestination(pathInfo, true); 
          }) 
      } 
      .navDestination(this.pageMap) 
    } 
  }
  • 方案三:使用基于navigation组件的自定义路由框架跳转。

    方案二虽然可以实现跨模块跳转的功能,但当模块间跳转需求增多,各个模块间将存在非常复杂的依赖关系,甚至会导致多个har/hsp间循环依赖。为了解决模块间的强耦合关系,并且提升页面加载性能,推荐使用自定义路由框架。该方案的整体思路如下:

    1. 自定义一个路由管理模块RouterModule,各个需要使用路由功能的模块均依赖此模块。
    2. 路由管理模块RouterModule内部定义路由栈NavPathStack,并对NavPathStack进行封装,对外提供路由能力。
    3. 在使用Navigation组件时,需将Navigation组件对应的NavPathStack注册到路由管理模块中。通过路由管理模块RouterModule的NavPathStack对路由能力进行控制。
    4. 各个路由页面不再提供组件,转为提供@builder封装的构建函数,再通过WrappedBuilder封装进行传递使用。
    5. 各个路由页面将模块名称、路由名称、WrappedBuilder封装后的构建函数注册到路由管理模块RouterModule的路由表中。
    6. 当路由需要跳转到指定路由时,调用路由管理模块RouterModule的push方法。该方法对指定的模块的路由页面动态导入,并完成路由跳转。

问题11:如何处理错误码9568300:moduleName is not unique

问题原因

用户配置了远端依赖,本地依赖未屏蔽,导致最后的包既有远端依赖,也会有本地依赖,两个依赖moduleName一致导致重复。

解决措施

依赖远端仓库时,屏蔽本地模块,不打包本地模块。

问题12:如何解决依赖的版本冲突问题

发生依赖的版本冲突时,可以给项目级别的oh-package.json5文件配置override字段统一版本。

问题13:当前支持的HAP安装到设备的方式有哪些

支持两种方式安装HAP包到设备:

  • 通过hdc命令安装:hdc install xxx.hap
  • 将.app文件上架到应用市场后,通过应用市场将HAP分发部署到设备中

问题14:如何让两个HSP不相互依赖,使用对方的组件

可以将需要共用的组件抽离出来,然后放到一个共享包中使用。

问题15:为什么同一App下的HSP文件vendor参数不同时会安装失败

因为安装时会有参数一致性的校验,需要保证module.json中app标签下的vendor字段一致。