克隆大作战!一份代码如何做出多个产品包?鸿蒙开发多目标产物配置

日常开发中,因为某些需求(黑的、白的、灰的),我们经常需要将一份项目代码编译成不同的产品安装包。

鸿蒙开发IDE-DevEco是支持多目标产物配置的,因此就进行了一番研究。

需求

首先说一下我们的常用诉求:

  • 应用信息差异化:名称、图标、包名、版本号、发布者;
  • 应用内容差异化:纯逻辑代码、页面、资源(配置项、图片)、服务卡片、依赖项;
  • 工程信息差异化:签名、模块化;

如何实现

DevEco的的多产物配置

配置多目标产物-能力说明

DevEco的多目标产物配置能力分为HAP/HAR和APP。

Product多产物

应用级的信息都是在Product产物中配置。

配置文件为:./build-profile.json5,app-products

{
  "name": "Ultimate",
  // 签名配置名称
  "signingConfig": "Ultimate",
  // 包名
  "bundleName": "com.example.ultimate.app",
  // 应用图标
  "icon": "$media:app_icon_ultimate",
  // 应用名称
  "label": "$string:app_name_ultimate",
  // 版本code
  "versionCode": 10000,
  // 版本号
  "versionName": "1.0.0",
  // 发布者
  "vendor": "Ultimate"
}

资源文件:

.
├── AppScope
│   ├── app.json5
│   ├── resources
│   │   └── base
│   │       ├── element
│   │       │   └── string.json
│   │       └── media
│   │           └── app_icon.png
│   │           └── app_icon_ultimate.png

string.json内容:

{
  "string": [
    {
      "name": "app_name",
      "value": "应用"
    },
    {
      "name": "app_name_ultimate",
      "value": "应用Ultimate"
    }
  ]
}

Product产物的配置项中包含了名称、图标、包名、版本号、发布者等信息。
你如果按照官方文档-能力说明里的配置进行修改,就会得出上面的配置方法,AppScope-resources-media里有两个图片;string.json里也有两个应用名称。

这种方法运行没有什么问题,但是在最终的打包成果中,无论你构建哪个Product对应的HAP包,resources目录里都会包含两个图标文件。

如果你要配置十几个不同的Product,那这里就有十几个图标文件。这效果显然不是我们想要的。

实际上能力说明文档里对于APP多产物配置的说明不是全部,在DevEco中,./build-profile.json5,app-products,选中products跳转到“声明和用法”,我们就能看到它支持的所有配置项。

{
        "products": {
          "description": "This field is used to describe different product types defined by the openHarmony application. By default, a default product exists and different signature materials can be specified.",
          "type": "array",
          "items": {
            "type": "object",
            "oneOf": [
              {
                "propertyNames": {
                  "enum": [
                    "name",
                    "signingConfig",
                    "bundleName",
                    "buildOption",
                    "runtimeOS",
                    "compileSdkVersion",
                    "compatibleSdkVersion",
                    "compatibleSdkVersionStage",
                    "targetSdkVersion",
                    "bundleType",
                    "label",
                    "icon",
                    "versionCode",
                    "versionName",
                    "resource",
                    "output",
                    "vendor"
                  ]
                }
              }
            ]
          }
        }
}

里面包含了resource配置,所以我们可以通过多个resource目录进行差异化配置。

{
  "name": "Ultimate",
  // ultimate版本包名
  "bundleName": "com.example.ultimate.app",
  // ultimate版本指定资源目录
  "resource": {
    "directories": [
      "./AppScope/resources_ultimate"
    ]
  }
}

资源文件:

.
├── AppScope
│   ├── app.json5
│   ├── resources
│   │   └── base
│   │       ├── element
│   │       │   └── string.json
│   │       └── media
│   │           └── app_icon.png
│   ├── ultimateRes
│   │   └── base
│   │       ├── element
│   │       │   └── string.json
│   │       └── media
│   │           └── app_icon.png

这样每个Product单独打包时,HAP都只会包含自己的图标。

Target多产物

配置文件为:./build-profile.json5,modules:

  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default",
            "release"
          ]
        },
        {
          "name": "ultimate",
          "applyToProducts": [
            "ultimate"
          ]
        }
      ]
    }
  ]

配置文件为:./entry/build-profile.json5,targets:

    {
      "name": "ultimate",
      "source": {
        "pages": [
          "pages/Index",
          "pages/Detail",
        ],
        "sourceRoots": [
          "./src/ultimate"
        ]
      },
      "resource": {
        "directories": [
          "./src/main/resources_ultimate",
          "./src/main/resources",
        ]
      }
    }

页面差异化

ultimate版本包含的独有页面可以通过source-pages进行配置添加。
也可以通过resource-directoriesresources_ultimate-base-profile目录下添加main_pages.json文件进行差异化配置。

公共页面的差异化配置

若某些公共页面上的显示内容有差异化,比如文本或者图片和普通版本不同,可以通过resource-directoriesresources_ultimate目录下添加同名的media资源或者element数据进行覆盖。

需要注意的是:

请注意,如果target引用的多个资源文件目录下,存在同名的资源,则在构建打包过程中,将按照配置的资源文件目录顺序进行选择。

也就是说在directories列表中,越靠前优先级越高。

公共页面的差异化逻辑

差异化逻辑所在的ets文件目录通过source-sourceRoots进行配置添加。
比如同样的一个功能,标准版本是通过网络请求获取数据,ultimate版本是读取本地rawfile,就可以将这部分逻辑封装到同名类中进行差异化配置。
具体操作参照官方文档。

不过有一点需注意的是sourceRootsresource的配置不同,sourceRoots中的文件是以增量的方式拷贝到主目录下的。
也就是说虽然"sourceRoots": ["./src/ultimate"]只配了一个目录,但最终打包的代码是ultimate中的代码+main目录的代码。

服务卡片差异化

和页面差异化类似,服务卡片的显示也依赖于配置文件resources-base-profile-form_config.json

因此差异化主要是通过修改此配置进行的。

在差异化目录的配置文件中配置了几个卡片,就会显示几个卡片。

模块差异化和依赖差异化

oh-package.json5中的依赖项并没有多产物的差异化配置能力。

如果你的差异化能力依赖了某个第三方组件,你又不想每个应用都将此组件代码包含进去,那么就需要将此依赖项封装成单独的har包,然后在modules中进行配置。

  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default",
            "release"
          ]
        },
        {
          "name": "ultimate",
          "applyToProducts": [
            "ultimate"
          ]
        }
      ]
    },
    {
      "name": "har",
      "srcPath": "./har",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "ultimate"
          ]
        }
      ]
    }
  ]

不足之处

页面只是差异化了配置,没有差异化实际代码

页面中的配置,只是修改了当前HAP支持跳转的页面列表,而不支持跳转的页面源代码还是存在的。

那么是否可以将页面代码和配置同时进行差异化呢?

比如将页面代码放到source-sourceRoots中?

主要的问题还是在于鸿蒙的路由管理方案!

基于页面pages配置的方案不可行,因为页面代码放到sourceRoots指定的目录后,就无法通过pages/xxxx来定位了。

那么是否可以不用pages配置呢?有,那就是命名路由,但是entry不支持命名路由。
HAR/HSP支持命名路由,但还有一个问题,命名路由需要先import对应的页面类。而差异化页面的入口一般都是公共页面,没办法显式的import。

至于Navigation页面,如果是路由表方案,同样存在页面路径的问题。其他的方案比较复杂,暂时还没验证。

整体看,无法满足需求。

服务卡片的差异化配置,也没有差异化服务卡片的代码

和上面的页面差异化类似,服务卡片的配置文件中也包含了文件路径src

{
  "forms": [
    {
      "name": "Card",
      "displayName": "$string:Card_display_name",
      "description": "$string:Card_desc",
      "src": "./ets/card/pages/Card.ets",
      "uiSyntax": "arkts",
    }
  ]
}

虽然我可以将服务卡片的代码挪到sourceRoots中,但是src的值是限定死的main目录的子目录,无法指定到sourceRoots目录。
因此,虽然在多产物配置中可以隐藏不需要的卡片,但卡片的代码还是会打进包里,更麻烦的是元服务单包有2M的限制,工程大了就很麻烦。

多产物配置涉及列表的地方不是合并操作

核心问题还是在于resource-directories配置,它在处理同名文件时,只能存在一个,对于内容无法合并。

在常见的产品场景中经常有功能组合的情况,比如产品A包含卡片1,卡片2;产品B包含卡片2,卡片3;产品C包含卡片3;

那现在就要在产品A、B、C对应的form_config.json文件中完整的配置其需要的卡片信息,有大量的重复配置,后期修改也麻烦。

类似:

// 产品A的form_config.json
{
  "forms": [
    {
      "name": "Card1",
      // ...
    },
    {
      "name": "Card2",
      // ...
    }
  ]
}
// 产品B的form_config.json
{
  "forms": [
    {
      "name": "Card2",
      // ...
    },
    {
      "name": "Card3",
      // ...
    }
  ]
}
// 产品C的form_config.json
{
  "forms": [
    {
      "name": "Card3",
      // ...
    }
  ]
}

更加高效的方案应该是类似:

// 产品A的Target配置
{
  "name": "targetA",
  "resource": {
    "directories": [
      "./AppScope/resources_card1",
      "./AppScope/resources_card2",
    ]
  }
}
// 产品B的Target配置
{
  "name": "targetA",
  "resource": {
    "directories": [
      "./AppScope/resources_card2",
      "./AppScope/resources_card3",
    ]
  }
}
// 产品C的Target配置
{
  "name": "targetA",
  "resource": {
    "directories": [
      "./AppScope/resources_card3",
    ]
  }
}

每个服务卡片拥有自己的form_config.json配置,然后target根据配置列表对同名配置文件的内容进行合并。

页面也有类似的问题,如果我的应用有100个页面,分别包装成5个产品,那最终可能就要有接近500条页面配置...

总结

DevEco目前的多目标产物配置的功能能够满足基础的需求,但是对于差异化细节的考量不足,尤其是对于元服务,包体积影响比较大。
当前的多目标产物配置方案比较散碎,而且基于文件的配置化方案和路由、服务卡片等能力耦合较强,总给人束手束脚的感觉。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐