前言

欢迎来到 Flutter三方库适配OpenHarmony 系列文章!本系列围绕 flutter_libphonenumber 这个 电话号码处理库 的鸿蒙平台适配,进行全面深入的技术分享。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里插入图片描述
在这里插入图片描述

上一篇我们分析了 57 个国家格式化规则的数据结构设计。从本篇开始,我们进入 核心 API 实现 部分。本篇将追踪 format() 方法的完整调用链路——从应用层的一次函数调用开始,经过 Dart 侧的 MethodChannel 编码、Flutter Engine 的二进制传输、ArkTS 侧的消息接收与分发、AsYouTypeFormatter 的逐字符格式化,最终将结果回传到 Dart 侧。

format() 是一个典型的 跨平台异步调用。理解它的完整链路,就理解了 Flutter 鸿蒙插件中 Dart ↔ ArkTS 通信的核心模式。


一、format() 的使用方式

1.1 应用层调用

// 在 example 工程中的使用
final res = await _plugin.format(
  '+8613123456789',  // 电话号码
  'CN',              // 区域代码
);
print(res['formatted']);  // '+86 131 2345 6789'

1.2 方法签名

// FlutterLibphonenumberPlatform 中的定义
Future<Map<String, String>> format(
  final String phone,
  final String region,
) async;
参数 类型 说明 示例
phone String 待格式化的电话号码 '+8613123456789'
region String 区域代码(ISO 3166-1) 'CN'
返回值 Map 包含 formatted 字段 {'formatted': '+86 131 2345 6789'}

二、完整调用链路(6 步)

2.1 链路总览

步骤 ①  App 层
  plugin.format('+8613123456789', 'CN')
      │
步骤 ②  Dart: FlutterLibphonenumberOhos
  _channel.invokeMapMethod('format', {'phone':..., 'region':...})
      │
步骤 ③  Flutter Engine
  StandardMessageCodec 编码 → BinaryMessenger 传输
      │
步骤 ④  ArkTS: FlutterLibphonenumberPlugin
  onMethodCall → handleFormat(call, result)
      │
步骤 ⑤  ArkTS: PhoneNumberUtil
  AsYouTypeFormatter 逐字符格式化
      │
步骤 ⑥  ArkTS → Dart
  result.success({formatted: '...'}) → Future 完成

2.2 步骤 ① — 应用层调用

// example/lib/main.dart
onPressed: () async {
  final res = await _plugin.format(
    manualFormatController.text,
    _currentSelectedCountry.countryCode,
  );
  setState(() =>
    manualFormatController.text = res['formatted'] ?? '');
}

应用层通过 _plugin(即 FlutterLibphonenumberPlatform.instance)调用 format()。由于是 async 方法,使用 await 等待结果。

2.3 步骤 ② — Dart 侧 MethodChannel 调用

// FlutterLibphonenumberOhos.format()

Future<Map<String, String>> format(
  final String phone,
  final String region,
) async {
  return await _channel.invokeMapMethod<String, String>(
    'format',
    {'phone': phone, 'region': region},
  ) ?? <String, String>{};
}

关键点:

要素 说明
通道 _channel MethodChannel('com.bottlepay/flutter_libphonenumber_ohos')
方法名 'format' ArkTS 侧 onMethodCall 的匹配依据
参数 {'phone': ..., 'region': ...} Map 类型,自动序列化
返回类型 Map<String, String>? 可空,用 ?? <String, String>{} 兜底

2.4 步骤 ③ — Flutter Engine 传输

Dart Map {'phone': '+8613123456789', 'region': 'CN'}
    │
    ↓ StandardMessageCodec.encodeMessage()
    │ 将 Map 编码为二进制字节流
    │
    ↓ BinaryMessenger.send()
    │ 通过 Flutter Engine 内部通道传输
    │
    ↓ ArkTS 侧 BinaryMessenger 接收
    │
    ↓ StandardMessageCodec.decodeMessage()
    │ 将字节流解码为 MethodCall 对象
    │
    ↓ MethodCall { method: 'format', arguments: {...} }

StandardMessageCodec 支持的类型映射:

Dart 类型 ArkTS 类型
null null
bool boolean
int number
double number
String string
Map ESObject (需 call.argument() 提取)

2.5 步骤 ④ — ArkTS 侧消息分发

// FlutterLibphonenumberPlugin.ets
onMethodCall(call: MethodCall, result: MethodResult): void {
  if (call.method === 'format') {
    this.handleFormat(call, result);  // ← 路由到这里
  }
  // ...
}

2.6 步骤 ⑤ — handleFormat 处理

private handleFormat(
  call: MethodCall, result: MethodResult
): void {
  let phone = call.argument('phone') as string;
  let region = call.argument('region') as string;

  if (phone === null || phone.length === 0) {
    result.error('InvalidParameters',
      "Invalid 'phone' parameter.", null);
    return;
  }

  try {
    let useRegion = region !== null ? region : 'CN';
    let formatter =
      this.phoneUtil.getAsYouTypeFormatter(useRegion);
    let formatted = '';
    formatter.clear();

    for (let i = 0; i < phone.length; i++) {
      formatted = formatter.inputDigit(phone.charAt(i));
    }

    let response: Map<string, string> = new Map();
    response.set('formatted', formatted);
    result.success(this.convertMapToRecord(response));
  } catch (e) {
    result.error('FORMAT_ERROR',
      'Failed to format phone number', null);
  }
}

2.7 步骤 ⑥ — 结果回传

ArkTS: result.success({formatted: '+86 131 2345 6789'})
    │
    ↓ Record<string,string> → StandardMessageCodec 编码
    │
    ↓ BinaryMessenger 回传
    │
    ↓ Dart: invokeMapMethod() 的 Future 完成
    │
    ↓ Map<String, String> {'formatted': '+86 131 2345 6789'}
    │
    ↓ App: res['formatted'] → '+86 131 2345 6789'

三、Dart 侧的调用封装

3.1 FlutterLibphonenumberOhos 的 format() 实现

// flutter_libphonenumber_ohos.dart
const _channel = MethodChannel(
  'com.bottlepay/flutter_libphonenumber_ohos');

class FlutterLibphonenumberOhos
    extends FlutterLibphonenumberPlatform {

  
  Future<Map<String, String>> format(
    final String phone,
    final String region,
  ) async {
    return await _channel.invokeMapMethod<String, String>(
      'format',
      {'phone': phone, 'region': region},
    ) ?? <String, String>{};
  }
}

3.2 invokeMapMethod 的内部机制

invokeMapMethod<K, V>()MethodChannel 提供的类型安全调用方法。它的内部执行流程:

invokeMapMethod<String, String>('format', args)
    │
    ├── ① 构建 MethodCall 对象
    │   MethodCall('format', {'phone': ..., 'region': ...})
    │
    ├── ② StandardMethodCodec 编码
    │   方法名 → UTF-8 字节
    │   参数 Map → 键值对序列化
    │
    ├── ③ BinaryMessenger.send()
    │   通过通道名路由到 ArkTS 侧
    │
    ├── ④ 等待响应(异步)
    │   Dart 侧挂起当前 Future
    │
    ├── ⑤ 接收响应字节流
    │   StandardMethodCodec 解码
    │
    └── ⑥ 类型转换
        dynamic → Map<String, String>?

3.3 与 invokeMethod 的区别

Flutter MethodChannel 提供了三种调用方法:

方法 返回类型 适用场景
invokeMethod<T> Future<T?> 返回单一值(String, int 等)
invokeListMethod<T> Future<List<T>?> 返回列表
invokeMapMethod<K,V> Future<Map<K,V>?> 返回键值对

format() 返回的是 Map<String, String>,因此使用 invokeMapMethod。如果使用 invokeMethod<Map>,需要手动进行类型转换:

// 使用 invokeMapMethod(推荐)
final result = await _channel
    .invokeMapMethod<String, String>('format', args);
// result 类型: Map<String, String>?

// 使用 invokeMethod(需手动转换)
final raw = await _channel
    .invokeMethod<Map>('format', args);
final result = raw?.cast<String, String>();
// 多了一步 cast 操作

3.4 null 安全处理

return await _channel.invokeMapMethod<String, String>(
  'format', {'phone': phone, 'region': region},
) ?? <String, String>{};

?? <String, String>{} 处理了两种情况:

情况 invokeMapMethod 返回值 最终返回值
ArkTS 正常返回 {'formatted': '...'} {'formatted': '...'}
ArkTS 返回 null null {} (空 Map)
ArkTS 返回 error 抛出 PlatformException 不走 ?? 逻辑

注意:当 ArkTS 侧调用 result.error() 时,Dart 侧会抛出 PlatformException,不会走到 ?? 逻辑。?? 只处理 result.success(null) 的情况。


四、ArkTS 侧的消息接收与参数提取

4.1 onMethodCall 分发

onMethodCall(call: MethodCall, result: MethodResult): void {
  if (call.method === 'format') {
    this.handleFormat(call, result);
  } else if (call.method === 'parse') {
    this.handleParse(call, result);
  } else if (call.method === 'get_all_supported_regions') {
    this.handleGetAllSupportedRegions(result);
  } else {
    result.notImplemented();
  }
}

call.method 是一个字符串,与 Dart 侧 invokeMapMethod 的第一个参数完全对应。

4.2 参数提取方式

let phone = call.argument('phone') as string;
let region = call.argument('region') as string;

call.argument(key) 从 Dart 侧传入的 Map 中提取指定 key 的值。返回类型是 ESObject(类似 any),需要通过 as string 进行类型断言。

参数对应关系:

Dart 侧 ArkTS 侧
{'phone': '+8613123456789'} call.argument('phone') → '+8613123456789'
{'region': 'CN'} call.argument('region') → 'CN'

4.3 参数校验

if (phone === null || phone.length === 0) {
  result.error('InvalidParameters',
    "Invalid 'phone' parameter.", null);
  return;
}

校验逻辑只检查 phone 参数,region 参数允许为 null(会使用默认值 'CN'):

let useRegion: string = region !== null ? region : 'CN';
phone region 行为
null 任意 返回 InvalidParameters 错误
'' 任意 返回 InvalidParameters 错误
'+86...' null 使用默认 region=‘CN’
'+86...' 'CN' 正常处理

4.4 handleFormat 的完整执行流程

handleFormat(call, result)
    │
    ├── ① 提取参数
    │   phone = '+8613123456789'
    │   region = 'CN'
    │
    ├── ② 参数校验
    │   phone 非空 → 通过 ✅
    │
    ├── ③ 确定区域
    │   useRegion = 'CN'
    │
    ├── ④ 创建 AsYouTypeFormatter
    │   formatter = phoneUtil.getAsYouTypeFormatter('CN')
    │   formatter.clear()
    │
    ├── ⑤ 逐字符格式化
    │   for (i = 0; i < 14; i++)
    │     formatted = formatter.inputDigit(phone[i])
    │
    ├── ⑥ 构建响应
    │   response = Map { 'formatted': '+86 131 2345 6789' }
    │
    ├── ⑦ Map → Record 转换
    │   record = { formatted: '+86 131 2345 6789' }
    │
    └── ⑧ 返回结果
        result.success(record)

五、AsYouTypeFormatter 逐字符格式化详解

5.1 格式化过程

'+8613123456789' 为例,handleFormat 中的循环:

for (let i = 0; i < phone.length; i++) {
  formatted = formatter.inputDigit(phone.charAt(i));
}

每次 inputDigit() 的返回值:

步骤 输入字符 返回值 内部状态变化
1 + + isInternational=true
2 8 +8 countryCode=‘8’
3 6 +86 countryCode=‘86’, region=‘CN’
4 1 +86 1 nationalNumber=‘1’
5 3 +86 13 nationalNumber=‘13’
6 1 +86 131 nationalNumber=‘131’
7 2 +86 131 2 nationalNumber=‘1312’
8 3 +86 131 23 nationalNumber=‘13123’
9 4 +86 131 234 nationalNumber=‘131234’
10 5 +86 131 2345 nationalNumber=‘1312345’
11 6 +86 131 2345 6 nationalNumber=‘13123456’
12 7 +86 131 2345 67 nationalNumber=‘131234567’
13 8 +86 131 2345 678 nationalNumber=‘1312345678’
14 9 +86 131 2345 6789 nationalNumber=‘13123456789’

5.2 区号识别过程

在步骤 2-3 中,inputDigit 尝试识别区号:

步骤 2: countryCode='8'
  → findRegionByCode('8') → null(没有区号为8的国家)
  → 继续累积

步骤 3: countryCode='86'
  → findRegionByCode('86') → 'CN' ✅
  → region 更新为 'CN'
  → 后续数字按中国格式化

5.3 国内号码的格式化

识别到 CN 后,nationalNumber 的格式化使用 formatPartialNumber()

// 国际模式下的输出拼接
this.currentOutput = '+' + this.countryCode + ' '
  + this.formatPartialNumber(this.nationalNumber);

formatPartialNumber() 按长度分段:

长度 1-3:  直接输出          '131'
长度 4-6:  3+N              '131 2'  '131 23'  '131 234'
长度 7-10: 3+3+N            '131 234 5'  ...  '131 2345 678'
长度 11+:  3+4+N            '131 2345 6789'

六、多国格式化结果对比

6.1 10 国 format() 实际调用结果

国家 输入 格式化结果
中国手机 +8613123456789 +86 131 2345 6789
中国固话 +861012345678 +86 101 234 5678
美国 +12015550123 +1 201 555 0123
英国 +447400123456 +44 740 012 3456
日本 +819012345678 +81 901 234 5678
德国 +4915123456789 +49 151 2345 6789
法国 +33612345678 +33 612 345 678
澳大利亚 +61412345678 +61 412 345 678
巴西 +5511912345678 +55 119 1234 5678
俄罗斯 +79123456789 +7 912 345 6789

6.2 格式化差异分析

注意 format() 使用的是 AsYouTypeFormatter(逐字符格式化),它的输出与 formatInternational()(一次性格式化)可能略有不同:

方法 格式化方式 CN 结果
format() AsYouTypeFormatter 逐字符 +86 131 2345 6789
formatInternational() 一次性 formatWithSpaces +86 131 2345 6789

对于中国号码,两者结果一致。但对于某些国家,逐字符格式化可能产生不同的分组方式,因为 AsYouTypeFormatter 使用通用的 formatPartialNumber() 而非国家专用格式化函数。


七、错误处理链路

7.1 参数为空

App: plugin.format('', 'CN')
  → Dart: invokeMapMethod('format', {'phone': '', 'region': 'CN'})
  → ArkTS: phone.length === 0
  → result.error('InvalidParameters', "Invalid 'phone' parameter.", null)
  → Dart: PlatformException(InvalidParameters, ...)
  → App: catch (e) { ... }

7.2 格式化异常

App: plugin.format('+999...', 'XX')
  → ArkTS: AsYouTypeFormatter 处理
  → 如果抛出异常 → catch (e)
  → result.error('FORMAT_ERROR', 'Failed to format phone number', null)
  → Dart: PlatformException(FORMAT_ERROR, ...)

7.3 Dart 侧的防御性处理

return await _channel.invokeMapMethod<String, String>(
  'format', {...}
) ?? <String, String>{};  // null 时返回空 Map

?? <String, String>{} 确保即使 ArkTS 侧返回 null,Dart 侧也不会抛出空指针异常。


八、性能分析

8.1 各阶段耗时

阶段 预估耗时 说明
Dart 参数构建 < 0.01ms Map 创建
StandardMessageCodec 编码 < 0.1ms 二进制序列化
Engine 传输 < 1ms 进程内通信
ArkTS 参数提取 < 0.01ms call.argument()
AsYouTypeFormatter < 0.5ms 逐字符格式化
Map→Record 转换 < 0.01ms 1 个字段
结果回传 < 1ms 反向传输
总计 < 3ms 用户无感知

8.2 与 formatNumberSync 的性能对比

指标 format()(异步) formatNumberSync()(同步)
耗时 ~3ms < 0.1ms
跨平台通信 有(MethodChannel) 无(纯 Dart)
适用场景 手动触发 实时输入
格式化精度 AsYouTypeFormatter Mask 匹配

为什么实时输入用同步LibPhonenumberTextFormatterformatEditUpdate() 在每次按键时调用,如果使用异步的 format() 会导致格式化延迟和闪烁。因此实时输入场景必须使用同步的 formatNumberSync()


九、与 parse() 调用链路的对比

9.1 相同点

相同点 说明
通道 同一个 MethodChannel
编码方式 同一个 StandardMessageCodec
参数格式 都是 {'phone': ..., 'region': ...}
错误处理 都有参数校验 + 异常兜底

9.2 不同点

对比项 format() parse()
方法名 'format' 'parse'
处理方式 AsYouTypeFormatter 逐字符 PhoneNumberUtil.parse() 一次性
返回字段数 1 个(formatted) 7 个(type, e164, …)
有效性验证 isValidNumber()
错误码 FORMAT_ERROR InvalidNumber / PARSE_ERROR
Dart 返回类型 Map<String, String> Map<String, dynamic>

十、format() 在 example 工程中的使用

10.1 手动格式化按钮

ElevatedButton(
  child: const Text('Format\n(Async)'),
  onPressed: () async {
    final res = await _plugin.format(
      manualFormatController.text,
      _currentSelectedCountry.countryCode,
    );
    setState(() =>
      manualFormatController.text = res['formatted'] ?? '');
  },
)

10.2 使用流程

用户输入: +8613123456789
    │
    ├── 点击 "Format (Async)" 按钮
    │
    ├── plugin.format('+8613123456789', 'CN')
    │   → MethodChannel → ArkTS → AsYouTypeFormatter
    │   → '+86 131 2345 6789'
    │
    └── TextField 更新为格式化后的号码

十一、AsYouTypeFormatter 的国家专用格式化

11.1 10 个国家的专用格式化函数

AsYouTypeFormatter 对 10 个主要国家提供了专用的 formatPartialXxx() 函数,其余国家使用通用的 formatPartialNumber()

国家 专用函数 格式特点
CN formatPartialChinese() 3-4-4 分组
US/CA formatPartialNANP() (NPA) NXX-XXXX
GB formatPartialUK() 0XXXX XXXXXX
JP formatPartialJapanese() 0XX-XXXX-XXXX
DE formatPartialGerman() 0XXX XXXX XXXX
FR formatPartialFrench() 0X XX XX XX XX
AU formatPartialAustralian() 0XXX XXX XXX
IN formatPartialIndian() XXXXX XXXXX
BR formatPartialBrazilian() (XX) XXXXX-XXXX
RU formatPartialRussian() 8 XXX XXX-XX-XX

11.2 国际模式 vs 国内模式

AsYouTypeFormatter 根据输入是否以 + 开头,选择不同的格式化路径:

inputDigit(digit: string): string {
  if (digit === '+' && this.nationalNumber.length === 0) {
    this.isInternational = true;  // 国际模式
    // ...
  }
  // ...
  if (this.isInternational) {
    // 国际模式:+区号 + 通用格式
    this.currentOutput = '+' + this.countryCode + ' '
      + this.formatPartialNumber(this.nationalNumber);
  } else {
    // 国内模式:使用国家专用格式
    this.currentOutput =
      this.formatPartialNational(this.nationalNumber);
  }
}

关键区别:

模式 触发条件 格式化函数 CN 示例
国际 输入以 + 开头 formatPartialNumber() +86 131 2345 6789
国内 输入不以 + 开头 formatPartialChinese() 131 2345 6789

重要发现:国际模式下,format() 使用的是 通用formatPartialNumber()(按 3-3-4 或 3-4-N 分组),而非国家专用函数。这意味着国际模式下所有国家的分组方式是统一的。

11.3 国内模式的中国号码格式化

private formatPartialChinese(number: string): string {
  let len = number.length;
  if (len <= 3) {
    return number;                    // '131'
  }
  if (len <= 7) {
    return number.substring(0, 3) + ' '
      + number.substring(3);          // '131 2345'
  }
  return number.substring(0, 3) + ' '
    + number.substring(3, 7) + ' '
    + number.substring(7);            // '131 2345 6789'
}

中国手机号的 3-4-4 分组:

输入: '13123456789'
  len=3:  '131'
  len=4:  '131 2'
  len=7:  '131 2345'
  len=8:  '131 2345 6'
  len=11: '131 2345 6789'

11.4 国内模式的美国号码格式化

private formatPartialNANP(number: string): string {
  let len = number.length;
  if (len <= 3) {
    return '(' + number;              // '(201'
  }
  if (len <= 6) {
    return '(' + number.substring(0, 3) + ') '
      + number.substring(3);          // '(201) 555'
  }
  return '(' + number.substring(0, 3) + ') '
    + number.substring(3, 6) + '-'
    + number.substring(6);            // '(201) 555-0123'
}

美国号码的 (NPA) NXX-XXXX 格式:

输入: '2015550123'
  len=1:  '(2'
  len=3:  '(201'
  len=4:  '(201) 5'
  len=6:  '(201) 555'
  len=7:  '(201) 555-0'
  len=10: '(201) 555-0123'

11.5 国内模式的日本号码格式化

private formatPartialJapanese(number: string): string {
  let len = number.length;
  if (len <= 2) {
    return '0' + number;              // '090'
  }
  if (len <= 6) {
    return '0' + number.substring(0, 2) + '-'
      + number.substring(2);          // '090-1234'
  }
  return '0' + number.substring(0, 2) + '-'
    + number.substring(2, 6) + '-'
    + number.substring(6);            // '090-1234-5678'
}

日本号码使用连字符分隔,且加上前导 0

输入: '9012345678'
  len=1:  '09'
  len=2:  '090'
  len=3:  '090-1'
  len=6:  '090-1234'
  len=7:  '090-1234-5'
  len=10: '090-1234-5678'

十二、format() 与 getFormattedParseResult() 的关系

12.1 getFormattedParseResult 的实现

getFormattedParseResult() 是一个更高级的 API,它内部组合了 parse() 和格式化逻辑:

Future<FormatPhoneResult?> getFormattedParseResult(
  final String phoneNumber,
  final CountryWithPhoneCode country, {
  final PhoneNumberType phoneNumberType =
      PhoneNumberType.mobile,
  final PhoneNumberFormat phoneNumberFormat =
      PhoneNumberFormat.international,
}) async {
  try {
    final res = await parse(
      phoneNumber,
      region: country.countryCode,
    );

    late final String formattedNumber;
    if (phoneNumberFormat == PhoneNumberFormat.international) {
      formattedNumber = res['international'] ?? '';
    } else if (phoneNumberFormat == PhoneNumberFormat.national) {
      formattedNumber = res['national'] ?? '';
    } else {
      formattedNumber = '';
    }

    return FormatPhoneResult(
      e164: res['e164'] ?? '',
      formattedNumber: formattedNumber,
    );
  } catch (e) {
    return null;
  }
}

12.2 format() vs getFormattedParseResult()

对比项 format() getFormattedParseResult()
内部调用 MethodChannel → handleFormat MethodChannel → handleParse
格式化引擎 AsYouTypeFormatter formatInternational/formatNational
返回类型 Map<String, String> FormatPhoneResult?
返回字段 formatted e164 + formattedNumber
有效性验证 有(parse 内部验证)
无效号码 返回部分格式化结果 返回 null
适用场景 快速格式化 格式化 + 验证

12.3 三种格式化方式的选择指南

需要实时输入格式化?
  └── YES → formatNumberSync()(同步,Dart 侧)

需要验证号码有效性?
  └── YES → getFormattedParseResult()(异步,含验证)

只需要格式化结果?
  └── YES → format()(异步,ArkTS 侧)
场景 推荐方法 原因
TextField 实时格式化 formatNumberSync() 同步,< 0.1ms
手动点击格式化按钮 format() 异步,精度高
提交前验证 + 格式化 getFormattedParseResult() 一步完成验证和格式化
批量格式化号码列表 formatNumberSync() 同步,无通信开销
显示 e164 格式 getFormattedParseResult() 直接返回 e164

总结

本文完整追踪了 format() 异步格式化的 6 步调用链路。关键要点回顾:

  1. format() 是一个 跨平台异步调用,从 Dart 经 MethodChannel 到 ArkTS,再将结果回传
  2. 参数通过 StandardMessageCodec 编码为二进制字节流,在 Flutter Engine 内部传输
  3. ArkTS 侧使用 AsYouTypeFormatter 进行 逐字符格式化,模拟用户逐个输入数字的过程
  4. 区号识别在输入前 1-3 个数字时完成,识别后切换到对应国家的格式化规则
  5. 返回值只有 1 个字段 formatted,经过 Map→Record 转换后通过 result.success() 回传
  6. 整个链路耗时约 3ms 以内,用户无感知,但不适合实时输入场景(应使用 formatNumberSync()

下一篇我们将分析 formatNumberSync() 同步格式化与 Mask 匹配原理——一个完全在 Dart 侧执行、无需跨平台通信的高性能格式化方案。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐