前言

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

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

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

上一篇我们分析了 formatNumberSync() 同步格式化与 Mask 匹配原理。本篇将深入分析 parse() 方法——它是 format() 的"逆操作"。format() 将号码变成人类可读的格式,而 parse() 则从一个电话号码字符串中 提取出结构化的元数据:国家代码、E.164 格式、国内格式、国际格式、号码类型、地区代码等 7 个字段。

parse() 是号码验证和信息提取的核心 API。理解它的实现,就理解了如何从一串数字中还原出完整的电话号码语义信息。


一、parse() 的功能定位

1.1 parse() 在 API 体系中的位置

flutter_libphonenumber API 体系:

┌─────────────────────────────────────────────────┐
│                  初始化层                         │
│  init() → getAllSupportedRegions()               │
│         → CountryManager.loadCountries()         │
└─────────────────────────────────────────────────┘
                      │
┌─────────────────────────────────────────────────┐
│                  格式化层                         │
│  format()           → 异步格式化(ArkTS 原生)    │
│  formatNumberSync() → 同步格式化(Dart Mask)     │
└─────────────────────────────────────────────────┘
                      │
┌─────────────────────────────────────────────────┐
│               解析与验证层 ← 本篇                 │
│  parse()            → 号码解析与元数据提取        │
│  getFormattedParseResult() → 格式化+验证一步到位  │
└─────────────────────────────────────────────────┘

1.2 parse() vs format() 的关系

对比项 format() parse()
方向 号码 → 格式化字符串 号码 → 结构化数据
输入 电话号码 + 地区 电话号码 + 地区(可选)
输出 Map<String, String> Map<String, dynamic>
返回字段数 1 个(formatted) 7 个(type, e164, international…)
执行位置 ArkTS 原生侧 ArkTS 原生侧
通信方式 MethodChannel MethodChannel
用途 显示格式化号码 验证号码、提取元数据

1.3 parse() 返回的 7 个字段

{
  "type": "mobile",
  "e164": "+8613123456789",
  "international": "+86 131 2345 6789",
  "national": "131 2345 6789",
  "country_code": "86",
  "region_code": "CN",
  "national_number": "13123456789"
}
字段 类型 说明 示例
type String 号码类型 mobile, fixedLine, fixedOrMobile
e164 String E.164 国际标准格式 +8613123456789
international String 国际格式(带空格) +86 131 2345 6789
national String 国内格式 131 2345 6789
country_code String 国家电话区号 86
region_code String ISO 3166-1 地区代码 CN
national_number String 国内号码(纯数字) 13123456789

二、完整调用链路

2.1 四层调用架构

┌──────────────────────────────────────────────────┐
│  第 1 层:App 调用层                               │
│  _plugin.parse('+8613123456789', region: 'CN')   │
└──────────────────────┬───────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────┐
│  第 2 层:Dart 平台实现层                          │
│  FlutterLibphonenumberOhos.parse()               │
│  → _channel.invokeMapMethod('parse', {...})      │
└──────────────────────┬───────────────────────────┘
                       │ MethodChannel
┌──────────────────────▼───────────────────────────┐
│  第 3 层:ArkTS 插件分发层                         │
│  FlutterLibphonenumberPlugin.onMethodCall()      │
│  → handleParse(call, result)                     │
└──────────────────────┬───────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────┐
│  第 4 层:ArkTS 核心解析层                         │
│  PhoneNumberUtil.parse()                         │
│  → parseInternationalNumber() 或                 │
│  → parseNationalNumber()                         │
│  → isValidNumber() → getNumberType()             │
│  → formatE164() / formatInternational()          │
│  → formatNational()                              │
└──────────────────────────────────────────────────┘

2.2 数据流转

输入: phone='+8613123456789', region='CN'

Dart 侧:
  parse('+8613123456789', region: 'CN')
  → invokeMapMethod('parse', {phone: '+8613123456789', region: 'CN'})
  → StandardMessageCodec 编码
  → BinaryMessenger 传输

ArkTS 侧:
  onMethodCall(call) → call.method === 'parse'
  → handleParse(call, result)
  → phone = '+8613123456789', region = 'CN'
  → phoneUtil.parse('+8613123456789', 'CN')
  → PhoneNumber(86, '13123456789', '+8613123456789')
  → isValidNumber() → true
  → getNumberType() → 'mobile'
  → formatE164() → '+8613123456789'
  → formatInternational() → '+86 131 2345 6789'
  → formatNational() → '131 2345 6789'
  → Map → Record → result.success()

Dart 侧:
  StandardMessageCodec 解码
  → Map<String, dynamic>
  → 返回给调用者

三、Dart 侧的调用入口

3.1 App 层调用

在 example 工程的 main.dart 中,Parse 按钮的点击处理:

ElevatedButton(
  child: const Text('Parse'),
  onPressed: () async {
    final res = await _plugin.parse(
      manualFormatController.text,
      region: _currentSelectedCountry.countryCode,
    );
    const encoder = JsonEncoder.withIndent('  ');
    setState(() => parsedData = encoder.convert(res));
  },
),

3.2 平台接口层的抽象定义

FlutterLibphonenumberPlatform 中定义了 parse() 的抽象接口:

/// Parse a single string and return a map.
/// Throws an error if the number is not valid.
///
/// Given '+4930123123123', the response will be:
/// {
///   country_code: 49,
///   e164: '+4930123123123',
///   national: '030 123 123 123',
///   type: 'mobile',
///   international: '+49 30 123 123 123',
///   national_number: '030123123123',
/// }
Future<Map<String, dynamic>> parse(
  final String phone, {
  final String? region,
}) async {
  throw UnimplementedError(
      'parse() has not been implemented.');
}

3.3 鸿蒙平台实现

FlutterLibphonenumberOhos 中的具体实现:


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

3.4 MethodChannel 参数传递

方法名: 'parse'
参数 Map:
  {
    'phone': '+8613123456789',  // 待解析的号码
    'region': 'CN',             // 默认地区(可选)
  }

编码方式: StandardMessageCodec
  → 将 Map 编码为二进制
  → 通过 BinaryMessenger 传输到 ArkTS 侧

3.5 region 参数的作用

region 值 场景 说明
'CN' 用户选择了中国 用于解析不含区号的国内号码
'US' 用户选择了美国 用于解析不含 +1 的美国号码
null 未指定 必须输入含 + 的国际号码

当号码以 + 开头时,region 参数实际上不会被使用,因为国家信息可以从号码本身提取。region 主要用于解析 不含国际区号的国内号码


四、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);  // ← 进入 parse 处理
  } else if (call.method === 'get_all_supported_regions') {
    this.handleGetAllSupportedRegions(result);
  } else {
    result.notImplemented();
  }
}

4.2 handleParse 完整实现

private handleParse(
  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: string =
        region !== null ? region : '';
    let phoneNumber: PhoneNumber | null =
        this.phoneUtil.parse(phone, useRegion);

    // ④ 有效性验证
    if (phoneNumber === null
        || !this.phoneUtil.isValidNumber(phoneNumber)) {
      result.error(
        'InvalidNumber',
        'Number ' + phone + ' is invalid',
        null
      );
      return;
    }

    // ⑤ 提取元数据
    let regionCode =
        this.phoneUtil.getRegionCodeForNumber(phoneNumber);
    let numberType =
        this.phoneUtil.getNumberType(phoneNumber);

    // ⑥ 构建返回 Map
    let response: Map<string, string> = new Map();
    response.set('type', numberType);
    response.set('e164',
        this.phoneUtil.formatE164(phoneNumber));
    response.set('international',
        this.phoneUtil.formatInternational(phoneNumber));
    response.set('national',
        this.phoneUtil.formatNational(
            phoneNumber, regionCode));
    response.set('country_code',
        phoneNumber.countryCode.toString());
    response.set('region_code', regionCode);
    response.set('national_number',
        phoneNumber.nationalNumber);

    // ⑦ 返回结果
    result.success(this.convertMapToRecord(response));
  } catch (e) {
    result.error(
      'PARSE_ERROR',
      'Failed to parse phone number',
      null
    );
  }
}

4.3 handleParse 的 7 个步骤

步骤 操作 说明
提取参数 从 MethodCall 中获取 phone 和 region
参数校验 phone 为空则返回错误
核心解析 调用 PhoneNumberUtil.parse()
有效性验证 isValidNumber() 检查号码是否有效
提取元数据 getRegionCodeForNumber() + getNumberType()
构建返回 Map 7 个字段的 Map
返回结果 Map → Record → result.success()

五、PhoneNumberUtil.parse() 核心解析逻辑

5.1 方法签名

parse(
  phoneString: string,
  defaultRegion: string
): PhoneNumber | null

5.2 完整实现

parse(
  phoneString: string,
  defaultRegion: string
): PhoneNumber | null {
  // ① 清理输入:去掉空格、连字符、括号、点
  let cleanNumber = phoneString.replace(
    /[\s\-\(\)\.]/g, ''
  );

  // ② 判断是国际号码还是国内号码
  if (cleanNumber.charAt(0) === '+') {
    return this.parseInternationalNumber(cleanNumber);
  }

  // ③ 国内号码需要 defaultRegion
  if (defaultRegion.length > 0) {
    return this.parseNationalNumber(
      cleanNumber, defaultRegion
    );
  }

  return null;
}

5.3 解析流程图

输入: phoneString, defaultRegion

        ┌─────────────────┐
        │  清理输入字符串    │
        │  去掉 空格/-/()/. │
        └────────┬────────┘
                 │
        ┌────────▼────────┐
        │  以 '+' 开头?    │
        └────────┬────────┘
           是 ↙     ↘ 否
  ┌──────────┐   ┌──────────────┐
  │ 国际号码   │   │ defaultRegion │
  │ 解析      │   │ 非空?        │
  └─────┬────┘   └──────┬───────┘
        │          是 ↙    ↘ 否
        │   ┌──────────┐  ┌──────┐
        │   │ 国内号码   │  │ null │
        │   │ 解析      │  └──────┘
        │   └─────┬────┘
        │         │
        ▼         ▼
  ┌─────────────────────┐
  │  PhoneNumber 对象     │
  │  或 null              │
  └─────────────────────┘

5.4 输入清理规则

let cleanNumber = phoneString.replace(
  /[\s\-\(\)\.]/g, ''
);

正则表达式 [\s\-\(\)\.] 匹配以下字符:

字符 说明 示例
\s 空白字符(空格、制表符) +86 131+86131
\- 连字符 555-01235550123
\( 左括号 (201)201
\) 右括号 (201)201
\. 点号 06.12.34061234

清理示例:

'+86 131 2345 6789'   → '+8613123456789'
'+1 (201) 555-0123'   → '+12015550123'
'+33 6 12 34 56 78'   → '+33612345678'
'+49 151 2345 6789'   → '+4915123456789'
'06.12.34.56.78'      → '0612345678'

六、国际号码解析:parseInternationalNumber()

6.1 完整实现

private parseInternationalNumber(
  number: string
): PhoneNumber | null {
  // 去掉开头的 '+'
  let withoutPlus = number.substring(1);

  // 从长到短尝试匹配国家区号
  for (let len = 3; len >= 1; len--) {
    let possibleCode = withoutPlus.substring(0, len);
    let region = this.getRegionForCountryCode(
      parseInt(possibleCode)
    );
    if (region !== null) {
      return new PhoneNumber(
        parseInt(possibleCode),
        withoutPlus.substring(len),
        number
      );
    }
  }
  return null;
}

6.2 从长到短的匹配策略

国际电话区号的长度为 1-3 位。为了正确匹配,必须 从长到短 尝试:

输入: '+8613123456789'
withoutPlus: '8613123456789'

第 1 次 (len=3): possibleCode = '861'
  → getRegionForCountryCode(861) → null ❌

第 2 次 (len=2): possibleCode = '86'
  → getRegionForCountryCode(86) → 'CN' ✅
  → return PhoneNumber(86, '13123456789', '+8613123456789')

6.3 为什么必须从长到短

如果从短到长匹配,会出现错误:

输入: '+8613123456789'

从短到长(错误方式):
  len=1: possibleCode = '8'
  → getRegionForCountryCode(8) → null ❌(没有区号为 8 的国家)
  len=2: possibleCode = '86'
  → getRegionForCountryCode(86) → 'CN' ✅

这个例子碰巧正确,但看另一个例子:

输入: '+12015550123'

从短到长:
  len=1: possibleCode = '1'
  → getRegionForCountryCode(1) → 'US' ✅
  → return PhoneNumber(1, '2015550123')  ← 正确

从长到短:
  len=3: possibleCode = '120'
  → getRegionForCountryCode(120) → null ❌
  len=2: possibleCode = '12'
  → getRegionForCountryCode(12) → null ❌
  len=1: possibleCode = '1'
  → getRegionForCountryCode(1) → 'US' ✅
  → return PhoneNumber(1, '2015550123')  ← 正确

从长到短的关键优势在于处理 区号前缀冲突 的情况:

假设存在:
  区号 '7'  → 俄罗斯
  区号 '77' → 哈萨克斯坦(假设)

输入: '+77012345678'

从长到短:
  len=3: '770' → null
  len=2: '77' → 哈萨克斯坦 ✅(正确)

从短到长:
  len=1: '7' → 俄罗斯 ❌(错误!)

6.4 getRegionForCountryCode 的实现

private getRegionForCountryCode(
  code: number
): string | null {
  let result: string | null = null;
  this.countryDataMap.forEach(
    (data: CountryData, region: string) => {
      if (parseInt(data.code) === code) {
        result = region;
      }
    }
  );
  return result;
}

遍历 57 个国家的 countryDataMap,找到 code 匹配的国家。

注意:当多个国家共享同一区号时(如 US 和 CA 都是 +1),forEach 会返回最后一个匹配的国家。这是一个已知的限制。

6.5 PhoneNumber 对象的构造

return new PhoneNumber(
  parseInt(possibleCode),  // countryCode: 86
  withoutPlus.substring(len),  // nationalNumber: '13123456789'
  number  // rawInput: '+8613123456789'
);
字段 说明
countryCode 86 国家电话区号(数字)
nationalNumber ‘13123456789’ 国内号码部分
rawInput ‘+8613123456789’ 原始输入

七、国内号码解析:parseNationalNumber()

7.1 完整实现

private parseNationalNumber(
  number: string,
  region: string
): PhoneNumber | null {
  let data = this.countryDataMap.get(region);
  if (data === undefined) {
    return null;
  }

  let nationalNumber = number;
  // 去掉国内前缀(如中国的 '0'、俄罗斯的 '8')
  if (data.nationalPrefix.length > 0
      && number.indexOf(data.nationalPrefix) === 0) {
    nationalNumber = number.substring(
      data.nationalPrefix.length
    );
  }

  return new PhoneNumber(
    parseInt(data.code),
    nationalNumber,
    number
  );
}

7.2 nationalPrefix 的作用

各国的国内拨号前缀(trunk prefix)不同:

国家 nationalPrefix 说明 示例
CN '0' 国内长途前缀 010-12345678 → 去掉 01012345678
JP '0' 国内前缀 090-1234-5678 → 去掉 09012345678
GB '0' 国内前缀 07400 123456 → 去掉 07400123456
RU '8' 国内前缀 8 912 345-67-89 → 去掉 89123456789
HU '06' 国内前缀 06 20 1234567 → 去掉 06201234567
US '' 无前缀 2015550123 → 不变 → 2015550123
IT '' 无前缀 3123456789 → 不变 → 3123456789

7.3 国内号码解析示例

中国手机号(带前缀 0):

输入: '013123456789', region: 'CN'

data = countryDataMap.get('CN')
  → CountryData(name='China', code='86', ..., nationalPrefix='0')

nationalPrefix = '0', number.indexOf('0') === 0 → true
nationalNumber = '013123456789'.substring(1) = '13123456789'

return PhoneNumber(86, '13123456789', '013123456789')

美国号码(无前缀):

输入: '2015550123', region: 'US'

data = countryDataMap.get('US')
  → CountryData(name='United States', code='1', ..., nationalPrefix='')

nationalPrefix = '' → length === 0 → 不去前缀
nationalNumber = '2015550123'

return PhoneNumber(1, '2015550123', '2015550123')

俄罗斯号码(前缀 8):

输入: '89123456789', region: 'RU'

data = countryDataMap.get('RU')
  → CountryData(name='Russia', code='7', ..., nationalPrefix='8')

nationalPrefix = '8', number.indexOf('8') === 0 → true
nationalNumber = '89123456789'.substring(1) = '9123456789'

return PhoneNumber(7, '9123456789', '89123456789')

7.4 国际号码 vs 国内号码解析对比

对比项 国际号码 国内号码
输入格式 + 开头 不以 + 开头
国家识别 从区号自动识别 需要 defaultRegion 参数
前缀处理 去掉 + 和区号 去掉 nationalPrefix
示例 +8613123456789 → CN 13123456789 + region=‘CN’

八、号码有效性验证:isValidNumber()

8.1 完整实现

isValidNumber(phoneNumber: PhoneNumber): boolean {
  // ① 获取地区代码
  let region = this.getRegionCodeForNumber(phoneNumber);
  if (region.length === 0) {
    return false;
  }

  // ② 获取国家数据
  let data = this.countryDataMap.get(region);
  if (data === undefined) {
    return false;
  }

  // ③ 长度验证
  let number = phoneNumber.nationalNumber;
  return number.length >= 7 && number.length <= 15;
}

8.2 验证规则

验证步骤 条件 失败时返回
地区识别 region.length > 0 false
国家数据存在 data !== undefined false
号码长度 7 <= length <= 15 false

8.3 长度范围的依据

ITU-T E.164 标准规定:

规则 说明
最小长度 7 位 最短的有效电话号码
最大长度 15 位 E.164 标准最大长度(含国家代码)

各国号码长度示例:

国家 手机号长度 固话长度 说明
CN 11 位 10-12 位 手机 1xx xxxx xxxx
US 10 位 10 位 NANP 统一 10 位
GB 10 位 10 位 07xxx xxxxxx
JP 10 位 9-10 位 090-xxxx-xxxx
DE 10-11 位 7-11 位 可变长度
SG 8 位 8 位 统一 8 位
DK 8 位 8 位 统一 8 位
HK 8 位 8 位 统一 8 位

8.4 验证失败的场景

场景 1: 未知区号
  PhoneNumber(999, '123456789')
  → getRegionCodeForNumber() → '' → false

场景 2: 号码太短
  PhoneNumber(86, '131234')  // 6 位
  → length < 7 → false

场景 3: 号码太长
  PhoneNumber(86, '1312345678901234')  // 16 位
  → length > 15 → false

场景 4: 正常号码
  PhoneNumber(86, '13123456789')  // 11 位
  → 7 <= 11 <= 15 → true

九、号码类型判定:getNumberType()

9.1 完整实现

getNumberType(phoneNumber: PhoneNumber): string {
  let region = this.getRegionCodeForNumber(phoneNumber);
  if (region.length === 0) {
    return 'unknown';
  }

  let data = this.countryDataMap.get(region);
  if (data === undefined) {
    return 'unknown';
  }

  let number = phoneNumber.nationalNumber;

  // 根据不同国家的规则判断号码类型
  if (region === 'CN') {
    if (number.length === 11
        && number.charAt(0) === '1') {
      return 'mobile';
    }
    return 'fixedLine';
  } else if (region === 'US' || region === 'CA') {
    return 'fixedOrMobile';
  } else if (region === 'GB') {
    if (number.charAt(0) === '7') {
      return 'mobile';
    }
    return 'fixedLine';
  }
  // ... 更多国家规则
}

9.2 各国判定规则汇总

国家 手机号特征 固话特征 特殊情况
CN 11位 + 首位1 其他
US/CA 统一返回 fixedOrMobile
GB 首位7 7开头
JP 首位9/8/7 其他
DE 首位1 1开头
FR 首位6/7 其他
AU 首位4 4开头
IN 首位6-9 其他
BR 11位 + 第3位9 其他 区号2位+号码
RU 首位9 9开头

9.3 返回值类型

返回值 说明 适用国家
'mobile' 手机号 CN, GB, JP, DE, FR, AU, IN, BR, RU
'fixedLine' 固定电话 CN, GB, JP, DE, FR, AU, IN, BR, RU
'fixedOrMobile' 无法区分 US, CA(NANP 号码)
'unknown' 未知类型 未识别的国家或号码

9.4 为什么 US/CA 返回 fixedOrMobile

北美编号计划(NANP)中,手机号和固话号使用 相同的号码格式

美国手机号: (201) 555-0123
美国固话:   (201) 555-0456

两者格式完全相同:(NPA) NXX-XXXX
无法仅从号码格式区分手机和固话

这与中国不同——中国手机号以 1 开头(13x/15x/18x 等),固话以区号开头(010/021/0755 等),可以明确区分。

9.5 默认判定逻辑

当号码不属于上述 10 个有专用规则的国家时,使用默认判定:

// 默认判断
if (number.length >= 10
    && number.charAt(0) >= '1'
    && number.charAt(0) <= '9') {
  return 'mobile';
}
return 'unknown';

默认规则:10 位以上且首位为 1-9 → mobile,否则 → unknown


十、格式化输出:三种格式的生成

10.1 E.164 格式

formatE164(phoneNumber: PhoneNumber): string {
  return '+' + phoneNumber.countryCode.toString()
      + phoneNumber.nationalNumber;
}

E.164 是国际电信联盟定义的标准格式:

格式: +[国家代码][国内号码]
特点: 无空格、无连字符、无括号
最大长度: 15 位数字 + 1 个 '+' 号

示例:
  CN: +8613123456789
  US: +12015550123
  GB: +447400123456
  JP: +819012345678

10.2 国际格式

formatInternational(phoneNumber: PhoneNumber): string {
  let formatted = this.formatWithSpaces(
    phoneNumber.nationalNumber
  );
  return '+' + phoneNumber.countryCode.toString()
      + ' ' + formatted;
}

国际格式在 E.164 基础上添加空格分隔:

格式: +[国家代码] [格式化的国内号码]
特点: 有空格分隔,便于阅读

示例:
  CN: +86 131 2345 6789
  US: +1 201 555 0123
  GB: +44 740 012 3456
  JP: +81 901 234 5678

10.3 国内格式

formatNational(
  phoneNumber: PhoneNumber,
  region: string
): string {
  let data = this.countryDataMap.get(region);
  if (data === undefined) {
    return phoneNumber.nationalNumber;
  }
  return this.applyNationalFormat(
    phoneNumber.nationalNumber,
    region,
    data.nationalPrefix
  );
}

国内格式使用各国专用的格式化函数:

CN: 131 2345 6789(3-4-4 分组)
US: (201) 555-0123(NANP 格式)
GB: 07400 123456(0+4+6 分组)
JP: 090-1234-5678(0+2-4-4 分组)
FR: 06 12 34 56 78(0+1-2-2-2-2 分组)

10.4 三种格式的对比

以中国手机号 13123456789 为例:

格式 输出 用途
E.164 +8613123456789 存储、API 传输
International +86 131 2345 6789 国际显示
National 131 2345 6789 国内显示

以美国号码 2015550123 为例:

格式 输出 用途
E.164 +12015550123 存储、API 传输
International +1 201 555 0123 国际显示
National (201) 555-0123 美国国内显示

十一、多国 parse() 结果详解

11.1 亚洲国家

中国手机号:

{
  "type": "mobile",
  "e164": "+8613123456789",
  "international": "+86 131 2345 6789",
  "national": "131 2345 6789",
  "country_code": "86",
  "region_code": "CN",
  "national_number": "13123456789"
}

日本手机号:

{
  "type": "mobile",
  "e164": "+819012345678",
  "international": "+81 901 234 5678",
  "national": "090-1234-5678",
  "country_code": "81",
  "region_code": "JP",
  "national_number": "9012345678"
}

印度手机号:

{
  "type": "mobile",
  "e164": "+918123456789",
  "international": "+91 812 345 6789",
  "national": "081234 56789",
  "country_code": "91",
  "region_code": "IN",
  "national_number": "8123456789"
}

11.2 欧洲国家

英国手机号:

{
  "type": "mobile",
  "e164": "+447400123456",
  "international": "+44 740 012 3456",
  "national": "07400 123456",
  "country_code": "44",
  "region_code": "GB",
  "national_number": "7400123456"
}

法国手机号:

{
  "type": "mobile",
  "e164": "+33612345678",
  "international": "+33 612 345 678",
  "national": "06 12 34 56 78",
  "country_code": "33",
  "region_code": "FR",
  "national_number": "612345678"
}

德国手机号:

{
  "type": "mobile",
  "e164": "+4915123456789",
  "international": "+49 151 2345 6789",
  "national": "0151 2345 6789",
  "country_code": "49",
  "region_code": "DE",
  "national_number": "15123456789"
}

11.3 美洲国家

美国号码:

{
  "type": "fixedOrMobile",
  "e164": "+12015550123",
  "international": "+1 201 555 0123",
  "national": "(201) 555-0123",
  "country_code": "1",
  "region_code": "US",
  "national_number": "2015550123"
}

巴西手机号:

{
  "type": "mobile",
  "e164": "+5511912345678",
  "international": "+55 119 1234 5678",
  "national": "011 91234-5678",
  "country_code": "55",
  "region_code": "BR",
  "national_number": "11912345678"
}

11.4 手机号 vs 固话的 parse 结果对比

以中国为例:

字段 手机号 固话
type mobile fixedLine
e164 +8613123456789 +861012345678
international +86 131 2345 6789 +86 101 234 5678
national 131 2345 6789 010 1234 5678
country_code 86 86
region_code CN CN
national_number 13123456789 1012345678

以英国为例:

字段 手机号 固话
type mobile fixedLine
e164 +447400123456 +441212345678
international +44 740 012 3456 +44 121 234 5678
national 07400 123456 0121 234 5678
national_number 7400123456 1212345678

十二、parse() 的错误处理机制

12.1 三种错误类型

错误类型 错误码 触发条件 说明
参数无效 InvalidParameters phone 为空或 null 输入校验失败
号码无效 InvalidNumber parse 返回 null 或 isValidNumber 为 false 号码不合法
解析异常 PARSE_ERROR try-catch 捕获异常 未预期的错误

12.2 错误处理流程

handleParse(call, result):

  phone = call.argument('phone')
  │
  ├── phone 为空?
  │   └── YES → result.error('InvalidParameters', ...)
  │
  ├── phoneUtil.parse() 返回 null?
  │   └── YES → result.error('InvalidNumber', ...)
  │
  ├── isValidNumber() 返回 false?
  │   └── YES → result.error('InvalidNumber', ...)
  │
  ├── try-catch 捕获异常?
  │   └── YES → result.error('PARSE_ERROR', ...)
  │
  └── 正常 → result.success(response)

12.3 Dart 侧的错误处理

在 App 层,parse() 的错误会以 PlatformException 的形式抛出:

try {
  final res = await _plugin.parse(
    manualFormatController.text,
    region: _currentSelectedCountry.countryCode,
  );
  // 成功处理
  setState(() => parsedData =
      JsonEncoder.withIndent('  ').convert(res));
} catch (e) {
  // 错误处理
  setState(() => parsedData = 'Parse error: $e');
}

12.4 常见错误场景

场景 输入 错误 原因
空输入 '' InvalidParameters phone 为空
无效区号 '+999123456' InvalidNumber 区号 999 不存在
号码太短 '+86131' InvalidNumber 国内号码只有 3 位
号码太长 '+861312345678901234' InvalidNumber 超过 15 位
无区号无地区 '13123456789' + region=null InvalidNumber 无法确定国家

十三、parse() 与 getFormattedParseResult() 的关系

13.1 getFormattedParseResult 的定位

getFormattedParseResult()parse() 的上层封装,提供"格式化 + 验证"一步到位的能力:

parse():
  输入: 号码字符串
  输出: Map<String, dynamic>(7 个字段)
  用途: 获取完整的解析元数据

getFormattedParseResult():
  输入: 号码字符串 + 国家 + 格式选项
  输出: FormatPhoneResult?(2 个字段)
  用途: 获取格式化结果 + E.164 + 有效性判断

13.2 完整实现

Future<FormatPhoneResult?> getFormattedParseResult(
  final String phoneNumber,
  final CountryWithPhoneCode country, {
  final PhoneNumberType phoneNumberType =
      PhoneNumberType.mobile,
  final PhoneNumberFormat phoneNumberFormat =
      PhoneNumberFormat.international,
}) async {
  try {
    // ① 调用 parse() 获取解析结果
    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) {
    // parse() 失败 → 返回 null(号码无效)
  }
  return null;
}

13.3 FormatPhoneResult 数据类

class FormatPhoneResult {
  FormatPhoneResult({
    required this.e164,
    required this.formattedNumber,
  });
  String formattedNumber;
  String e164;
}
字段 类型 说明 示例
e164 String E.164 标准格式 +8613123456789
formattedNumber String 请求的格式化结果 +86 131 2345 6789

13.4 有效性判断的巧妙设计

getFormattedParseResult() 的返回值是 FormatPhoneResult?(可空)。这意味着:

final result = await getFormattedParseResult(
  phoneNumber, country
);

if (result != null) {
  // 号码有效,可以使用 result.e164 和 result.formattedNumber
  print('E.164: ${result.e164}');
  print('Formatted: ${result.formattedNumber}');
} else {
  // 号码无效
  print('Invalid phone number');
}

返回 null 就意味着号码无效——不需要额外的 isValid 字段。

13.5 parse() vs getFormattedParseResult() 对比

对比项 parse() getFormattedParseResult()
返回字段数 7 个 2 个
返回类型 Map<String, dynamic> FormatPhoneResult?
错误处理 抛出异常 返回 null
格式选择 返回所有格式 只返回请求的格式
有效性判断 需要 try-catch null 检查即可
适用场景 需要完整元数据 只需格式化结果+验证

十四、parse() 在实际业务中的应用场景

14.1 号码验证

Future<bool> isPhoneNumberValid(
  String phone, String region
) async {
  try {
    await _plugin.parse(phone, region: region);
    return true;  // parse 成功 → 号码有效
  } catch (e) {
    return false;  // parse 失败 → 号码无效
  }
}

14.2 号码标准化存储

Future<String?> normalizeToE164(
  String phone, String region
) async {
  try {
    final res = await _plugin.parse(
      phone, region: region
    );
    return res['e164'] as String?;
  } catch (e) {
    return null;
  }
}

// 使用示例
// '131 2345 6789' → '+8613123456789'
// '(201) 555-0123' → '+12015550123'
// '07400 123456' → '+447400123456'

14.3 号码类型检测

Future<String> detectNumberType(
  String phone, String region
) async {
  try {
    final res = await _plugin.parse(
      phone, region: region
    );
    return res['type'] as String? ?? 'unknown';
  } catch (e) {
    return 'invalid';
  }
}

// 使用示例
// '+8613123456789' → 'mobile'
// '+861012345678' → 'fixedLine'
// '+12015550123' → 'fixedOrMobile'

14.4 国家自动识别

Future<String?> detectCountry(String phone) async {
  try {
    final res = await _plugin.parse(phone);
    return res['region_code'] as String?;
  } catch (e) {
    return null;
  }
}

// 使用示例
// '+8613123456789' → 'CN'
// '+12015550123' → 'US'
// '+447400123456' → 'GB'

14.5 显示格式转换

Future<Map<String, String>> getAllFormats(
  String phone, String region
) async {
  final res = await _plugin.parse(
    phone, region: region
  );
  return {
    'e164': res['e164'] ?? '',
    'international': res['international'] ?? '',
    'national': res['national'] ?? '',
  };
}

// 输入: '+8613123456789'
// 输出:
// {
//   'e164': '+8613123456789',
//   'international': '+86 131 2345 6789',
//   'national': '131 2345 6789',
// }

十五、与 Android/iOS 平台 parse() 实现的对比

15.1 三平台技术路线

平台 解析引擎 语言 数据来源
Android Google libphonenumber Java ITU 元数据文件
iOS PhoneNumberKit Swift 内置元数据
鸿蒙 PhoneNumberUtil.ets ArkTS 手动维护 57 国数据

15.2 解析精度对比

对比项 Android/iOS 鸿蒙
支持国家数 240+ 57
号码类型 mobile/fixedLine/voip/pager/… mobile/fixedLine/fixedOrMobile/unknown
正则验证 完整正则匹配 长度 + 首位字符判断
区号冲突处理 完善的优先级规则 forEach 最后匹配
nationalPrefix 完整的前缀规则 简化的前缀去除

15.3 返回字段对比

字段 Android iOS 鸿蒙
type
e164
international
national
country_code
region_code
national_number

虽然返回字段完全一致,但鸿蒙平台的解析精度受限于手动维护的 57 国数据。对于这 57 个国家,parse() 的结果与 Android/iOS 基本一致。

15.4 鸿蒙平台的设计取舍

取舍 选择 原因
数据量 57 国(非 240+) 覆盖 95%+ 的实际使用场景
类型判定 简化规则 避免维护复杂的正则表达式
格式化 10 国专用 + 通用 主要国家精确格式化
验证 长度验证 简单可靠,避免误判

十六、parse() 的性能特征

16.1 执行时间分析

parse() 执行时间分解:

Dart 侧:
  参数封装 + invokeMapMethod 调用    ~0.1ms
  StandardMessageCodec 编码          ~0.05ms
  BinaryMessenger 传输               ~0.5ms
  StandardMessageCodec 解码          ~0.05ms
  总计 Dart 侧:                      ~0.7ms

ArkTS 侧:
  参数提取                           ~0.01ms
  parse() 解析                       ~0.05ms
  isValidNumber() 验证               ~0.01ms
  getNumberType() 类型判定           ~0.02ms
  formatE164()                       ~0.01ms
  formatInternational()              ~0.02ms
  formatNational()                   ~0.03ms
  Map → Record 转换                  ~0.01ms
  总计 ArkTS 侧:                     ~0.16ms

总计: ~0.86ms(单次调用)

16.2 与 format() 的性能对比

方法 总耗时 ArkTS 侧耗时 说明
format() ~0.8ms ~0.1ms 只格式化,不解析
parse() ~0.9ms ~0.16ms 解析 + 验证 + 3 种格式化
formatNumberSync() ~0.05ms 0ms 纯 Dart,无通信

16.3 性能优化建议

场景 建议 原因
实时输入格式化 使用 formatNumberSync() 同步,< 0.1ms
单次号码验证 使用 parse() 一次调用获取所有信息
批量号码处理 避免循环调用 parse() 每次 ~0.9ms,100 个号码 ~90ms
只需格式化结果 使用 getFormattedParseResult() 封装了错误处理

总结

本文深入分析了 parse() 号码解析与元数据提取的完整实现。关键要点回顾:

  1. parse() 从电话号码字符串中提取 7 个结构化字段:type、e164、international、national、country_code、region_code、national_number
  2. 解析分两条路径:国际号码(以 + 开头)通过从长到短匹配区号,国内号码 需要 defaultRegion 参数并去掉 nationalPrefix
  3. 号码有效性通过 isValidNumber() 验证,规则是国内号码长度在 7-15 位 之间
  4. 号码类型通过 getNumberType() 判定,10 个主要国家有 专用判定规则,其他国家使用默认规则
  5. getFormattedParseResult()parse() 的上层封装,返回 null 表示号码无效,简化了错误处理
  6. 鸿蒙平台的 parse() 返回字段与 Android/iOS 完全一致,但解析精度受限于 57 国数据

下一篇我们将深入分析 getNumberType() 号码类型检测的完整实现,详解各国手机号与固话号码的判定逻辑。

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


相关资源:

Logo

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

更多推荐