【参赛心得】从“碰一碰”到“服务流转”:HarmonyOS创新赛金奖作品“智游文博”全流程复盘!

全文目录:

      • 开篇语
      • 摘要
      • 第一章:序章:灵感的火花与赛道的抉择
        • [1.1 参赛初心:当"开发者"遇上"博物馆迷"](#1.1 参赛初心:当“开发者”遇上“博物馆迷”)
        • [1.2 传统导览的"三大痛点"](#1.2 传统导览的“三大痛点”)
        • [1.3 "智游文博"的诞生:一个"碰一碰"的构想](#1.3 “智游文博”的诞生:一个“碰一碰”的构想)
        • [1.4 我们的技术蓝图与团队分工](#1.4 我们的技术蓝图与团队分工)
      • [第二章:万物互联的"第一触点":NFC 近场通信深度实践](#第二章:万物互联的“第一触点”:NFC 近场通信深度实践)
        • [2.1 为什么是 NFC?------ 我们的技术选型思考](#2.1 为什么是 NFC?—— 我们的技术选型思考)
        • [2.2 HarmonyOS NFC 基础:从配置到权限](#2.2 HarmonyOS NFC 基础:从配置到权限)
        • [2.3 实战:NFC 标签的"初始化"------ 写入展品 ID](#2.3 实战:NFC 标签的“初始化”—— 写入展品 ID)
        • [2.4 实战:捕获 `TAG_DISCOVERED` 并解析数据](#2.4 实战:捕获 TAG_DISCOVERED 并解析数据)
        • [2.5 踩坑与调试:NFC 的"灵"与"不灵"](#2.5 踩坑与调试:NFC 的“灵”与“不灵”)
      • 第三章:体验的飞从"打开App"到"服务直达"(元服务篇)
        • [3.1 获奖点(一):打破"App孤岛",实现"即碰即看"](#3.1 获奖点(一):打破“App孤岛”,实现“即碰即看”)
        • [3.2 元服务 (Widget) 架构设计](#3.2 元服务 (Widget) 架构设计)
        • [3.3 核心实现:NFC 如何拉起"服务卡片"?](#3.3 核心实现:NFC 如何拉起“服务卡片”?)
      • [第四章:服务的流转:AppLinking 的"最后一公里"](#第四章:服务的流转:AppLinking 的“最后一公里”)
        • [4.1 获奖点(二):从"卡片"到"应用"的丝滑衔接](#4.1 获奖点(二):从“卡片”到“应用”的丝滑衔接)
        • [4.2 AppLinking 详解:它解决了什么?](#4.2 AppLinking 详解:它解决了什么?)
        • [4.3 实战:配置 AppLinking 链接](#4.3 实战:配置 AppLinking 链接)
        • [4.4 实战:卡片按钮的终极形态](#4.4 实战:卡片按钮的终极形态)
        • [4.5 实战:主应用 (EntryAbility) 接收与解析 AppLinking](#4.5 实战:主应用 (EntryAbility) 接收与解析 AppLinking)
      • [第五章:赛前冲刺:APMS 性能调优实战](#第五章:赛前冲刺:APMS 性能调优实战)
        • [5.1 遭遇瓶颈:评委面前,卡顿是"原罪"](#5.1 遭遇瓶颈:评委面前,卡顿是“原罪”)
        • [5.2 引入"鸿蒙开放能力":APMS](#5.2 引入“鸿蒙开放能力”:APMS)
        • [5.3 案例分析(一):定位"卡片加载慢" (ANR)](#5.3 案例分析(一):定位“卡片加载慢” (ANR))
        • [5.4 案例分析(二):定位"主应用冷启动慢"](#5.4 案例分析(二):定位“主应用冷启动慢”)
      • 第六章:总结与致谢:星途探索,永不止步
        • [6.1 我们的"获奖密码"复盘](#6.1 我们的“获奖密码”复盘)
        • [6.2 参赛的最大收获:从"App开发者"到"场景设计师"](#6.2 参赛的最大收获:从“App开发者”到“场景设计师”)
        • [6.3 感恩与致谢](#6.3 感恩与致谢)
        • [6.4 展望未来](#6.4 展望未来)
      • 文末

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

摘要

本文以第一人称视角,完整复盘了 HarmonyOS 创新赛金奖(设定)作品------"智游文博"智慧博物馆导览应用的从 0 到 1 全流程。作为一篇以技术分享为主、故事性为辅的参赛心得,本文从赛道选题的痛点分析出发,详细阐述了我们团队如何摒弃传统的二维码或蓝牙方案,转而利用 HarmonyOS 的"原子化能力"构建全新的导览体验。本文的核心技术分享聚焦于四大关键能力:NFC 近场通信 (实现"碰一碰"的无感交互)、元服务 (实现"即碰即看"的轻量级服务卡片)、AppLinking (实现从卡片到主应用的无缝参数流转)以及 APMS(在赛前冲刺阶段进行性能分析与调优)。文章包含了大量的核心代码示例、配置详情以及我们"踩过"的坑,旨在为其他参赛者和开发者提供一套可复现的、围绕 HarmonyOS 新特性构建创新应用的最佳实践。

关键词: 参赛心得、 HarmonyOS 创新赛、 NFC、元服务、AppLinking、APMS、ArkTS等

第一章:序章:灵感的火花与赛道的抉择

1.1 参赛初心:当"开发者"遇上"博物馆迷"

2024 年末,当 HarmonyOS 创新赛的号角吹响时,我们团队正沉浸在 ArkTS 和声明式 UI 的学习热情中。我们既是开发者,也是一群博物馆爱好者。我们热爱历史的厚重,却也时常为当下博物馆的导览体验感到"抓狂"。

"星光"为引,鸿蒙聚能。这次大赛不仅仅是技术的比拼,更是寻找"用鸿蒙技术解决实际问题"的绝佳舞台。我们当即一拍即合:参赛!

1.2 传统导览的"三大痛点"

我们的灵感,源于一次次在博物馆中的"糟糕体验":

  1. "扫码"的割裂感:走到一个展品前,掏出手机,解锁,打开微信/App,对焦那个反光的、贴在角落的二维码,等待加载... 这一套流程下来,与文物的"精神交流"早已被彻底打断。
  2. "租导览器"的笨重:租用实体导览器,不仅需要押金,设备老旧、音质差,而且无法提供图文、视频等多媒体的沉浸式体验。
  3. "蓝牙 Beacon"的不精准:部分博物馆尝试过蓝牙 iBeacon 方案,但信号"漂移"问题严重,常常是人站在 A 展品前,手机却在推送 B 展品的信息,体验极差。

这些痛点,让我们看到了一个明确的突破口:我们能否创造一种"无感"的、"即时"的、"精准"的导览体验?

1.3 "智游文博"的诞生:一个"碰一碰"的构想

我们把目光投向了 HarmonyOS。如果说其他系统是在"App"的围墙花园里做优化,那么鸿蒙从诞生之初,就在思考如何"拆掉围墙",让服务"流"起来。

我们的"Aha!"时刻不期而至:

  • 如果,我们用 NFC 标签代替二维码呢?
  • 如果,用户手机"碰一碰"标签,不是打开一个笨重的 App,而是"秒出"一张轻盈的服务卡片呢?
  • 如果,用户在卡片上意犹未尽,点击"详情",又能"无缝"流入 App 的深度体验(如 3D 模型、AR 互动)呢?

这个 "碰一碰(NFC)-> 秒出卡片(元服务)-> 详情流转(AppLinking)" 的构想,让我们所有人兴奋不已。它几乎完美契合了 HarmonyOS"原子化服务"和"无缝流转"的核心理念。这不仅是一个参赛作品,更是一个真正能提升用户体验的解决方案。

1.4 我们的技术蓝图与团队分工

"智游文博 (Smart Museum Tour)"项目正式立项。我们的目标非常明确:打造基于 HarmonyOS 近场能力的极致导览体验。

团队分工如下:

  • 我(组长/架构):负责整体架构设计,主攻 NFC 与元服务的技术预研和实现。
  • 小A (ArkUI 专家):负责主 App 和元服务卡片的 UI/UX 设计与实现,确保多端适配。
  • 小B (后端/云开发):负责(模拟的)博物馆展品数据库,以及 AppLinking、APMS 等开放能力的后台配置与联调。

我们的征途,就从最核心的"第一触点"------ NFC 开始。

第二章:万物互联的"第一触点":NFC 近场通信深度实践

这是我们的第一个技术难关,也是我们方案的基石。如果"碰一碰"的体验做不好,后续的一切都无从谈起。

2.1 为什么是 NFC?------ 我们的技术选型思考

在答辩中,评委一定会问:为什么是 NFC,而不是更廉价的二维码?

我们的答案是:体验的"质变"

对比维度 二维码 (QR Code) 蓝牙 (Beacon) NFC
交互步骤 拿出手机 -> 解锁 -> 打开 App -> 扫码 -> 等待 拿出手机 -> 解锁 -> 打开 App -> 等待推送 拿出手机 -> 碰一碰 -> 服务直达
精准度 依赖对焦和光线 易受干扰,范围漂移 极高(厘米级),一对一
速度 慢 (2-5 秒) 不稳定 极快 (< 0.5 秒)
体验感 割裂,有操作负担 被动,易打扰 无感,主动,有"魔法感"

NFC 带来的"主动"与"即时"的魔法感,是二维码永远无法比拟的。我们坚信,这是未来场景交互的方向。

2.2 HarmonyOS NFC 基础:从配置到权限

要在 HarmonyOS 上玩转 NFC,首先必须搞定配置。

步骤 1:声明权限

entry/src/main/module.json5 文件中,我们必须声明 NFC 相关的权限:

json 复制代码
{
  "module": {
    // ...
    "requestPermissions": [
      {
        // 读写 NFC 标签的权限
        "name": "ohos.permission.NFC_TAG",
        "reason": "App needs this permission to read exhibit info from NFC tags.",
        "usedScene": {
          "ability": [".EntryAbility"],
          "when": "inuse"
        }
      }
      // 如果你还需要卡模拟(HCE),则需要下面这个
      // {
      //   "name": "ohos.permission.NFC_CARD_EMULATION",
      //   "reason": "...",
      //   "usedScene": { ... }
      // }
    ]
  }
}

步骤 2:配置 TechIDs关键)

NFC 标签有很多种技术标准(如 Ndef, IsoDep, NfcA/B/F/V 等)。我们必须告诉系统,我们的应用关心哪些类型的标签。这同样在 module.json5 中配置。

json 复制代码
{
  "module": {
    // ...
    "metadata": {
      "techList": [
        // 我们项目主要使用 NDEF 格式
        "ohos.nfc.tech.Ndef",
        // 同时兼容常见的 Type A 和 Type B 标签
        "ohos.nfc.tech.NfcA",
        "ohos.nfc.tech.NfcB"
      ]
    }
  }
}
2.3 实战:NFC 标签的"初始化"------ 写入展品 ID

在博物馆"布展"时,我们需要先给每个展品旁的空白 NFC 标签写入信息。我们开发了一个内部管理功能来实现这一点。

typescript 复制代码
// viewmodel/NfcWriterViewModel.ts
import nfc from '@ohos.nfc.tag';
import { BusinessError } from '@ohos.base';

class NfcWriterViewModel {
  // 假设从 UI 传入了要写入的展品 ID
  public async writeNfcTag(exhibitId: string, tagSession: nfc.TagSession) {
    if (!tagSession) {
      console.error('NFC TagSession is invalid.');
      return;
    }

    try {
      // 1. 我们使用 NDEF 格式,NDEF 是 NFC 数据交换的标准格式
      let ndefTag = nfc.getNdef(tagSession);
      if (!ndefTag) {
        console.error('This tag does not support NDEF.');
        return;
      }

      // 2. 连接标签
      await ndefTag.connect();

      // 3. 创建一个 NDEF 记录 (Record)
      // 我们选择使用 URI 类型,这是拉起应用或卡片的最高效方式
      // 格式:[我们的自定义协议]://[业务路径]?[参数]
      const uriPayload = `smartmuseum://exhibit?id=${exhibitId}`;
      let ndefRecord = nfc.makeUriRecord(uriPayload, nfc.UriIdentifierType.RECOMMEND);

      // 4. 将记录封装成 NDEF 消息 (Message)
      let ndefMessage = nfc.createNdefMessage([ndefRecord]);

      // 5. 写入消息
      await ndefTag.writeNdefMessage(ndefMessage);

      console.log(`Successfully wrote exhibitId ${exhibitId} to NFC tag.`);
      // 别忘了关闭连接
      await ndefTag.close();

    } catch (error) {
      const e = error as BusinessError;
      console.error(`Failed to write NFC tag. Code: ${e.code}, Msg: ${e.message}`);
    }
  }
}

关键设计 :我们没有写入"展品名称"或"介绍"等大数据,而是只写入了一个小巧的、指向性的 URIsmartmuseum://exhibit?id=123。这极大提高了写入速度和读取效率。

2.4 实战:捕获 TAG_DISCOVERED 并解析数据

当用户手机"碰"到这个标签时,系统会发出一个 Want,我们的应用需要捕获它并解析。

这部分逻辑通常写在 EntryAbilityonNewWant 生命周期中,因为此时 App 可能已经启动。

typescript 复制代码
// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import nfc from '@ohos.nfc.tag';
import { BusinessError } from '@ohos.base';

export default class EntryAbility extends UIAbility {
  // ... (onCreate 等)

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.log('onNewWant received.');
    // 检查是不是 NFC 相关的 Want
    if (want.action === 'ohos.nfc.action.TAG_DISCOVERED') {
      console.log('NFC Tag Discovered!');
      this.processNfcWant(want);
    }
  }

  private async processNfcWant(want: Want) {
    try {
      // 1. 从 Want 中获取 TagSession 对象
      let tagSession: nfc.TagSession = want.parameters[nfc.param.TAG_INFO] as nfc.TagSession;
      if (!tagSession) {
        console.error('Failed to get TagSession from want.');
        return;
      }

      // 2. 获取 NDEF 标签
      let ndefTag = nfc.getNdef(tagSession);
      if (!ndefTag) {
        console.error('Tag does not support NDEF.');
        return;
      }

      // 3. 连接并读取 NDEF 消息
      await ndefTag.connect();
      let ndefMessage = await ndefTag.readNdefMessage();
      await ndefTag.close();

      if (!ndefMessage || ndefMessage.ndefRecords.length === 0) {
        console.error('NdefMessage is empty.');
        return;
      }

      // 4. 解析 Record,获取我们写入的 URI
      const firstRecord = ndefMessage.ndefRecords[0];
      const payload = firstRecord.payload;
      // 将 payload (Uint8Array) 转换为字符串
      const uriString = this.uint8ArrayToString(payload);
      
      // 注意:UriRecord 的 payload 会包含一个表示类型的 prefix,需要去掉
      // 例如,payload[0] 可能代表 "https://" 或 "http://"
      // 在我们的自定义 URI (makeUriRecord) 中,它通常会有一个前缀字节
      const parsedUri = this.parseUriFromPayload(payload); // (需要自己实现解析)
      
      console.log(`Parsed URI from NFC: ${parsedUri}`);
      
      // 5. 关键:拿到 URI,但先不急着跳转 App
      // 我们的方案是:拉起元服务卡片!
      // this.showExhibitCard(parsedUri); // (详见下一章)
      
    } catch (error) {
      const e = error as BusinessError;
      console.error(`Error processing NFC tag. Code: ${e.code}, Msg: ${e.message}`);
    }
  }
  
  // (辅助函数:Uint8Array 转 String)
  private uint8ArrayToString(array: Uint8Array): string {
    // ... 实现
  }}
  
  // (辅助函数:从 NDEF Payload 解析 URI)
  private parseUriFromPayload(payload: Uint8Array): string {    // NDEF UriRecord 的第一个字节是
    // 0x00 表示无前缀
    // 0x01 表示 "http://www." 等
    // 我们的自定义 URI (makeUriRecord) 通常是 0x00
    if (payload[0] === 0x00) {
      return this.uint8ArrayToString(payload.slice(1));
    }
    // ... 处理其他前缀
    return this.uint8ArrayToString(payload); // 简化处理
  }
}
2.5 踩坑与调试:NFC 的"灵"与"不灵"

这是我们参赛过程中耗时最长的阶段之一:

  • 坑 1:机容性。我们发现,不同型号的华为手机,NFC 感应区域(背部)不完全一致。我们必须在 demo 视频里明确演示"正确"的触碰姿势。
  • 坑 2:onNewWant 不触发 。调试了很久,发现 module.json5 里的 techList 没配对。我们买的标签是 NfcA + Ndef,但一开始只配了 Ndef,导致系统在底层就给过滤了。
  • 坑 3:读写冲突 。在开发"写入"功能时,如果写入失败,标签可能会"锁死"或进入一个奇怪的状态,导致后续读取也失败。解决方案 :每次 connect() 之后,必须保证 close() 被调用,即使在 catch 块中也要处理。

攻克了 NFC,我们就打通了"人"与"物"的连接。下一步,是优化这个连接的"反馈"。

第三章:体验的飞从"打开App"到"服务直达"(元服务篇)

这是我们方案的第一个**"关键获奖点"**。传统方案(包括微信小程序)在NFC"碰一碰"后,无论如何都要加载一个"页面"。而我们要做的,是"卡片"。

3.1 获奖点(一):打破"App孤岛",实现"即碰即看"

我们的核心理念是:用户 90% 的需求只是想"看一眼"展品简介。为了这 90% 的需求,去加载一个 10% 才会用到的完整 App(带 3D 模型、AR 功能)是极大的性能浪费。

务(服务卡片) 是完美的解决方案。它轻量、免安装、可数据驱动,是承载"惊鸿一瞥"信息的最佳形态。

3.2 元服务 (Widget) 架构设计

我们创建了一个 WidgetExtensionAbility,专门用于处理和展示展品信息的卡片。

步骤 1:创建 WidgetExtensionAbility

entry 模块上右键 -> New -> Ability -> WidgetExtensionAbility。我们将其命名为 ExhibitWidgetAbility

步骤 2:配置 form_config.json

resources/base/profile/form_config.json 中,我们定义卡片的属性:

json 复制代码
{
  "forms": [
    {
      "name": "ExhibitWidget", // 卡片名称
      "description": "Exhibit Information Card",
      "src": "./ets/exhibitwidget/widgets/ExhibitWidgetCard.ets", // 卡片 UI 文件
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "updateDuration": 0, // 0 表示不自动更新,由我们手动触发
      "defaultDimension": "2x4", // 我们设计了一个 2x4 的卡片
      "supportDimensions": ["2x4"]
    }
  ]
}

步骤 3:编写卡片 UI

卡片 UI (ExhibitWidgetCard.ts) 必须简洁明了。

typescript 复制代码
// ets/exhibitwidget/widgets/ExhibitWidgetCard.ets

// 接收从 Ability 传来的数据
interface ExhibitData {
  title: string;
  dynasty: string;
  imageUrl: string;
}

@Entry
@Component
struct ExhibitWidgetCard {
  @Prop data: ExhibitData = {
    title: '加载中...',
    dynasty: '...',
    imageUrl: ''
  };

  build() {
    Column() {
      if (this.data.imageUrl) {
        Image(this.data.imageUrl) // 异步加载图片
          .width('100%').height(120).objectFit(ImageFit.Cover)
      } else {
        // ... 显示占位图
      }
      Text(this.data.title)
        .fontSize(16).fontWeight(FontWeight.Bold).margin(5)
      Text(this.data.dynasty)
        .fontSize(14).fontColor(Color.Gray)
      
      // ...
      
      // 关键:"查看详情"按钮,为下一章 AppLinking 做准备
      Button('查看查看 3D 详情')
        .width('90%').margin({ top: 10 })
        .onClick(() => {
          // 点击
          this.onDetailClick();
        })
    }
    .padding(10)
    .backgroundColor('#FFFFFF')
  }
  
  onDetailClick() {
    // ... (详见下一章)
  }
}
3.3 核心实现:NFC 如何拉起"服务卡片"?

这是我们方案中最具技巧性的部分。NFC 默认是拉起 App (EntryAbility) 的,我们如何让它"转而"拉起卡片呢?

我们没有(也不能)直接拉起卡片。

我们的流程是:

  1. NFC 依然拉起 EntryAbility (或一个专门的后台 ServiceAbility)。
  2. EntryAbilityonNewWant 中解析出展品 ID。
  3. EntryAbility 立即请求系统并显示一个临时卡片
  4. EntryAbility 获取展品数据,并更新这个卡片。
typescript 复制代码
// EntryAbility.ts (续)
import formManager from '@ohos.app.form.formManager';

// ... (在 processNfcWant 中)
private async processNfcWant(want: Want) {
  try {
    // ... (解析 NFC 数据的代码) ...
    const parsedUri = this.parseUriFromPayload(payload); // 假设得到 "smartmuseum://exhibit?id=123"
    const exhibitId = this.getIdFromUri(parsedUri); // 拿到 "123"

    if (exhibitId) {
      console.log(`Exhibit ID ${exhibitId} found. Requesting widget...`);
      // 关键步骤:请求系统显示我们的卡片卡片
      this.requestShowWidget(exhibitId);
    }
  } catch (e) {
    // ...
  }
}

// 请求创建并显示一个临时卡片
private async requestShowWidget(exhibitId: string) {
  try {
    // 1. 准备卡片数据,先把 ID 传过去
    const formData = {
      exhibitId: exhibitId,
      title: '正在加载...', // 初始骨架屏数据
      dynasty: '...',
      imageUrl: ''
    };
    
    // 2. 创建一个 Want,指向我们的 WidgetExtensionAbility
    const want = {
      bundleName: this.context.bundleName,
      abilityName: 'ExhibitWidgetAbility', // 我们的卡片 Ability
      parameters: {
        [formManager.FormParam.IDENTITY_KEY]: `ExhibitWidget_${exhibitId}`, // 唯一的卡片 ID
        [formManager.FormParam.NAME_KEY]: 'ExhibitWidget', // form_config.json 中的 name
        [formManager.FormParam.DIMENSION_KEY]: formManager.FormDimension.DIMENSION_2_4, // 2x4 尺寸
        [formManager.FormParam.TEMPORARY_KEY]: true, // **关键:true 表示这是一个临时的、用完即走的卡片**
        'formData': JSON.stringify(formData) // 携带初始数据
      }
    };
    
    // 3. 向系统发起请求
    await formManager.requestForm(want.parameters[formManager.FormParam.IDENTITY_KEY], want);
    console.log('Request form successful.');
    
    // 4. (异步) 获取真实数据并更新卡片
    this.updateWidgetData(exhibitId, want.parameters[formManager.FormParam.IDENTITY_KEY]);
    
  } catch (error) {
    const e = error as BusinessError;
    console.erroror(`Failed to request form. Code: ${e.code}, Msg: ${e.message}`);
  }
}

// 异步获取数据并更新卡片rivate async updateWidgetData(exhibitId: string, formId: string) {
  try {
    // 模拟从云端获取数据
    const exhibitData = await this.fetchExhibitDetails(exhibitId);
    
    const formData = {
      exhibitId: exhibitId,
      title: exhibitData.title,
      dynasty: exhibitData.dynasty,
      imageUrl: exhibitData.imageUrl
    };
    
    // 使用 formProvider 更新卡片
    await formProvider.updateForm(formId, formBindingData.createFormBindingData(formData));
    console.log(`Widget ${formId} updated with real data.`);
    
  } catch (e) {
    // ...
  }
}

// 模拟 API
private async fetchExhibitDetails(id: string): Promise<any> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        title: '清明上河图 (局部)',
        dynasty: '北宋',
        imageUrl: '...' // 真实的图片 URL
      });
    }, 500); // 模拟网络延迟
  });
}

通过这套"NFC -> EntryAbility (后台处理) -> formManager.requestForm (临时卡片) -> formProvider.updateForm异步更新) "的组合拳,我们完美实现了"即碰即看"的丝滑体验。用户在碰触后,桌面会立刻弹出一个"加载中"的卡片,0.5 秒后数据刷新,体验远胜于等待 App 启动。

第四章:服务的流转:AppLinking 的"最后一公里"

卡片解决了"浅层需求",但对于想深入了解(如查看 3D 模型、AR 互动)的用户,我们必须提供一条"无缝"的路径流入主 App。

4.1 获奖点(二):从"卡片"到"应用"的丝滑衔接

如果用户在卡片上点击"查看详情",我们拉起了 App 首页,让用户自己去列表里找这个展品,那体验无疑是灾难性的。

我们必须做到:卡片 -> 拉起 App -> 精准进入该展品的详情页

这就是 AppLinking 的用武之地。它是一个跨平台的深度链接服务,能帮我们生成一个链接,无论用户是否安装了 App,都能被智能地引导。

4.2 AppLinking 详解:它解决了什么?

AppLinking 解决了"服务流转"的"寻址"问题。它提供了一个统一的 URL,后台会自动处理:

  • 已安装 App:直接拉起 App,并传递参数。
  • 未安装 App :引导至 AppGallery 下载,下载安装后首次启动时,依然能传递参数。
4.3 实战:配置 AppLinking 链接

这是小B同学(负责后台)的工作,但作为参赛者都必须了解。

  1. 开通服务:在 AppGallery Connect (AGC) 中,为我们的"智游文博"项目开通"App Linking"服务。

  2. 配置 URL 前缀 :AGC 会分配给我们一个短链域名,例如 smartmuseum.agconnect.link

  3. 创建 AppLinking

    • :系统自动生成(或自定义)。
    • 深度链接 (Deep Link) :这是关键。我们指定一个 App 能识别的 URI,例如:smartmuseum://details
    • 安卓/鸿蒙参数 :设置我们的包名 com.example.smartmuseum,以及启动的 Activity(在鸿蒙上即EntryAbility`)。
    • 回退行为:如果未安装,跳转到我们的 AppGallery 详情页。
  4. 最终形态 :我们创建了一个链接,它会根据参数动态变化,例如:
    https://smartmuseum.agconnect.link/open?exhibitId=123

    这个链接,会最终被解析为 smartmuseum://details?exhibitId=123 并传递给我们的 App。

4.4 实战:卡片按钮的终极形态

现在,我们回到 ExhibitWidgetCard.ets,给那个"查看详情"按钮赋予灵魂。

typescript 复制代码
// ets/exhibitwidget/widgets/ExhibitWidgetCard.ets
import router from '@ohos.router';

// ...
@Component
struct ExhibitWidgetCard {
  @Prop data: ExhibitData;
  
  // ...
  
  onDetailClick() {
    // 1. 获取当前卡片的数据,我们之前存入了 exhibitId
    const exhibitId = this.data.exhibitId; // 假设 data 中有 id
    if (!exhibitId) return;

    // 2. 构建我们的 AppLinking 链接
    // 这个域名是在 AGC 上配置的
    const appLinkingUrl = `https://smartmuseum.agconnect.link/open?exhibitId=${exhibitId}`;

    // 3. 使用 router.pushUrl 来拉起这个链接
    // 系统会自动识别这个 AppLinking 链接,并将其
    // 路由到我们的主 App (EntryAbility)
    try {
      router.pushUrl({
        url: appLinkingUrl
      }, (err) => {
        if (err) {
          console.error(`Failed to push AppLinking URL. Code: ${err.code}, Msg: ${err.message}`);
        }
      });
    } catch (e) {
      console.error('Exception in router.pushUrl', e);
    }
  }
}

注意 :卡片是一个独立的 FormExtension 进程,它使用 router.pushUrl 时,会向系统发出一个 Want。系统中的 AppLinking 服务会拦截这个 URL,解析它,然后构建一个新的 Want 来拉起我们的 EntryAbility

4.5 实战:主应用 (EntryAbility) 接收与解析 AppLinking

最后一步。用户点击卡片按钮后,EntryAbility 会被再次唤醒,我们又回到了 onNewWant

typescript 复制代码
// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import router from '@ohos.router';

export default class EntryAbility extends UIAbility {
  // ...

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.log('onNewWant received.');
    
    // 检查是不是 NFC 相关的 Want
    if (want.action === 'ohos.nfc.action.TAG_DISCOVERED') {
      console.log('NFC Tag Discovered!');
      this.processNfcWant(want);
      return;
    }

    // 检查是不是 AppLinking 相关的 Want
    // AppLinking 传递过来的 Want,其 URI 会是我们在 AGC 配置的 Deep Link
    if (want.uri && want.uri.startsWith('smartmuseum://details')) {
      console.log(`AppLinking URI detected: ${want.uri}`);
      this.processAppLinkingWant(want);
      return;
    }
    
    // ... 其他 Want 处理
  }

  private processAppLinkingWant(want: Want) {
    // want.uri 可能是 "smartmuseum://details?exhibitId=123"
    // 我们需要从中解析出参数
    const exhibitId = this.getQueryParam(want.uri, 'exhibitId');
    
    if (exhibitId) {
      console.log(`Exhibit ID ${exhibitId} from AppLinking.`);
      
      // 关键:我们不再显示卡片,而是直接跳转到 App 内部的详情页
      // 确保我们的 router 已经配置了 "pages/ExhibitDetail" 页面
      router.pushUrl({
        url: 'pages/ExhibitDetail',
        params: {
          id: exhibitId
        }
      }, (err) => {
        if (err) {
          console.error(`Router push to ExhibitDetail failed. Code: ${err.code}, Msg: ${err.message}`);
        }
      });
      
      // (可选)如果卡片是临时的,我们可以在这里把它关掉
      // this.dismissTemporaryCard(...);
    }
  }
  
  // 辅助函数:解析 URI 参数
  private getQueryParam(uri: string, key: string): string | null {
    try {
      const parts = uri.split('?');
      if (parts.length < 2) return null;
      const params = new URLSearchParams(parts[1]);
      return params.get(key);
    } catch (e) {
      return null;
    }
  }
}

至此,我们的核心获奖链路 "碰一碰(NFC)-> 秒出卡片(元服务)-> 详情(AppLinking)" 全部打通!这套组合拳,为评委们呈现了一个兼具"魔法感"和"实用性"的完美闭环。

第五章:赛前冲刺:APMS 性能调优实战

功能跑通了,但我们离"金奖"还差最后一步------性能

5.1 遭遇瓶颈:评委面前,卡顿是"原罪"

在赛前内测时,我们发现了两个致命问题:

  1. 卡片慢:NFC 碰触后,卡片有时要 2-3 秒才弹出,有时甚至会 ANR (应用无响应)。
  2. App 冷启动慢:从卡片点击"详情"拉起主 App 时,白屏时间过长,超过了 3 秒。

在创新赛这种"一分钟定胜负"的答辩上,任何一次卡顿都是"原罪"。我们必须解决它。

5.2 引入"鸿蒙开放能力":APMS

我们引入了华为提供的"APMS (应用性能管理服务)"。它就像一个随身的"性能医生",可以帮我们非侵入式地监控和分析应用的性能。

我们集成了 APMS SDK,并在 AGC 后台打开了"性能分析"。

5.3 案例分析(一):定位"卡片加载慢" (ANR)

APMS 的 ANR 监控很快上报了问题。

  • 问题定位 :通过 ANR 报告的堆栈,我们震惊震惊地发现,问题出在 EntryAbilityonNewWant 方法里。
  • 错误原因onNewWant行在主线程 。我们调用的 processNfcWant 方法中,包含了 ndefTag.connect()ndefTag.readNdefessage() 这两个 I/O 操作。当 NFC 标签接触不良或数据较大时,这两个方法可能会阻塞主线程超过 500 毫秒,引发 ANR!

解决方案:万物皆可异步!

我们必须将所有 I/O 操作移出主线程。

typescript 复制代码
// EntryAbility.ts
import { taskpool } from '@kit.PerformanceAnalysisKit'; // 引入任务池

// ...
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // ...
    if (want.action === 'ohos.nfc.action.TAG_DISCOVERED') {
      console.log('NFC Tag Discovered! Processing in background task.');
      
      // **优化:将耗时操作放入后台任务池执行**
      const nfcTask = new taskpool.Task(this.processNfcWant.bind(this), want);
      taskpool.execute(nfcTask);
    }
    // ...
  }

  // processNfcWant 保持 async 不变,它将在后台线程被执行
  private async processNfcWant(want: Want) {
    try {
      // ... (connect, read, requestShowWidget 等所有逻辑)
      // 注意:requestShowWidget 和 updateWidgetData 内部是异步的
      // 它们最终会回到主线程更新 UI,这是安全的
    } catch (e) {
      // ...
    }
  }

调优成果 :修改后,onNewWant 瞬间执行完毕。NFC 碰触后,App 主线程毫无压力,卡片弹出的 ANR 问题彻底解决。

5.4 案例分析(二):定位"主应用冷启动慢"

APMS 的"应用启动"分析报告了"冷启动耗时过长"。

  • 问题定位 :报告显示,EntryAbilityonCreate 方法耗时高达 1.5 秒。
  • 错误原因 :我们在 onCreate 里"好心"地做了太多初始化:初始化数据库、加载全部房间和展品列表、初始化 3D 渲染引擎、初始化 AR SDK...
  • 解决方案载(Lazy Initialization)onCreate 只做最轻量级的、必需的初始化。
typescript 复制代码
// EntryAbility.ts

// 模拟各种服务
const dbService = { init: () => console.log('DB init') };
const roomService = { init: () => console.log('Room init') };
const arEngine = { init: () => console.log('AR Engine init') };

export default class EntryAbility extends UIAbility {

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.log('onCreate startt.');
    
    // **优化前 (错误示范)**
    // dbService.init(); // 耗时
    // roomService.init(); //耗时
    // arEngine.init(); // 巨耗时
    
    // **优化后**
    // 1. 只做最必要的初始化
    dbService.init(); // 假设数据库必须先初始化
    
    // 2. 其他非紧急的服务,延迟加载
    this.lazyInitServices();
    
    console.log('onCreate end.');
  }
  
  private lazyInitServices() {
    // 使用 postTask 将任务推迟到启动阶段之后
    try {
      taskpool.execute(new taskpool.Task(() => {
        console.log('Lazy init task started...');
        roomService.init();
        arEngine.init();
        console.log('Lazy init task finished.');
      }));
    } catch (e) {
      console.error('Lazy init task failed', e);
    }
  }
  // ...
}

调优成果 :通过懒加载,onCreate 的耗时从 1.5 秒降到了 0.2 秒。配合 AppLinking 的精准跳转,用户从点击卡片到进入详情页的冷启动时间缩短到 1 秒内,体验大幅提升。

在最终答辩时,我们自豪地展示了 APMS 的"优化前后"对比图,这成为了评委给出"工程质量分"的重要依据。

第六章:总结与致谢:星途探索,永不止步

6.1 我们的"获奖密码"复盘

如今回看,"智游文博"能获得评委的青睐,我想我们的"获奖密码"可以总结为三点:

  1. 精准的场景切入:我们没有做"大而全"的应用,而是聚焦于"博物馆导览"这一个垂直场景的"核心痛点"。
  2. "鸿蒙原生思维:我们没有把鸿蒙当成另一个"安卓",而是从立项之初就思考如何利用其"原子化"和"流转"的特性。我们方案的核心,不是 App,而是"服务"。
  3. 技术链的完美闭环 :我们打通了 NFC(输入)-> 元服务(轻反馈)-> AppLinking(重反馈) 的全链路,并用 APMS() 保证了体验的极致。这是一个完整且优雅的鸿蒙解决方案。
6.2 参赛的最大收获:从"App开发者"到"场景设计师"

这次 HarmonyOS 创新赛,带给我们的不仅是奖项的荣誉,更是开发思维的彻底重塑。

我们不再仅仅思考"这个页面怎么画",而是开始思考"这个服务应该在何时、以何种形态(卡片/App/语音在哪个设备上被触发"。我们从一个"App 开发者",真正开始转变为一个"全场景服务的设计师"。

6.3 感恩与致谢

星光不负赶路人。感谢 CSDN 和华为搭建的"星光"平台,让我们有机会将奇思妙想付诸实践;感谢我的队友小 A 和小 B,是无数个深夜的联调和争论,才打磨出了"智游文博";更要感谢鸿蒙生态,是它提供的强大技术底座,让我们得以站在巨人的肩膀上,去构想下一个时代的交互。

6.4 展望未来

"智游文博"的故事还远未结束。未来,我们计划引入 AR 能力,当用户通过 AppLinking 进入详情页后,可以直接开启 AR 模式,让文物"活"在手机屏幕上。

我们的"星途探索"才刚刚开始。希望我们的这点参赛心得,能为后来者提供一点微光,激励更多开发者加入鸿蒙生态,共同用技术点亮全场景的未来!

... ...

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

... ...

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。

⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

相关推荐
鸿蒙小白龙4 小时前
OpenHarmony平台大语言模型本地推理:llama深度适配与部署技术详解
人工智能·语言模型·harmonyos·鸿蒙·鸿蒙系统·llama·open harmony
安卓开发者4 小时前
鸿蒙NEXT Wear Engine开发实战:手机侧应用如何调用穿戴设备能力
华为·智能手机·harmonyos
Damon小智5 小时前
仓颉 Markdown 解析库在 HarmonyOS 应用中的实践
华为·typescript·harmonyos·markdown·三方库
ZIM学编程6 小时前
把握鸿蒙生态红利:HarmonyOS 应用开发学习路径与实战课程推荐
学习·华为·harmonyos
安卓开发者1 天前
鸿蒙NEXT应用接入快捷栏:一键直达,提升用户体验
java·harmonyos·ux
HMS Core1 天前
消息推送策略:如何在营销与用户体验间找到最佳平衡点
华为·harmonyos·ux
Brianna Home1 天前
【案例实战】鸿蒙分布式调度:跨设备协同实战
华为·wpf·harmonyos
Bert丶seven1 天前
鸿蒙Harmony实战开发教学(No.4)-RichText组件基础到高阶介绍篇
华为·harmonyos·arkts·鸿蒙·鸿蒙系统·arkui·开发教程
鸿蒙小白龙1 天前
openharmony之分布式蓝牙实现多功能场景设备协同实战
分布式·harmonyos·鸿蒙·鸿蒙系统·open harmony