手机碰一碰 NFC 标签就弹出配置页、碰一碰配网 WiFi、碰一碰开门禁——NFC 是智能家居和 IoT 场景的关键入口。HarmonyOS NEXT 提供了完整的 NFC API:标签识别、NDEF 读写、前台分发优先注册。这篇把 NFC 近场通信的核心用法讲清楚。

NFC 通信概览

NFC(近场通信)的核心概念:

  • Tag(标签)——NFC 贴纸、NFC 卡片等被动设备
  • NDEF——NFC 数据交换格式,标准化的数据组织方式
  • 前台分发——APP 在前台时优先接收 NFC 意图
  • AAR——Android Application Record,碰一碰自动打开指定 APP
import { tag } from '@kit.ConnectivityKit'
import { ndef } from '@kit.ConnectivityKit'

权限配置

{
  "requestPermissions": [
    { "name": "ohos.permission.NFC_TAG" },
    { "name": "ohos.permission.NFC_ADAPTER" }
  ]
}

标签检测

标签靠近手机时,系统通过 Ability 的 onNewWant 分发 NFC 意图。

// EntryAbility.ets
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  let tagInfo: tag.TagInfo | null = null
  try {
    tagInfo = tag.getTagInfo(want)
  } catch (e) {
    return
  }
  if (tagInfo === null) return

  // 判断标签类型
  if (tag.isNdef(tagInfo)) {
    let ndefTag:ndef.NdefTag = ndef.getNdefTag(tagInfo)
    // 读取 NDEF 内容
  }
}

要点: getTagInfo 从 want 中提取标签信息,isNdef 判断是否为 NDEF 格式标签。非 NDEF 标签(如 MifareClassic)用对应的 getXXXTag 方法。

NDEF 读写

NDEF 是最常用的 NFC 数据格式——类似"JSON for NFC"。

读取 NDEF

let ndefTag: ndef.NdefTag = ndef.getNdefTag(tagInfo)
let ndefMessage: ndef.NdefMessage = ndefTag.getNdefMessage()
let records: ndef.NdefRecord[] = ndefMessage.getRecords()
for (let i: number = 0; i < records.length; i++) {
  let record: ndef.NdefRecord = records[i]
  let payload: string = record.payload as string
  let recordType: string = record.recordType as string
  // URI 类型: recordType = 'U'
  // 文本类型: recordType = 'T'
}

写入 NDEF

// 创建 NDEF 消息
let uriRecord: ndef.NdefRecord = ndef.createUriRecord('https://harmonyos.com')
let ndefMsg: ndef.NdefMessage = ndef.createNdefMessage([uriRecord])

// 写入标签
ndefTag.writeNdef(ndefMsg)

创建不同类型的 NDEF Record

// URI 记录
let uri: ndef.NdefRecord = ndef.createUriRecord('https://example.com')

// 文本记录
let text: ndef.NdefRecord = ndef.createTextRecord('zh', '你好HarmonyOS')

// MIME 记录
let mime: ndef.NdefRecord = ndef.createMimeRecord('application/json', '{"key":"value"}')

// AAR 记录(碰一碰打开APP)
let aar: ndef.NdefRecord = ndef.createAarRecord('com.example.myapp')

要点: AAR 记录让手机碰标签时自动打开指定包名的 APP,实现"碰一碰直连"。

前台分发

当多个 APP 都能处理 NFC 标签时,前台分发让当前 APP 优先接收。

import { tag } from '@kit.ConnectivityKit'

// 注册前台分发
let elementName: ElementName = {
  bundleName: 'com.example.myapp',
  abilityName: 'EntryAbility'
}
let techList: string[][] = [
  ['ndef', 'ndef-formatable']
]
tag.registerForegroundDispatch(elementName, techList)

// 取消前台分发
tag.unregisterForegroundDispatch(elementName)

要点: registerForegroundDispatch 在 Ability 的 onForeground 注册,onBackground 取消。techList 指定感兴趣的标签技术类型。
在这里插入图片描述

NFC 碰一碰场景模拟 Demo

由于 NFC 需要真机 + NFC 标签,这里用模拟方式展示完整交互流程。

interface NfcRecord {
  id: string
  type: string
  content: string
  time: string
}

@Entry
@Component
struct NfcSimDemo {
  @State nfcRecords: NfcRecord[] = []
  @State tagDetected: boolean = false
  @State currentTagType: string = ''
  @State ndefContent: string = ''
  @State writeInput: string = 'https://harmonyos.com'
  @State statusMsg: string = '将NFC标签靠近手机...'

  build() {
    Column({ space: 16 }) {
      Text('NFC 碰一碰模拟')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .width('100%')

      Column() {
        Column()
          .width(100)
          .height(100)
          .borderRadius(50)
          .backgroundColor(this.tagDetected ? '#E8F5E9' : '#F5F5F5')
          .border({ width: 3, color: this.tagDetected ? '#4CAF50' : '#E0E0E0', style: BorderStyle.Dashed })
        Text(this.tagDetected ? '标签已检测' : '等待NFC标签')
          .fontSize(14)
          .fontColor(this.tagDetected ? '#4CAF50' : '#999999')
          .margin({ top: 8 })
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
      .padding(16)

      Row({ space: 8 }) {
        Button('模拟标签靠近').onClick(() => {
          this.tagDetected = true
          this.currentTagType = 'NDEF'
          this.ndefContent = 'https://harmonyos.com'
          this.addRecord('检测', '发现NDEF标签')
          this.statusMsg = '标签已检测到(模拟)'
        })
        Button('模拟标签移开').onClick(() => {
          this.tagDetected = false
          this.ndefContent = ''
          this.statusMsg = '标签已移开'
          this.addRecord('移开', '标签离开')
        })
      }

      if (this.tagDetected) {
        Column({ space: 8 }) {
          Text('标签信息')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .width('100%')

          Row() {
            Text('类型:').fontSize(14).fontColor('#666666').width(60)
            Text(this.currentTagType).fontSize(14).fontWeight(FontWeight.Medium)
          }
          Row() {
            Text('NDEF:').fontSize(14).fontColor('#666666').width(60)
            Text(this.ndefContent || '(空)').fontSize(14).fontColor('#1a73e8')
          }

          TextInput({ text: this.writeInput, placeholder: '写入内容' })
            .onChange((value: string) => { this.writeInput = value })
          Row({ space: 8 }) {
            Button('写入NDEF').onClick(() => {
              this.ndefContent = this.writeInput
              this.addRecord('写入', this.writeInput)
            })
            Button('读取NDEF').onClick(() => {
              this.addRecord('读取', this.ndefContent || '(空)')
            })
            Button('清空').onClick(() => {
              this.ndefContent = ''
              this.addRecord('清空', 'NDEF已清除')
            })
          }
        }
        .width('100%')
        .padding(16)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
      }

      // 操作记录
      if (this.nfcRecords.length > 0) {
        Text('操作记录:')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width('100%')
        ForEach(this.nfcRecords.slice().reverse(), (record: NfcRecord) => {
          Row({ space: 8 }) {
            Text(record.time).fontSize(11).fontColor('#999999').width(60)
            Text(record.type).fontSize(12).fontWeight(FontWeight.Medium).fontColor('#1a73e8').width(50)
            Text(record.content).fontSize(12).fontColor('#333333').layoutWeight(1)
          }
          .padding(4)
        }, (record: NfcRecord) => record.id)
      }

      Text(this.statusMsg)
        .fontSize(12)
        .fontColor('#999999')
    }
    .width('100%')
    .padding(20)
  }

  private addRecord(type: string, content: string): void {
    this.nfcRecords.push({
      id: Date.now().toString(),
      type: type,
      content: content,
      time: new Date().toLocaleTimeString()
    })
  }
}

碰一碰配网实战

最常见的 IoT 场景——手机碰一碰设备上的 NFC 标签,自动跳转配网页面,输入 WiFi 密码后下发给设备。

// NDEF 标签中写入的内容
// URI: https://myapp.com/setup?device=ABC123
// AAR: com.example.iotapp

// APP 收到 NFC 意图后解析
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  let tagInfo: tag.TagInfo | null = tag.getTagInfo(want)
  if (tagInfo === null) return

  if (tag.isNdef(tagInfo)) {
    let ndefTag: ndef.NdefTag = ndef.getNdefTag(tagInfo)
    let msg: ndef.NdefMessage = ndefTag.getNdefMessage()
    let records: ndef.NdefRecord[] = msg.getRecords()
    // 解析 URI 中的设备 ID
    // 跳转到配网页面
    let uri: string = records[0].payload as string
    let deviceId: string = this.parseDeviceId(uri)
    router.pushUrl({ url: 'pages/WifiSetup', params: { deviceId: deviceId } })
  }
}

标签类型判断

不同标签支持不同的技术:

标签类型 判断方法 适用场景
NDEF tag.isNdef() 通用数据读写
MifareClassic tag.isMifareClassic() 门禁卡、公交卡
MifareUltralight tag.isMifareUltralight() NFC 贴纸
NfcA tag.isNfcA() ISO 14443-3A
NfcB tag.isNfcB() ISO 14443-3B
NfcF tag.isNfcF() FeliCa
NfcV tag.isNfcV() ISO 15693

要点: 先判断标签类型,再用对应的 API 操作。NDEF 是最通用的格式,优先使用。

踩坑清单

问题 原因 解决
getTagInfo 返回 null want 不是 NFC 意图 检查 intent 来源
isNdef 返回 false 标签不是 NDEF 格式 空白标签先格式化
writeNdef 报错 标签只读或容量不足 检查标签是否可写
前台分发不生效 未在 onForeground 注册 onForeground 注册、onBackground 取消
碰标签打开别的 APP 未注册前台分发 注册前台分发 + AAR 记录
NDEF 解析乱码 编码处理不对 URI 类型用 UTF-8,文本注意语言码
readNdef 返回空 标签无 NDEF 消息 先检查 NDEF 消息是否存在
标签检测延迟 系统轮询间隔 前台分发 + 注册 techList
多标签干扰 同时靠近多个标签 一次只靠近一个标签
createUriRecord 格式错 URI 不完整 必须含协议前缀 https://
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐