跳到主要内容

产品特性按需分发(ArkTS)

场景介绍

随着HarmonyOS应用的持续发展,应用的功能将越来越丰富,实际上80%的用户使用时长都会集中在20%的特性上,其余的功能可能也仅仅是面向部分用户。为了避免用户首次下载应用耗时过长,及过多占用用户空间,应用市场服务提供按需分发的能力,支持用户按需动态下载自己所需的增强特性。

基本概念

按需分发:一个应用程序被打包成多个安装包,安装包包含了所有的应用程序代码和静态资源。用户从应用市场下载的应用只包含基本功能的安装包,当用户需要使用增强功能时,相应安装包将会从服务器下载到设备上(应用发布请参考发布HarmonyOS应用)。

业务流程

  1. 用户下载A应用的基础包。
  2. 用户使用增强功能。
  3. 应用通过API下载动态安装包。
  4. 动态安装包下载完成。
  5. 通过on接口告知用户下载结果。

约束与限制

  • 应用需要上架应用市场。
  • 产品特性按需分发功能支持Phone、Tablet、PC/2in1设备。并且从5.1.1(19)版本开始,新增支持TV设备。
  • 产品特性按需分发接入调试功能支持ARM版本、X86版本的模拟器。

接口说明

产品特性按需分发场景提供以下ArkTS接口,具体API说明详见接口文档

接口名描述
getInstalledModule(moduleName: string): InstalledModule查询模块安装信息接口。
createModuleInstallRequest(context: common.UIAbilityContextcommon.ExtensionContext): ModuleInstallRequest
addModule(moduleName: string): ReturnCode添加要按需加载的模块名。
fetchModules(moduleInstallRequest: ModuleInstallRequest): Promise<ModuleInstallSessionState>按需加载请求接口,异步返回结果。
cancelTask(taskId: string): ReturnCode取消下载任务接口。
showCellularDataConfirmation(context: common.UIAbilityContextcommon.ExtensionContext, taskId: string): ReturnCode
on(type: 'moduleInstallStatus', callback: Callback<ModuleInstallSessionState>, timeout: number): void监听当前应用下载任务的进度。
off(type: 'moduleInstallStatus', callback?: Callback<ModuleInstallSessionState>): void取消监听当前应用下载任务的进度。

开发步骤

获取模块安装信息

  1. 导入moduleInstallManager模块及相关公共模块。

    // LoadInstallService.ets
    import { moduleInstallManager } from '@kit.AppGalleryKit';
  2. 构造参数。

    入参为需要查询的模块名称。

    const moduleName: string = 'AModule';
  3. 调用getInstalledModule方法,将步骤2中构造的参数传入模块中的getInstalledModule方法。

    const moduleInfo: moduleInstallManager.InstalledModule = moduleInstallManager.getInstalledModule(moduleName);

创建按需加载的请求实例

  1. 导入moduleInstallManager模块及相关公共模块。

    // LoadInstallService.ets
    import { moduleInstallManager } from '@kit.AppGalleryKit';
    import type { common } from '@kit.AbilityKit';
  2. 构造参数。

    入参为当前应用的上下文context,只支持UIAbilityContextExtensionContext类型的上下文,其中UIAbilityContext类型的上下文是要校验当前应用是否在前台,如果不在前台,则会被拒绝调用。

    const context: common.UIAbilityContext | common.ExtensionContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
  3. 调用createModuleInstallRequest方法,将步骤2中构造的参数依次传入模块中的createModuleInstallRequest方法。

    const myModuleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider();
    const myModuleInstallRequest: moduleInstallManager.ModuleInstallRequest = myModuleInstallProvider.createModuleInstallRequest(context);

请求按需加载模块

  1. 导入moduleInstallManager模块及相关公共模块。

    // LoadInstallService.ets
    import type { common } from '@kit.AbilityKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { moduleInstallManager } from '@kit.AppGalleryKit';
  2. 构造参数。

    入参为当前要按需加载的模块名。

    const moduleNameA: string = 'AModule';
    const moduleNameB: string = 'BModule';
  3. 调用ModuleInstallRequest中的addModule方法,将步骤2中构造的参数依次传入模块中的addModule方法。

    let myModuleInstallRequest: moduleInstallManager.ModuleInstallRequest;
    try {
    const myModuleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider();
    const context: common.UIAbilityContext | common.ExtensionContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    myModuleInstallRequest = myModuleInstallProvider.createModuleInstallRequest(context);
    const aResult: moduleInstallManager.ReturnCode = myModuleInstallRequest.addModule(moduleNameA);
    const bResult: moduleInstallManager.ReturnCode = myModuleInstallRequest.addModule(moduleNameB);
    hilog.info(0, 'TAG', 'aResult:' + aResult + ' bResult:' + bResult);
    } catch (error) {
    hilog.error(0, 'TAG', `addModule onError.code is ${error.code}, message is ${error.message}`);
    }
  4. 调用fetchModules方法,将步骤3中的myModuleInstallRequest传入模块中的fetchModules方法。

    try {
    moduleInstallManager.fetchModules(myModuleInstallRequest)
    .then(() => {
    hilog.info(0, 'TAG', 'Succeeded in fetching Modules data.');
    })
    } catch (error) {
    hilog.error(0, 'TAG', `fetching Modules onError.code is ${error.code}, message is ${error.message}`);
    }

使用动态模块

假如应用A由entry.hap、AModulelib.hsp两个包组成,其中entry是基础包,AModulelib扩展是功能包(创建方式请参考应用程序包开发与使用)。通过应用市场下载安装只会下载安装entry包,在entry包里面可以通过fetchModules接口动态下载AModulelib包,并使用动态import技术调用AModulelib里的方法和组件。

AModulelib中主要实现如下:

  • 在动态模块AModulelib的module.json5中设置deliveryWithInstall为false,来标识当前AModulelib在用户主动安装应用A的时候不会一起下载安装。

    {
    "module": {
    "name": "AModulelib",
    "deliveryWithInstall": false
    }
    }
  • 在动态模块AModulelib中定义add方法和DateComponent组件。其中add方法用于计算加法,DateComponent用于显示文本。

    Calc.ets定义如下:

    export function add(a:number, b:number) {
    return a + b;
    }

    DateComponent.ets定义如下:

    @Component
    struct DateComponent {
    build() {
    Column() {
    Text('我是AModulelib中的组件')
    .margin(10);
    }
    .width(300).backgroundColor(Color.Yellow);
    }
    }

    @Builder
    export function showDateComponent() {
    DateComponent()
    }
  • 在AModulelib的AModulelib/Index.ets中导出add方法和showDateComponent方法。

    export { add } from './src/main/ets/utils/Calc';
    export { showDateComponent } from './src/main/ets/components/DateComponent';

entry中主要实现如下:

  • 在entry基础模块中,增加动态依赖配置。entry的oh-package.json5中使用dynamicDependencies来动态依赖AModulelib模块。

    {
    "dynamicDependencies": {
    "AModulelib": "file:../AModulelib"
    }
    }
  • 在entry中使用动态模块AModulelib模块里面的方法和组件。在调用AModulelib中的功能前需要判断AModulelib是否已经加载,未加载时请参考请求按需加载的接口完成加载。

    import { moduleInstallManager } from '@kit.AppGalleryKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { BusinessError, Callback } from '@kit.BasicServicesKit';
    import { common } from '@kit.AbilityKit';

    @Entry
    @Component
    struct Index {
    @BuilderParam AModulelibComponent: Function;
    @State countTotal: number = 0;
    @State isShow: boolean = false;

    build() {
    Row() {
    Column() {
    Button(`调用增量模块中的add功能:3+6`)
    .onClick(() => {
    this.initAModulelib(() => {
    import('AModulelib').then((ns: ESObject) => {
    this.countTotal = ns.add(3, 6);
    }).catch((error: BusinessError) => {
    hilog.error(0, 'TAG', `add onError.code is ${error.code}, message is ${error.message}`);
    })
    })
    });
    Text('计算结果:' + this.countTotal)
    .margin(10);
    Button(`调用增量模块中的showDateComponent功能`)
    .onClick(() => {
    this.initAModulelib(() => {
    import('AModulelib').then((ns: ESObject) => {
    this.AModulelibComponent = ns.showDateComponent;
    this.isShow = true;
    }).catch((error: BusinessError) => {
    hilog.error(0, 'TAG', `showDateComponent onError.code is ${error.code}, message is ${error.message}`);
    })
    })
    }).margin({
    top: 10, bottom: 10
    });
    if (this.isShow) {
    this.AModulelibComponent()
    }
    }
    .width('100%')
    }
    .height('100%')
    }

    private showToastInfo(msg: string) {
    this.getUIContext().getPromptAction().showToast({
    message: msg,
    duration: 2000
    });
    }

    /**
    * 检查是否已加载AModulelib包
    *
    * @param successCallBack 回调
    */
    private initAModulelib(successCallBack: Callback<void>): void {
    try {
    const result: moduleInstallManager.InstalledModule = moduleInstallManager.getInstalledModule('AModulelib');
    if (result?.installStatus === moduleInstallManager.InstallStatus.INSTALLED) {
    hilog.info(0, 'TAG', 'AModulelib installed');
    successCallBack && successCallBack();
    } else {
    // AModulelib模块未安装, 需要调用fetchModules下载AModulelib模块
    hilog.info(0, 'TAG', 'AModulelib not installed');
    this.fetchModule('AModulelib', successCallBack)
    }
    } catch (error) {
    hilog.error(0, 'TAG', `getInstalledModule onError.code is ${error.code}, message is ${error.message}`);
    }
    }

    /**
    * 添加监听事件
    *
    * @param successCallBack 回调
    */
    private onListenEvents(successCallBack: Callback<void>): void {
    const timeout = 3 * 60; // 单位秒, 默认最大监听时间为30min(即30*60秒)
    moduleInstallManager.on('moduleInstallStatus', (data: moduleInstallManager.ModuleInstallSessionState) => {
    // 返回成功
    if (data.taskStatus === moduleInstallManager.TaskStatus.INSTALL_SUCCESSFUL) {
    successCallBack && successCallBack();
    this.showToastInfo('install success');
    }
    }, timeout)
    }

    /**
    * 加载指定包
    *
    * @param moduleName 需要加载的安装包名称
    * @param successCallBack 回调
    */
    private fetchModule(moduleName: string, successCallBack: Callback<void>) {
    try {
    hilog.info(0, 'TAG', 'handleFetchModules start');
    const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    const moduleInstallProvider: moduleInstallManager.ModuleInstallProvider =
    new moduleInstallManager.ModuleInstallProvider();
    const moduleInstallRequest: moduleInstallManager.ModuleInstallRequest =
    moduleInstallProvider.createModuleInstallRequest(context);
    if (!moduleInstallRequest) {
    hilog.warn(0, 'TAG', 'moduleInstallRequest is empty');
    return;
    }
    moduleInstallRequest.addModule(moduleName);
    moduleInstallManager.fetchModules(moduleInstallRequest)
    .then((data: moduleInstallManager.ModuleInstallSessionState) => {
    hilog.info(0, 'TAG', 'Succeeded in fetching Modules result.');
    if (data.code === moduleInstallManager.RequestErrorCode.SUCCESS) {
    this.onListenEvents(successCallBack)
    } else {
    hilog.info(0, 'TAG', 'fetchModules failure');
    }
    })
    .catch((error: BusinessError) => {
    hilog.error(0, 'TAG', `fetchModules onError.code is ${error.code}, message is ${error.message}`);
    })
    } catch (error) {
    hilog.error(0, 'TAG', `handleFetchModules onError.code is ${error.code}, message is ${error.message}`);
    }
    }
    }

运行结果效果图:

接入调试功能

产品特性按需分发为开发者提供接入调试功能,支持开发者在接入过程中进行调试,应用无需上架应用市场。假如应用A由entry.hap、AModulelib.hsp两个包组成,其中entry是基础包,AModulelib是扩展功能包(创建方式请参考应用程序包开发与使用)。

  1. 使用调试证书签名应用/服务,本地编译构建出entry.hap、AModulelib.hsp,可通过HDC命令安装或DevEco Studio直接安装基础包。

    hdc install entry.hap
  2. 打开开发者调试模式:进入设置 -> 机型 -> 关于手机,连续点击软件版本7次,弹出“开启“开发者模式””,点击“确认开启”。

  3. 访问设备沙箱路径,在应用el2级别加密数据目录下,创建cache/moduleinstall/<ModuleName>目录(这里<ModuleName>是AModulelib),将模块调试包AModulelib.hsp上传至对应模块目录下(请确保模块调试包文件应有读写权限)。

  4. 按照创建按需加载的请求实例请求按需加载的接口使用动态模块,无需改动参数即可安装好模块调试包。监听到安装成功后,对应模块目录下的文件会被自动删除。