鸿蒙多渠道打包

3,652 阅读9分钟

在 Android 开发中, 有多渠道打包的概念。主要是为不同的应用市场打不同的渠道包, 去做一些差异化的区分。

在鸿蒙中, 鸿蒙APP只上华为应用市场, 有必要打多渠道包嘛?

答案是需要的, 通常情况下,应用厂商会根据不同的部署环境,不同的目标人群,不同的运行环境等,将同一个应用定制为不同的版本,如国内版、国际版、普通版、VIP版、免费版、付费版等。这就需要多渠道打包。

同时公司内部HAR的开发, 同一个 HAR包同时给多个APP提供使用,但是又有一些差异性。使用多分支开发可以解决这个问题 但是不易维护。 最好的就是在同一个分支进行开发, 根据源码差异和打包脚本的配置, 可以输出多渠道的HAR产物,供其他APP集成。

1、配置多目标产物(类似于多渠道)

在了解鸿蒙应用的多渠道打包前,先了解target和product的概念:

  • 工程内的每一个Entry/Feature模块,对应的构建产物为HAP,HAP是应用/服务可以独立运行在设备中的形态。由于在不同的业务场景中,同一个模块可能需要定制不同的功能或资源,因此引入target的概念。一个模块可以定义多个target,每个target对应一个定制的HAP,通过配置可以实现一个模块构建出不同的HAP。
  • 一个HarmonyOS工程的构建产物为APP包,APP包用于应用/服务发布上架应用市场。由于不同的业务场景,需要定制不同的应用包,因此引入product概念。一个工程可以定义多个product,每个product对应一个定制化应用包,通过配置可以实现一个工程构建出多个不同的应用包。

简单来讲, target 对应 HAP或者HAR的多目标构建产物, product 对应HSP的多目标构建产物。

参考 华为官方文档:配置多目标产物

可以发现官方支持HAP, HAR,APP的多目标产物构建。

其中HAP多目标构建产物支持以下区分:

  • 产物的HAP包名
  • 产物的deviceType
  • 产物的distributionFilter
  • 产物preloads的分包
  • 产物的source源码集-pages
  • 产物的source源码集-sourceRoots
  • 产物的资源
  • 产物的icon、label、launchType
  • C++工程依赖的.so文件

HAR多目标构建产物支持以下区分:

  • 产物的deviceType
  • C++工程依赖的.so文件
  • 产物的资源

APP多目标构建产物支持以下区分:

  • APP包名和供应商名称
  • bundleName
  • bundleType
  • 签名配置信息
  • icon和label
  • 包含的target

可以看出支持差异化构建的类型还是挺多的, 我们下面仅以source源码集-sourceRoots 进行示例, 其他类型的使用, 大家可以参考官方文档

1.1 定制HAP多目标构建产物

1.1.1 构建HAP多目标产物

在模块的主代码空间(src/main)下,承载着开发者编写的公共代码。如果开发者需要实现不同target之间的差异化逻辑,可以使用差异化代码空间(sourceRoots)。配合差异化代码空间的能力,可以在主代码空间中代码不变的情况下,针对不同的target,编译对应的代码到最终产物中。

1.1.1.1 定义产物的source源码集-sourceRoots

  1. 在entry模块的build-profile.json5中添加sourceRoots:
    {
      "apiType": "stageMode",
      "buildOption": {},
      "targets": [ 
        { 
          "name": "default", 
          "source": { 
            "sourceRoots": ["./src/default"] // 配置target为default的差异化代码空间
          } 
        }, 
        { 
          "name": "custom", 
          "source": { 
            "sourceRoots": ["./src/custom"] // 配置target为custom的差异化代码空间
          } 
        } 
      ]
    }

2. 在src目录下新增default/Test.ets和custom/Test.ets,新增后的模块目录结构:

pAk0j54.png

  1. 在default/Test.ets中写入代码:
export const getName = () => "default"

2. 在custom/Test.ets中写入代码:

export const getName = () => "custom"

5、修改src/main/ets/pages/Index.ets的代码:

import { getName } from 'entry/Test'; // 其中entry为模块级的oh-package.json5中的name字段的值
    @Entry
    @Component
    struct Index { 
      @State message: string = getName(); 
      build() { 
        RelativeContainer() { 
          Text(this.message) 
        } 
        .height('100%') 
        .width('100%') 
      }
    }

6、 在工程级的build-profile.json5中配置targets:

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS",
      },
      {
        "name": "custom",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS",
      }
    ],
    "buildModeSet": [
      {
        "name": "debug",
      },
      {
        "name": "release"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "custom",
          "applyToProducts": [
            "custom"
          ]
        }
      ]
    }
  ]
}

7、 Sync完成后,选择entry的target为default,点击Run,界面展示default;选择entry的target为custom,点击Run,则界面展示custom。

pAkBYGj.png

通过点击图中所示的按钮, 来进行切换product, 和对应的 entry target。

default 运行结果: pAkBRQ1.jpg

custom 运行结果: pAkB4eK.jpg

1.2 定制HAR多目标构建产物

1.2.1 新建HAR库

1、鼠标移到工程目录顶部,单击右键,选择New > Module,在工程中添加模块。

pAknZOx.png

2、在Choose Your Ability Template界面中,选择Static Library,并单击Next

pAkmqYQ.png

3、在Configure New Module界面中,设置新添加的模块信息,设置完成后,单击Finish完成创建。

pAkmXSs.png

4、在根目录的 build-profile.json 文件中将library 加入到编译中。

    {
      "name": "library",
      "srcPath": "./library",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        }
      ]
    }

5、编译HAR

pAkDNkD.png

1.2.2 构建HAR多目标产物

1.2.2.1 定义产物的source源码集-sourceRoots

  1. 在library模块的build-profile.json5中添加sourceRoots:
{
  "apiType": "stageMode",
  "buildOption": {},
  "targets": [
    {
      "name": "default",
      "source": {
        "sourceRoots": ["./src/default"] // 配置target为default的差异化代码空间
      }
    },
    {
      "name": "custom",
      "source": {
        "sourceRoots": ["./src/custom"] // 配置target为custom的差异化代码空间
      }
    }
  ]
}

2、在src目录下新增default/Channel.ets和custom/Channel.ets,新增后的模块目录结构: pAkgah8.png

3、在default/Channel.ets中写入代码:

export const getName = () => "default"

4、在custom/Channel.ets中写入代码:

export const getChannel = () => "Custom"

5、修改src/main/ets/pages/Index.ets的代码:

import { getChannel } from 'library/Channel';
@Entry
@Component
struct Index {
  @State message: string = getChannel();
  build() {
    RelativeContainer() {
      Text(this.message)
    }
    .height('100%')
    .width('100%')
  }
}

6、在工程级的build-profile.json5中配置library 的 targets:

    {
      "name": "library",
      "srcPath": "./library",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "custom",
          "applyToProducts": [
            "custom"
          ]
        }
      ]
    }

7、sync 同步之后, 使用一下命令, 打两个渠道的产物

//default har, debug包
hvigorw --mode module -p product=default -p module=library@default -p buildMode=debug assembleHar

//custom har, debug包
hvigorw --mode module -p product=default -p module=library@custom -p buildMode=debug assembleHar

Default 渠道:

pAkgX9O.png

Custom渠道:

pAkgj3D.png

1.3 配置APP多目标构建产物

APP用于应用/服务上架发布,针对不同的应用场景,可以定制不同的product,每个product中支持对bundleName、bundleType、签名信息、icon和label以及包含的target进行定制。

在此, 只示例target定制,其他信息的定制, 可以参考官方文档:

1.3.1 定义product中包含的target

开发者可以选择需要将定义的target分别打包到哪一个product中,每个product可以指定一个或多个target。

同时每个target也可以打包到不同的product中,但是同一个module的不同target不能打包到同一个product中(除非该module的不同target配置了不同的deviceType或distributionFilter/distroFilter)。

例如,定义default、free和pay三个target,现需要将default target打包到default product中;将free target打包到productA中;将pay target打包到productB中,对应的示例代码如下所示:

{ 
  "app": { 
    "signingConfigs": [], //此处通过界面配置签名后会自动生成相应的签名配置,本文略 
    "products": [ 
      { 
        "name": "default", 
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example00.com"  
      }, 
      { 
        "name": "productA", 
        "signingConfig": "productA",
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example01.com"  
      }, 
      { 
        "name": "productB", 
        "signingConfig": "productB",  
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example02.com" 
      } 
    ], 
  "modules": [ 
    { 
      "name": "entry", 
      "srcPath": "./entry", 
      "targets": [ 
        { 
          "name": "default",  //将default target打包到default APP中 
          "applyToProducts": [ 
            "default" 
          ] 
        }, 
        { 
          "name": "free",  //将free target打包到productA APP中 
          "applyToProducts": [ 
            "productA" 
          ] 
        }, 
        { 
          "name": "pay",  //将pay target打包到productB APP中 
          "applyToProducts": [ 
            "productB" 
          ] 
        } 
      ] 
    } 
  ] 
}

2、定制构建

回到最初的问题。 部门开发的业务HAR, 要给好多个APP使用。由于某些原因。这个HAR依赖的第三方HAR, 多渠道时, 对这个第三方HAR依赖的版本不同, 单分支的情况下, 能不能实现 同一个HAR, 多渠道时, 依赖的第三方HAR 版本不同那?

答案是可以的。

像Android开发一样, 鸿蒙在编译的过程,支持对编译信息, 签名信息, 依赖等信息进行动态变更, 已达到动态化编译的目的。

我们仅以依赖信息为例, 其他的能力,大家可以参考官方文档。

2.1 自定义任务修改oh-package.json5依赖

1、HAP的编译依赖信息变更

// 模块级hvigorfile.ts
import {hapTasks,OhosHapContext,OhosPluginId,Target} from '@ohos/hvigor-ohos-plugin';
import { hvigor, HvigorNode, HvigorPlugin} from '@ohos/hvigor';
import * as fs from 'fs';
export function customPlugin(options: OnlineSignOptions): HvigorPlugin {

    const channel = "custom"
    return {
        pluginId: 'customPlugin',
        context() {
            return {
                signConfig: options
            };
        },
        async apply(currentNode: HvigorNode): Promise<void> {
            const hapContext = currentNode.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosHapContext;
            const dependency = hapContext.getDependenciesOpt({});//获取dependency依赖
            // dependency["library"]="file:library.har" 替换本地依赖
            // dependency["@ohos/xxx"]="^3.0.0" 替换远端依赖
            hapContext.setDependenciesOpt(dependency);}
    }
};
export default {
    system: hapTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[customPlugin()]         /* Custom plugin to extend the functionality of Hvigor. */
}

2、HAR的编译依赖信息变更

// 模块级hvigorfile.ts
import {harTasks,OhosHarContext,OhosPluginId,Target} from '@ohos/hvigor-ohos-plugin';
import { hvigor, HvigorNode, HvigorPlugin} from '@ohos/hvigor';
import * as fs from 'fs';
export function customPlugin(options: OnlineSignOptions): HvigorPlugin {

    const channel = "custom"
    return {
        pluginId: 'customPlugin',
        context() {
            return {
                signConfig: options
            };
        },
        async apply(currentNode: HvigorNode): Promise<void> {
            const harContext = currentNode.getContext(OhosPluginId.OHOS_HAR_PLUGIN) as OhosHarContext;
            const dependency = harContext.getDependenciesOpt({});//获取dependency依赖
            // dependency["library"]="file:library.har" 替换本地依赖
            // dependency["@ohos/xxx"]="^3.0.0" 替换远端依赖
            harContext.setDependenciesOpt(dependency);}
    }
};
export default {
    system: harTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[customPlugin()]         /* Custom plugin to extend the functionality of Hvigor. */
}

注意HAP使用的是OhosHapContext, HAR使用的是OhosHarContext。具体其他的API可以参考:

OhosHapContext

看官方文档, 不论是OhosHapContext 还是 OhosHarContext都没有获取当前渠道的API,就很奇怪, 这种动态化, 多渠道打包正好需要才对。 倒是 OhosAppContext 提供了 getCurrentProduct 获取当前渠道的信息。

2.2 分Target修改oh-package.json5依赖

既然OhosHarContext 不提供类似 getCurrentTarget的方法, 那我们去打HAR或者HAR的特殊渠道,怎么区分那? 答案是从BuildProfile 文件入手。

pAAiqaj.png

可以看出BuildProfile 文件中,定义了TARGET_NAME 的属性。 我们是不是可以通过构建脚本, 直接读取 可以看出BuildProfile的 定义了TARGET_NAME属性。 直接判断当前是那个target在构建了。

libray 的 hvigorfile.ts中

// 模块级hvigorfile.ts
import { harTasks, OhosHarContext, OhosPluginId, Target } from '@ohos/hvigor-ohos-plugin';
import { hvigor, HvigorNode, HvigorPlugin } from '@ohos/hvigor';
import * as fs from 'fs';

export function customPlugin(options: OnlineSignOptions): HvigorPlugin {

  const channel = "custom"

  return {
    pluginId: 'customPlugin',
    context() {
      return {
        signConfig: options
      };
    },
    async apply(currentNode: HvigorNode): Promise<void> {
      const targetName = hvigor.getParameter().getExtParm('product');
      console.log("targetName:", targetName);

      if (targetName == "custom") {

        console.log("当前渠道为custom, 要做特殊处理");
        const harContext = currentNode.getContext(OhosPluginId.OHOS_HAR_PLUGIN) as OhosHarContext;
        const dependency = harContext.getDependenciesOpt({}); //获取dependency依赖
        // dependency["library"]="file:library.har" 替换本地依赖
        // dependency["@ohos/xxx"]="^3.0.0" 替换远端依赖
        harContext.setDependenciesOpt(dependency);
      }
    }
  }
}
;

export default {
  system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
  plugins: [customPlugin()]         /* Custom plugin to extend the functionality of Hvigor. */
}

编译输出:

"D:\DevEco Studio\tools\node\node.exe" "D:\DevEco Studio\tools\hvigor\bin\hvigorw.js" --mode module -p module=entry@custom -p product=custom -p requiredDeviceType=phone assembleHap --analyze=normal --parallel --incremental --daemon
TARGET_NAME: custom
targetName: custom
当前渠道为custom, 要做特殊处理

验证后,发现可以实现。

我承认, 有取巧的成分了, 希望官方能给个获取当前Target 的 API, 就不用这么麻烦了。

3、参考

配置多目标产物

定制构建-动态修改签名和编译配置

扩展构建-插件上下文

BuildProfile文件生成