本文持续记录一些使用 ArkTS 进行 HarmonyOS 开发时遇到的一些问题以及解决方案。以及一些可能称不上是问题,但是在我看来有必要记录一下的点。

本文的主要结构将分为 ArkTSHarmonyOS 两部分。同时内容多与 iOS 开发以及 Swift 语言进行对比。

因为代码高亮限制,本文中涉及到 ArkTS 的代码,都将使用 TypeScript 的语法高亮设置

ArkTS

ArkTS 是 TS 的超集,同时也阉割了一些 TS 的用法。

本节所记录的内容是从一个 TS 零基础小白的视角出发所遇到的问题。所以某些内容未必属于 ArkTS 引入,也有可能是 TS 就存在。某些小节可能会指出该小节的内容是属于 ArkTS 独有,还是 TS 就有。但是大多数小节可能并不会做此区分。

async 与 await

ArkTS 的异步协程不像是 Swift 那样,需要在方法声明上显式增加 async 关键字。

只要方法返回 Promise 类型,那么在调用时候就可以通过添加 await 关键字,来获取 Promise 里所包含的数据。

比如下面的方法:

function getData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data fetched!");
        }, 1000);
    });
}

function main() {
    const result = await getData(); // 可以使用 await
    console.log(result); // 输出: "Data fetched!"
}

这其实是 TS 的语法。当你在 TypeScript 中声明一个函数为 async 时,该函数会自动返回一个 Promise 对象,而不是直接返回结果。即使函数内部没有显式地返回 Promiseasync 函数会将其结果封装在一个 Promise 中。比如下面这样:

async function myAsyncFunction() {
    return "Hello, Async!";
}

// 调用方式
myAsyncFunction().then(result => console.log(result)); // 输出: "Hello, Async!"

myAsyncFunction 因为被 async 修饰,所以自动返回了一个 Promise<string> 而不是单纯的 string

所以如果方法没有标记 async,但是返回了 Promise,其实和标记了但是省略返回类型是一样的,自然也可以在调时直接使用 await 同步获取内容了。

HarmonyOS

环境变量配置

这里说的环境变量是 “正式环境”、“预上线环境” 以及 “测试环境”。包含接口环境配置以及一些 debug 入口等等。

在 iOS 开发中我们有预编译宏,但是 HarmonyOS 这块儿没有这个。HarmonyOS 的方法是在各个子模块中建立 target 的 sourceRoots 字段,将不同环境的代码添加到不同的 target 中,然后 entry 再引用不同的 target,以此来达到不同环境加载不同代码的需求。

本节不详细展开具体的详细内容,详细内容见官方文档

在做这一项配置的过程中,有一个概念很容易搞错,或者说官方文档的讲解流程和实际上配置的流程有一定的出入。(当然也有可能是我个人能力不足导致理解错误)。

我们来看下面几个 build-profile.json5 文件。首先是一个 feature module 的:

// feature/build-profile.json5
{
  "apiType": "stageMode",
  "buildOption": {},
  "targets": [ 
    { 
      "name": "default", 
      "source": { 
        "sourceRoots": ["./src/default"] // 配置target为default的差异化代码空间
      } 
    }, 
    { 
      "name": "custom", 
      "source": { 
        "sourceRoots": ["./src/custom"] // 配置target为custom的差异化代码空间
      } 
    } 
  ]
}

其包含两个 target,使用 sourceRoots 引用了不同的代码。

然后再看 entry 的:

{
  "apiType": "stageMode",
  "buildOption": {},
  "targets": [ 
    { 
      "name": "default", 
    }, 
  ]
}

entry 只包含一个 default target。

最后看工程的:

{
  "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",
            "custom",
          ]
        },
      ]
    },
    {
      "name": "feature",
      "srcPath": "./feature",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "custom",
          "applyToProducts": [
            "custom"
          ]
        },
      ]
    },
  ]
}

在阅读官方文档后,我首先的想法就是上面这三个配置文件。定义 defaultcustom 两个 product,然后 entry 作为入口,添加到两个 product 中。feature/default 添加到 defaultfeature/custom 添加到 custom。这样 default product 包含 entryfeature/defaultcustom product 包含 entryfeature/custom。思路看着没问题对吧,然而实际运行起来会发现配置不生效custom product 使用的是 feature/default

问题在于思路不对,正确的思路应该是:将 entry 中定义的 target,下放给各个 module 使用。也就是说要先在 entry 中定义各个 target,然后再在各个 module 中使用 entry 定义的 target,自定向下使用。

比如有5个子 module,各自都有不同的 target 需求,分别是 1-5,那么 entry 中就应该定义这 1-5个 target,然后5个子 module 从中选取自己需要的那一个。

所以上述例子中的配置文件应该改成下面这样,先看 entry 的 build-profile.json5

{
  "apiType": "stageMode",
  "buildOption": {},
  "targets": [ 
    { 
      "name": "default", 
    }, 
    { 
      "name": "custom",  // 增加 custom target
    }, 
  ]
}

然后修改工程级 build-profile.json5

{
  "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",
          ]
        },
      ]
    },
    {
      "name": "feature",
      "srcPath": "./feature",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "custom",
          "applyToProducts": [
            "custom"
          ]
        },
      ]
    },
  ]
}

这么修改后再运行,custom product 就可以正确地读取到 feature/custom 了。

最后再来说一说官方中的例子。官方上的示例使用 entry 演示 sourceRoots 的用法,其实暗藏了 “entry 也需要定义对应的 target” 这一点,只不过没有明说,让读者忽略了这一关键内容。