鸿蒙OS Next基础问题记录

327 阅读11分钟

官网信息链接

社区

所有版本及对应关系

申请调试证书,添加调试设备UDID

如何获取签名信息中的指纹信息

bm工具错误码

Entry模块的HAP和Feature模块的HAP在使用和功能上的区别是什么

  • Entry类型的HAP:是应用的主模块,在module.json5配置文件中的type标签配置为“entry”类型。在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。
  • Feature类型的HAP:是应用的动态特性模块,在module.json5配置文件中的type标签配置为“feature”类型。一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可以配置成按需下载安装,也可以配置成随Entry类型的HAP一起下载安装。

如何理解App、HAP、HAR的关系

  • App是发布到应用市场的基本单元,不能直接在设备上安装和运行。
  • HAP(Harmony Ability Package)是应用安装和运行的基本单元,包含代码、资源、第三方库及配置文件等,主要分为entry和feature两种类型。
  • HAR(Harmony Archive)是静态共享包,包含代码、C++库、资源和配置文件。HAR支持多个模块或工程共享ArkUI组件和相关代码。
  • HSP(Harmony Shared Package)是动态共享包,包含代码、C++库、资源和配置文件,用于实现代码和资源的共享。HSP跟随宿主应用的App包一起发布,与宿主应用同进程,具有相同的包名和生命周期。

应用程序包基础知识

团队内部测试

环境搭建

Windows

  1. 下载最新IDE
  2. hdc环境变量配置路径:下载根目录\devecostudio-windows-5.0.11.100\DevEco Studio\sdk\default\openharmony\toolchains

MAC

配置环境变量

export PATH=\$PATH:/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains

export CLASSPATH HDC\_SERVER\_PORT=7035

launchctl setenv HDC\_SERVER\_PORT \$HDC\_SERVER\_PORT

export HDC\_SERVER\_PORT

\~

构建应用 >旧-编译构建

build-profile.json5

编译时可以选择debug或release

image.png

如何判断当前应用程序是Debug包还是Release包

API选择(Ctrl+Shift+ALt+S)

image.png

签名配置

可自动;也可以配置已申请的证书等 image.png

firgerprint

fingerprint对应应用包的指纹信息,取到的是应用签名证书(.cer文件)的SHA256(加密方式) hash值。官方回复

获取firgerprint的方式

  1. 通过鸿蒙API,日志打印;
  2. 通过keytool -printcert -file <xxxxx.cer> 获取对应所有者的证书指纹;
    我这里显示多个证书,找到所有者是自己公司名称的证书,对应的指纹跟api获取一致。 image.png

打包相关

  • 多包(HAP/HSP)引用相同的HAR时,会造成多包间代码和资源的重复拷贝,从而导致应用包膨大。
  • HAR可以依赖其他HAR,但不支持循环依赖,也不支持依赖传递。
    说明
    循环依赖:例如有三个HSP,HSP-A、HSP-B和HSP-C,循环依赖指HSP-A依赖HSP-B,HSP-B依赖HSP-C,HSP-C又依赖HSP-A。
    依赖传递:例如有三个HSP,HSP-A、HSP-B和HSP-C,依赖关系是HSP-A依赖HSP-B,HSP-B依赖HSP-C。不支持传递依赖指HSP-A可以使用HSP-B的方法和组件,但是HSP-A不能直接使用HSP-C的方法和组件。

编译har

打签名的har包

打har包时,会发现有时候打出的包,文件名不带-signed后缀。
此时,需要在hvigor-config.json5开启

    "properties": {
      // 构建签名HAR开关
      "ohos.sign.har": true
    }

编写插件 Hvigorfile.ts

实现打Har/Hap包,自动将包的文件名改成带版本号的命名

// 工程级hvigorfile.ts文件
import { hvigor, HvigorNode, HvigorPlugin } from '@ohos/hvigor';
import { appTasks, OhosHarContext, OhosPluginId, Target } from '@ohos/hvigor-ohos-plugin';
import * as fs from 'fs';
import * as path from 'path';

// 实现自定义插件
export function renameHarPlugin(): HvigorPlugin {
  return {
    pluginId: 'renameHarPlugin',
    context() {
      return {
        data: 'renameHarPlugin'
      };
    },
    async apply(currentNode: HvigorNode): Promise<void> {
      hvigor.nodesEvaluated(async () => {
        // 注册模块级任务
        harTask(currentNode);
      });
    }
  };
}

function harTask(currentNode: HvigorNode) {
  // 等待全部节点加载完成之后获取子节点信息
  currentNode.subNodes((node: HvigorNode) => {
    // 获取har模块上下文信息
    const harContext = node.getContext(OhosPluginId.OHOS_HAR_PLUGIN) as OhosHarContext;
    const moduleName = harContext?.getModuleName();
    console.log(`har模块 moduleName: ${moduleName}`);
    harContext?.targets((target: Target) => {
      const targetName = target.getTargetName();
      const outputPath = target.getBuildTargetOutputPath();
      console.log(`har模块 targetName:${targetName} outputPath: ${outputPath}`);
      node.registerTask({
        // 任务名称
        name: `${targetName}@renameHar`,
        // 任务执行逻辑主体函数
        run() {
          const version = harContext?.getVersion();
          console.log(`har模块 version:${version}`);
          const src = path.join(outputPath, `${moduleName}-signed.har`);
          const dst = path.join(outputPath, `${moduleName}-signed-${version}.har`);
          if (fs.existsSync(src)) {
            fs.renameSync(src, dst);
            console.log(`[har rename] ${src}${dst}`);
          } else {
            console.warn(`[har rename] missing: ${src}`);
          }
        },
        // 配置前置任务依赖
        // dependencies: [`${targetName}@PackageHar`],
        dependencies: [`${targetName}@PackageSignHar`],
        // 配置任务的后置任务依赖
        postDependencies: ['assembleHar']
      });
    });
  });
}

// 实现自定义插件
export function renameHapPlugin(): HvigorPlugin {
  return {
    pluginId: 'renameHapPlugin',
    context() {
      return {
        data: 'renameHapPlugin'
      };
    },
    async apply(currentNode: HvigorNode): Promise<void> {
      hvigor.nodesEvaluated(async () => {
        // 注册模块级任务
        hapTask(currentNode);
      });
    }
  };
}

function hapTask(currentNode: HvigorNode) {
  // 等待全部节点加载完成之后获取子节点信息
  currentNode.subNodes((node: HvigorNode) => {
    // 获取hap模块上下文信息
    const hapContext = node.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosHapContext;
    const moduleName = hapContext?.getModuleName();
    console.log(`hap模块 moduleName: ${moduleName}`);
    hapContext?.targets((target: Target) => {
      const targetName = target.getTargetName();
      const outputPath = target.getBuildTargetOutputPath();
      console.log(`hap模块 targetName:${targetName} outputPath: ${outputPath}`);
      if(targetName == 'default'){
        return
      }
      node.registerTask({
        // 任务名称
        name: `${moduleName}@renameHap`,
        // 任务执行逻辑主体函数
        run() {
          const version = hapContext?.getVersion();
          console.log(`hap模块 version:${version}`);
          const src = path.join(outputPath, `${moduleName}-${targetName}-signed.hap`);
          const dst = path.join(outputPath, `${moduleName}-${targetName}-signed-${version}.hap`);
          if (fs.existsSync(src)) {
            fs.renameSync(src, dst);
            console.log(`[hap rename] ${src}${dst}`);
          } else {
            console.warn(`[hap rename] missing: ${src}`);
          }
        },
        // 配置前置任务依赖
        dependencies: [`${targetName}@SignHap`],
        // 配置任务的后置任务依赖
        postDependencies: ['assembleHap']
      });
    });
  });
}

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

配置Product

image.png

image.png

Entry下的 build-profile.json5

{
  ...
  "buildModeBinder": [
    {
      "buildModeName": "release",
      "mappings": [
        {
          "buildOptionName": "release",
          "targetName": "release"
        }
      ]
    },
    {
      "buildModeName": "debug",
      "mappings": [
        {
          "buildOptionName": "debug",
          "targetName": "default"
        }
      ]
    }
  ],
  "targets": [
    {
      "name": "default"
    },
    {
      "name": "release",
    }
  ]
}

项目级的build-profile.json5

{
  ...
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        // 标识编译应用/元服务所使用的SDK版本
        "compileSdkVersion": "5.0.5(17)",
        // 标识应用/元服务运行所需目标SDK版本
        "targetSdkVersion": "5.0.5(17)",
        // 兼容的最低SDK版本
        "compatibleSdkVersion": "5.0.1(13)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      },
      {
        "name": "release",
        "signingConfig": "default",
        // 标识编译应用/元服务所使用的SDK版本
        "compileSdkVersion": "5.0.5(17)",
        // 标识应用/元服务运行所需目标SDK版本
        "targetSdkVersion": "5.0.5(17)",
        // 兼容的最低SDK版本
        "compatibleSdkVersion": "5.0.1(13)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ],
    "buildModeSet": [
      {
        "name": "debug"
      },
      {
        "name": "release"
      }
    ]
  },
  "modules": [
    {
      "name": "xxxEntry",
      "srcPath": "./products/xxxEntry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "release",
          "applyToProducts": [
            "release"
          ]
        }
      ]
    }
  ]
}

多目标产物构建

类似于Android的渠道
配置多目标产物

HAR改HSP

hsp资源引用

hap hsp安装

遇到的问题: 当sdk从har改为集成态hsp后,无法直接安装hap。

需要分别安装hsp和hap。
集成态tgz因没有签名,无法直接安装的。

编译项目后,需要获取签名后的hsp,安装hsp后,才能安装hap。 image.png

hdc install xxx.hsp
hdc install xxx.hap

PS

日志

hdc file recv /data/log/hilog

基础语法

关键点记录

引用类型在内存中通过指针访问数据,修改引用会影响原始数据。 -- 一点与java一样

一些基本转换

string转number

  • 使用parseInt/parseFloat,会截断非数字部分
let str: string = "123";
let num: number = parseInt(str);
console.log(num); // 输出: 123

// 处理非数字字符(会截断非数字部分)
let str2: string = "456abc";
let num2: number = parseInt(str2);
console.log(num2); // 输出: 456

// 处理非数字字符(会截断非数字部分)
let str2: string = "456abc789";
let num2: number = parseInt(str2);
console.log(num2); // 输出: 456

// 指定进制(如16进制)
let hexStr: string = "1a";
let hexNum: number = parseInt(hexStr, 16);
console.log(hexNum); // 输出: 26

// 转float
parseFloat(...)

  • 使用Number
let validStr: string = "123";
let num4: number = Number(validStr);
console.log(num4); // 输出: 123

let invalidStr: string = "123abc";
let num5: number = Number(invalidStr);
console.log(num5); // 输出: NaN
  • 使用+ 运算符
let str6: string = "99";
let num6: number = +str6;
console.log(num6); // 输出: 99

let str7: string = "99.99";
let num7: number = +str7;
console.log(num7); // 输出: 99.99

number转16进制字符串

// 基本转换方法
let decimalNumber: number = 255;

// 转换为十六进制字符串(小写)
let hexString: string = decimalNumber.toString(16);
console.log(hexString); // 输出 "ff"

Uint8Array转字符串

let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);

// 直接转换
let str = String.fromCharCode.apply(null, uint8Array);
// 或使用展开运算符
let str2 = String.fromCharCode(...uint8Array);

console.log(str); // 输出 "Hello"

Uint8Array转十六进制字符串

function uint8ToHex(uint8: Uint8Array): string {
  return Array.from(uint8)
    .map(byte => byte.toString(16).padStart(2, '0'))
    .join('');
}

let hex = uint8ToHex(new Uint8Array([255, 13, 171]));
console.log(hex); // 输出 "ff0dab"

字符串编码为UTF-8格式的Uint8Array

  1. 方式一:
let str: string = "Hello, HarmonyOS!";
let encoder = new util.TextEncoder();
let byteArray: Uint8Array = encoder.encode(str);
console.log(byteArray); // 输出UTF-8字节数组
  1. 方式一:
/**
 * 手动将字符串编码为UTF-8格式的Uint8Array
 * @param str 输入字符串
 * @returns UTF-8字节数组
 */
public stringToUtf8Bytes(str: string): Uint8Array {
  const bytes: number[] = [];
  for (let i = 0; i < str.length; i++) {
    const code = str.charCodeAt(i);
    // 处理单字节(ASCII)
    if (code < 0x80) {
      bytes.push(code);
    }
    // 处理双字节(常见中文)
    else if (code < 0x800) {
      bytes.push(0xC0 | (code >> 6));
      bytes.push(0x80 | (code & 0x3F));
    }
    // 处理三字节(部分生僻汉字)
    else if (code < 0x10000) {
      bytes.push(0xE0 | (code >> 12));
      bytes.push(0x80 | ((code >> 6) & 0x3F));
      bytes.push(0x80 | (code & 0x3F));
    }
    // 处理四字节(Emoji等)
    else {
      bytes.push(0xF0 | (code >> 18));
      bytes.push(0x80 | ((code >> 12) & 0x3F));
      bytes.push(0x80 | ((code >> 6) & 0x3F));
      bytes.push(0x80 | (code & 0x3F));
    }
  }
  return new Uint8Array(bytes);
}

ArrayBuffer与Uint8Array互转

  /**
   * arrayBuffer转Uint8Array
   * @param buffer
   * @returns
   */
  public arrayBufferToUint8Array(buffer: ArrayBuffer): Uint8Array {
    let temp = new Uint8Array(buffer);
    return temp;
  }

  /**
   * uint8Array转ArrayBuffer
   * @param buffer
   * @returns
   */
  public uint8ArrayToArrayBuffer(array: Uint8Array): ArrayBuffer {
    let temp = array.buffer as ArrayBuffer;
    return temp;
  }

固定数组长度

// 元组
[number, number]

元组

部分操作数组的方法

// 定义示例数组
let numbers: number[] = [1, 2, 3, 4, 5];
let users: { id: number; name: string }[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

// 使用 find 查找元素
let target = numbers.find((num) => num > 3); // 返回 4
let user = users.find((u) => u.id === 2);    // 返回 { id:2, name:"Bob" }

// 使用 map 转换数组
let doubled = numbers.map((num) => num * 2); // 返回 [2,4,6,8,10]
let names = users.map((u) => u.name);        // 返回 ["Alice", "Bob"]

// 使用 filter 过滤数组
let evens = numbers.filter((num) => num % 2 === 0); // 返回 [2,4]

枚举相关

遍历

export enum Test {
  Test_1 = 1,
  Test_2 = 2,
}

// 输出value的数组
let valueArr = Object.values(Test)
  .filter(value => typeof value === 'number') as Test[]
// 以枚举的key 作为dialog的显示文本
getEnumNames<T extends object>(e: T): TextCascadePickerRangeContent[] {
  return Object.keys(e).filter(key => isNaN(Number(key))).map(name => ({
    text: name
  } as TextCascadePickerRangeContent));
}

...
this.testArr = this.getEnumNames(Test);
this.getUIContext().showTextPickerDialog({
  range: this.testArr,
...
  • 用string匹配枚举的key,得到对应的value
enum Direction {
  Up = 1,
  Down = 2,
  Left = 3,
  Right = 4
}
let text = "Up"
let param: Direction = Direction[text as keyof Direction]
console.log(param);               // 输出 1
// 根据数值获取枚举名称的两种方式
const name1: string = Direction[1];      // "Up"
const name2: string = Direction[2];      // "Down"

// 使用示例
console.log(Direction[3]);               // 输出 "Left"
console.log(Direction[Direction.Up]);    // 输出 "Up"(先获取数值1,再反向映射)

仅枚举类型传参限制

在 ArkTS 中,enum(枚举)是一种特殊的值类型,用于定义一组具名常量。默认情况下,数字枚举的成员类型为 number,这意味着在函数参数声明为枚举类型时,调用方依旧可以传入任意数字而不仅限于枚举成员。要“严格”限制函数只接受枚举定义中出现的成员,而非任意数字,需要借助以下几种常见做法:

  1. 使用字符串枚举(String Enum) ,因字符串枚举只能接受定义中的常量而拒绝其他字符串。
  2. 将枚举成员组合成联合类型(Union Type) ,让函数参数类型变为各枚举成员字面量的联合,从而在编译时拒绝其他值。
  3. 结合类型别名(type)与索引类型操作符(keyof typeof ,在必要时进一步约束只允许通过枚举键名或值来调用。

2. 问题:数字枚举无法严格限制输入

如上所述,由于数字枚举成员的底层类型是 number,因此当我们定义:

enum Status {
  Idle = 0,
  Running = 1,
  Paused = 2
}

function setStatus(s: Status) {
  console.log("当前状态编号:", s);
}

// 这些调用都不会在编译时报错:
setStatus(Status.Idle);   // 合法
setStatus(1);             // 直接传数字 “1” 也被视作 `Status`,不报错
setStatus(42);            // 任意数字同样不会提示错误

原因:ArkTS 中的“数字枚举”其实等价于一个 number 类型(比如 Idle0Running1),因此编译阶段不会拒绝任何数字常量传入。([cnblogs.com]

2.1 为什么这在实际场景中不友好?

  • 当函数只期望输入枚举声明过的几种状态或选项时,调用方如果传入一个未定义的数字(如 42),可能会导致程序逻辑错误。
  • 不能在编译期保证传入的参数合法,增加了运行期检查的成本和风险。

3. 解决方案一:使用“字符串枚举”(String Enum)

最简洁的方式是 将枚举从“数字枚举”换成“字符串枚举” ,此时在函数参数声明为该枚举类型时,编译器会拒绝任何不在枚举成员列表中的字符串。

3.1 示例代码

/**
 * 将数字枚举替换为字符串枚举
 */
enum ApiEnvironment {
  Development = "development",
  Staging     = "staging",
  Production  = "production"
}

/**
 * 函数只接受 ApiEnvironment 中定义的三个字符串,
 * 其他字符串都会导致编译失败
 */
function configureEnv(env: ApiEnvironment) {
  console.log(`当前环境:${env}`);
}

// 合法调用:
configureEnv(ApiEnvironment.Development);
configureEnv(ApiEnvironment.Production);

// 下面调用均在编译时报错,提示:
// “不能将类型 'string' 分配给类型 'ApiEnvironment'”
// configureEnv("testing");       
// configureEnv("production");     // 即使字符串相似,也会报错
// configureEnv("Development");     // 大小写不匹配也报错
  • 在上例中,ApiEnvironment 的底层类型是 string,但它又仅限于三个字面量 "development""staging""production"
  • 编译器在解析 configureEnv("testing") 时,会发现 "testing" 既不是 ApiEnvironment.Development,也不是其他两个合法成员,于是直接编译失败。
  • 优点:简单直观,不需要额外类型别名;编译时即可保证调用方只能传预定义的字符串。

3.2 何时不适用?

  • 如果项目中已经使用“数字枚举”较多、或需要与第三方库保持兼容,全面改为字符串枚举的成本较高。
  • 如果枚举含义需要跟某些数字 ID 保持一致,则字符串枚举不可行。

4. 解决方案二:将枚举成员组合成“联合字面量类型”(Literal Union)

若一定要使用数字枚举,又想在函数参数处严格限制,仅允许枚举定义中的数字常量,可借助 联合类型(Union)将枚举成员列成字面量联合,再将函数参数类型标为该联合类型。

4.1 原理说明

假设我们有:

enum HttpCode {
  OK       = 200,
  NotFound = 404,
  Error    = 500
}

数字枚举 HttpCode 本身等同于 number,但我们只想允许 200404500。可以先提取出枚举成员的字面量:

// 1. 先列出字面量联合
type HttpCodeValue = 200 | 404 | 500;

// 2. 函数只接受上述联合
function handleResponse(code: HttpCodeValue) {
  console.log(`返回码:${code}`);
}

// 合法:
handleResponse(200);
handleResponse(404);

// 编译错误:
// “不能将类型 '500 | 200 | 404 | 400' 分配给类型 '200 | 404 | 500'”
// handleResponse(400);
// handleResponse(HttpCode.OK + 1); 

这样 handleResponse 参数类型就不再是宽泛的 number,而是被限制成 200 | 404 | 500,严格对应枚举成员。

4.2 结合“类型别名”与“索引类型运算符”自动提取(高级用法)

如果枚举成员较多,手动列出所有字面量会很麻烦。可以在 ArkTS 中(同 TS 的做法)结合如下写法自动提取:

enum HttpCode {
  OK       = 200,
  NotFound = 404,
  Error    = 500
}

/**
 * 1. keyof typeof HttpCode 会得到所有 “键名” 字面量: "OK" | "NotFound" | "Error"
 * 2. typeof HttpCode[Key] 就是对应枚举成员的值,即 200 | 404 | 500
 */
type HttpCodeValue = typeof HttpCode[keyof typeof HttpCode];

/**
 * 此时 HttpCodeValue 等价于:200 | 404 | 500
 */
function handleResponse(code: HttpCodeValue) {
  console.log(`返回码:${code}`);
}

// 合法:
handleResponse(HttpCode.OK);       // 200
handleResponse(HttpCode.NotFound); // 404

// 编译错误:
// “不能将类型 '400' 分配给类型 '200 | 404 | 500'”
// handleResponse(400);
  • typeof HttpCode 得到枚举对象类型,包含键 (key) 与值 (value) 的映射,例如 { OK: 200; NotFound: 404; Error: 500; 200: "OK"; 404: "NotFound"; ... }

  • keyof typeof HttpCode 只取出“键名”,即 "OK" | "NotFound" | "Error" | 200 | 404 | 500。为了只拿数字值,需要再进一步筛选:

    // 只保留枚举定义的“键名”对应的值
    type KeyNames = keyof typeof HttpCode;                  // "OK" | "NotFound" | "Error" | 200 | 404 | 500
    type ValueOfKeyNames = typeof HttpCode[KeyNames & string]; 
    // 这里用 "& string"去除数值索引,最终得到:200 | 404 | 500
    
  • 上述写法在 ArkTS 中与 TS 几乎相同,编译器会在类型检查阶段按照联合类型推断具体字面量范围。(docs.asprain.cn, docs.asprain.cn)

4.3 完整示例归纳

// 定义数字枚举
enum UserRole {
  Guest    = 0,
  Member   = 1,
  Admin    = 2,
  SuperAdmin = 3
}

// 自动提取 “只含数值成员” 的字面量联合
type UserRoleValue = typeof UserRole[keyof typeof UserRole] & number;
// 因为 typeof UserRole[keyof typeof UserRole] 会包含 “键名索引” 的成员类型
// 所以需要 “& number” 限定只保留数值部分。
// 最终 UserRoleValue 等同于:0 | 1 | 2 | 3

// 函数仅接受 0|1|2|3,编译阶段拒绝其他数字
function assignRole(role: UserRoleValue) {
  console.log("正在分配角色编号:", role);
}

// 合法:
// assignRole(UserRole.Admin);    // 自动推断为 2
// assignRole(3);                 // 3 属于联合类型

// 编译错误:
// “不能将类型 '4' 分配给类型 '0 | 1 | 2 | 3'”
// assignRole(4);

// “不能将类型 'string' 分配给类型 '0 | 1 | 2 | 3'”
// assignRole("Admin");

注意:如果枚举很大,自动提取联合类型的写法虽然麻烦一点,但一旦写对,就能为函数“参数只允许枚举值”提供动态且可维护的解决方案,无需手动列出每个字面量。(docs.asprain.cn, docs.asprain.cn)


5. 解决方案三:在函数签名中直接使用“键名联合”或“值联合”

在一些极端场景下,也可以不借助 typeof ...[keyof typeof ...] 提取,而是直接写成“联合字面量”或“键名联合 + 索引”,如下:

5.1 “键名联合”+索引(仅允许通过枚举键名调用)

enum Status {
  Ready   = 0,
  Waiting = 1,
  Done    = 2
}

/**
 * 参数只能是 "Ready"|"Waiting"|"Done" 三个字符串之一,
 * 然后再通过索引拿到对应的数字值。此时无法传入任意字符串或数字。
 */
function setStatusByName(name: keyof typeof Status) {
  const code: Status = Status[name];
  console.log(`已将状态设为 [${name} : ${code}]`);
}

// 合法:
setStatusByName("Ready");   
setStatusByName("Done");    

// 编译错误:
// “不能将类型 'string' 分配给类型 '"Ready" | "Waiting" | "Done"'”
// setStatusByName("Unknown"); 

// 由于传入必须是键名,无法直接传入数字
  • keyof typeof Status 只会展开成 "Ready" | "Waiting" | "Done" | 0 | 1 | 2

  • 如果你只想保留键名(字符串),则需要加上 & string

    type StatusKeyName = keyof typeof Status & string; // "Ready" | "Waiting" | "Done"
    

    这样 setStatusByName 只接受键名层面的字符串,进一步保证不能直接传数字。(docs.asprain.cn, docs.asprain.cn)

5.2 “值联合”直接写字面量(最简易,但不动态)

如果枚举成员较少,也可以直接在函数签名中写字面量联合:

enum Direction {
  Up    = 1,
  Down  = 2,
  Left  = 3,
  Right = 4
}

/**
 * 直接列出所有可能的字面量值进行联合
 */
function move(dir: 1 | 2 | 3 | 4) {
  console.log("移动方向编号:", dir);
}

// 合法:
move(1);

// 编译错误:
// “不能将类型 '5' 分配给类型 '1 | 2 | 3 | 4'”
// move(5);
  • 这种写法最为直白,但维护成本较高:每次枚举新增或删除,都得同步更新函数签名。(docs.asprain.cn, cnblogs.com)

如何查找一个元素是否存在于数组中

1. 直接使用 includes() 方法(适用于基本类型)

  • 语法array.includes(targetElement)

  • 特点:直接返回布尔值,简单高效,但仅适用于基本类型(如 numberstringboolean ,无法直接判断对象引用。

  • 示例: let numbers: number[] = [1, 2, 3, 4, 5]; let hasThree = numbers.includes(3); // 返回 true let hasSix = numbers.includes(6); // 返回 false


2. 使用 indexOf() 方法(兼容性更广)

  • 语法array.indexOf(targetElement) !== -1

  • 特点:返回元素索引,若不存在则返回 -1。兼容性较好,但同样不适用于对象引用比较

  • 示例

    let fruits: string[] = ["apple", "banana", "orange"];
    let hasBanana = fruits.indexOf("banana") !== -1; // 返回 true
    

3. 使用 some() 方法(灵活通用,支持对象和条件判断)

  • 语法array.some((item) => { /* 条件 */ })

  • 特点:可自定义判断条件,适用于所有类型(包括对象) ,是判断复杂元素的推荐方法。

  • 示例

    // 基本类型
    let numbers = [1, 2, 3];
    let hasEven = numbers.some(num => num % 2 === 0); // 检查是否存在偶数 → true
    
    // 对象类型(需匹配属性)
    interface User {
      id: number;
      name: string;
    }
    let users: User[] = [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
    ];
    let hasUserBob = users.some(user => user.name === "Bob"); // 返回 true
    

4. 使用 find() 方法(需判断返回值)

  • 语法array.find((item) => { /* 条件 */ }) !== undefined

  • 特点:返回匹配的元素,若未找到则返回 undefined。适用于需要同时获取元素的场景。

  • 示例

    let users: User[] = [ /* ... */ ];
    let isBobExist = users.find(user => user.name === "Bob") !== undefined; // true
    

总结:如何选择?

方法适用场景对象支持直接返回值
includes()基本类型的精确匹配布尔值
indexOf()基本类型且需兼容性索引值
some()对象或复杂条件判断(推荐)✔️布尔值
find()需同时获取匹配元素✔️元素或undefined

注意事项

  • 对象比较includes() 和 indexOf() 通过引用地址判断是否相等,若需检查对象属性是否匹配,必须用 some() 或 find()
  • 性能:对于大型数组,some() 和 find() 在找到第一个匹配项后立即停止遍历,效率较高。

for循环中串行Promise函数

方案一:使用 async/await + for 循环(推荐)

通过 async 函数配合 await 关键字,实现直观的串行控制流:

// 示例方法A:模拟异步操作
function methodA(param: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`执行完成: ${param}`);
      resolve();
    }, 1000);
  });
}

// 串行执行函数
async function runSerial() {
  const items = [1, 2, 3, 4]; // 待处理数据

  for (const item of items) {
    // 等待当前Promise完成后再进入下一轮循环
    await methodA(item);
  }

  console.log("所有任务完成");
}

// 执行
runSerial();

输出结果(每秒输出一行):

执行完成: 1
执行完成: 2
执行完成: 3
执行完成: 4
所有任务完成

方案二:使用 Array.reduce 链式调用

通过 reduce 构建 Promise 链,实现无 async/await 的串行控制:

function runSerialReduce() {
  const items = [1, 2, 3, 4];

  items.reduce((prevPromise, currentItem) => {
    return prevPromise.then(() => {
      return methodA(currentItem);
    });
  }, Promise.resolve()) // 初始Promise
  .then(() => {
    console.log("所有任务完成");
  });
}

// 执行
runSerialReduce();

关键差异说明

特性async/await + for 循环reduce + Promise 链
可读性高(符合同步代码习惯)中(需要理解 Promise 链式逻辑)
错误处理可直接用 try/catch 捕获需在 .catch() 中处理
动态中断能力支持(通过 breakreturn不支持(需额外状态控制)
适用场景大多数顺序执行场景函数式编程偏好场景

扩展:带结果收集的串行执行

若需要保留每个 Promise 的结果:

async function runSerialWithResults() {
  const items = [1, 2, 3, 4];
  const results: number[] = [];

  for (const item of items) {
    const result = await methodA(item); // 假设methodA返回实际结果
    results.push(result);
  }

  console.log("收集结果:", results);
}

持续更新......