手机碰一碰 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 } })
  }
}

标签类型判断

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

标签类型判断方法适用场景
NDEFtag.isNdef()通用数据读写
MifareClassictag.isMifareClassic()门禁卡、公交卡
MifareUltralighttag.isMifareUltralight()NFC 贴纸
NfcAtag.isNfcA()ISO 14443-3A
NfcBtag.isNfcB()ISO 14443-3B
NfcFtag.isNfcF()FeliCa
NfcVtag.isNfcV()ISO 15693

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

踩坑清单

问题原因解决
getTagInfo 返回 nullwant 不是 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、测试、元服务和应用上架分发等。

更多推荐