在 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
- 在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,新增后的模块目录结构:
- 在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。
通过点击图中所示的按钮, 来进行切换product, 和对应的 entry target。
1.2 定制HAR多目标构建产物
1.2.1 新建HAR库
1、鼠标移到工程目录顶部,单击右键,选择New > Module,在工程中添加模块。
2、在Choose Your Ability Template界面中,选择Static Library,并单击Next
3、在Configure New Module界面中,设置新添加的模块信息,设置完成后,单击Finish完成创建。
4、在根目录的 build-profile.json 文件中将library 加入到编译中。
{
"name": "library",
"srcPath": "./library",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
5、编译HAR
1.2.2 构建HAR多目标产物
1.2.2.1 定义产物的source源码集-sourceRoots
- 在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,新增后的模块目录结构:
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 渠道:
Custom渠道:
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 还是 OhosHarContext都没有获取当前渠道的API,就很奇怪, 这种动态化, 多渠道打包正好需要才对。 倒是 OhosAppContext 提供了 getCurrentProduct 获取当前渠道的信息。
2.2 分Target修改oh-package.json5依赖
既然OhosHarContext 不提供类似 getCurrentTarget的方法, 那我们去打HAR或者HAR的特殊渠道,怎么区分那? 答案是从BuildProfile 文件入手。
可以看出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, 就不用这么麻烦了。