更新记录

1.3.0(2026-10-27) 下载此版本

  • 新增由调用方明确发起的 Android NDEF 格式化:原生核心提供 NfcClient.startFormat,sfiora-ui 提供带操作面板的 NfcWriteController.startFormat。仅适用于当前 Android 设备支持格式化的标签。
  • RN 新旧架构、经典 uni-app legacy / UTS 和 uni-app x 均提供 formatNdef,共用 Android 原生操作面板,并支持能力查询、类型声明和自定义文案。iOS 不提供格式化,调用返回 NFC_UNSUPPORTED,不会启动扫描会话。
  • 格式化后要求移开并再次贴近同一标签,核对标签 ID、可写状态和空 NDEF 消息。已具备 NDEF 的标签保留原内容;未取得实时消息时明确返回内容未确认,不进行格式化或写入。
  • 格式化沿用取消、超时和会话互斥;指令执行后未完成验证时,提示标签可能已经改变。常规读取、写入及按业务标记初始化不会自动触发格式化。
  • 格式化面板提供中英文提示和完整自定义文案,区分格式化验证成功、原内容保留和内容未确认。
  • Android 操作面板在结果展示期间支持按返回键关闭,不会再次触发取消回调;标题随换行扩展,避免长标题被裁切。

1.2.0(2026-09-17) 下载此版本

  • 新增可选 Android 前台标签接管,避免应用空闲时因贴近标签而打开其他标签处理页面。按页面申请和释放,支持多个持有者及前后台、NFC 开关变化;默认关闭,不自动启动读写。
  • 新增 Android 操作面板状态查询与结束等待,便于完整展示操作反馈后再跳转。等待只包含调用时已经显示的面板,后续面板不延长等待;超时返回 PRESENTATION_TIMEOUT,不取消读写或强制关闭面板。
  • RN 两种架构、经典 uni-app 的 legacy / UTS 和 uni-app x 均提供上述桥接接口。iOS 可查询平台能力,但不支持 Android 前台接管或系统面板关闭时间的等待。
  • 在同一 Android 控制器上开始下一次操作时立即关闭上一次的面板,避免旧面板在页面退出后仍等待动画结束。
  • 修复 Android UTS 宿主因语言或配置变化重建后,前台标签接管可能停留在暂停状态的问题;桥接按当前 Activity 的真实生命周期恢复和清理。
  • Android UTS 读写和初始化仅在宿主处于前台时启动;旧 Activity 的迟到调用不会重新接管绑定或取消新宿主的读写,后台状态查询和清理仍可执行。

接入与生命周期示例见 Android 指南、RN 指南 和 UNI 指南。

1.1.0(2026-09-14) 下载此版本

  • RN Android 的模块调用与 Activity 生命周期统一在主线程管理;关闭中的控制器保留真实忙碌状态。
  • iOS 自动读取的 NDEF 查询与读取回调核对实际会话,忽略旧会话的迟到回调。
  • Android 将连接关闭移到独立线程,等待 I/O 退出及最终关闭后释放占用,并处理过期标签引起的关闭异常。
  • Android 深度读取在切换标签技术前完成 NDEF 关闭;切换期间取消或关闭失败时不进入后续探测。
  • Android 读取与初始化判断仅使用实时 NDEF 消息,空消息不再回退到发现时缓存。
  • 会话关闭异常迟延时,有界交付待处理结果;实际资源释放前仍保持忙碌,不自动重放写入。
  • RN、UNI legacy、UTS 和 x 提供 waitForIdle(),等待上限为 5 秒(可配置),查询失败或超时不会伪装为空闲。
  • 增加关闭阻塞、迟到结果、取消/超时、写后移开标签和 Android 平台 NDEF 编码交叉验证。
  • 补充标签技术互斥、iOS 生产读取链路的迟到回调,以及 RN 主队列与宿主生命周期交错的运行测试。
查看更多

平台兼容性

uni-app(5.24)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-vue插件版本 app-nvue Android Android插件版本 iOS iOS插件版本 鸿蒙
√ 1.1.0 √ 1.1.0 × × √ 1.1.0 × 6.0 1.1.0 15 1.1.0 ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uni-app x(5.24)

Chrome Safari Android Android插件版本 iOS iOS插件版本 鸿蒙 微信小程序
× × 6.0 1.1.0 15 1.1.0 × ×

其他

多语言 暗黑模式 宽屏模式 蒸汽模式
× √ × √

Sfiora 通用 NFC 读写

Sfiora 为经典 uni-app 和 uni-app x 的 Android/iOS App 提供 NFC 标签读取、NDEF 消息写入与回读验证,以及按标记保留或初始化。读写内容由应用决定,可使用文本、URI、MIME 或 NFC Forum External Type 记录。

Android 还提供主动调用的 NDEF 格式化与再次贴近验证;常规读写和标记初始化不会自动格式化。

兼容范围

接入方式 页面与运行环境 最低系统
经典 uni-app UTS Vue 2 / Vue 3 的 App Vue 页面,uni_modules Android API 21 / iOS 13
uni-app x Vapor,uni_modules Android API 23 / iOS 15
经典 uni-app legacy Vue 2 / Vue 3 的 App Vue 页面,原生插件 Android API 21 / iOS 13

使用 HBuilderX 5.24 或更新版本,并满足所用 HBuilderX、宿主框架和打包工具的系统要求。UTS 市场兼容表对 classic/x 使用共同最低声明 Android API 23 / iOS 15;经典 uni-app 的插件原生配置最低为 API 21 / iOS 13。

NFC 操作需要支持 NFC 的真机。本文的 uni-app x 接入说明面向 Vapor。不支持 H5、小程序、nvue 或 HarmonyOS,也不能仅靠标准基座或热更新添加原生 NFC 能力。

各渠道已上架的版本以渠道页面为准;离线包和版本历史见 GitHub Releases。同一个 App 只安装一种 Sfiora 插件形态。

安装与导入

UTS:经典 uni-app 与 uni-app x

从 DCloud 插件市场 导入 Sandrox-Sfiora,或者从 GitHub Releases 下载 sfiora-uniapp-<version>.zip。将 ZIP 内的文件解压到 uni_modules/Sandrox-Sfiora,确认 package.json 直接位于该目录中,避免再嵌套一层同名目录。

<version> 替换为所选发行版本。从 1.1.0 起,无后缀的 UNI ZIP 是经典 uni-app 与 uni-app x 共用的 UTS 包,带 -legacy 的 ZIP 是经典原生插件。历史 1.0.0 的 UTS 文件名为 sfiora-uniapp-uts-1.0.0.zip,无后缀文件为 legacy;安装历史版本时按该 Release 的说明选择。

两种宿主都使用下面的公共 JavaScript SDK:

import * as sfiora from '@/uni_modules/Sandrox-Sfiora/js_sdk/index.js';

它提供 Promise 返回值和带字符串错误码的 SfioraError。经典 uni-app 和 x Vapor 使用此入口即可,无须自行处理原生回调或 JSON。

需要在 UTS 代码中直接调用原生接口时,也可使用模块根入口:

import * as sfiora from '@/uni_modules/Sandrox-Sfiora'

直接 UTS 方法返回 Promise<UTSJSONObject>、Promise<boolean> 或 Promise<void>;拒绝值保留 code、message、recoverable、nativeError,应按字段处理,不依赖 instanceof Error。直接 UTS 导入不改变上表的宿主兼容范围,下文的 uni-app x 页面示例仍使用 Vapor。

Legacy:经典 uni-app 原生插件

从 GitHub Releases 下载 sfiora-uniapp-legacy-<version>.zip,将其中的 Sandrox-Sfiora 目录放入项目的 nativeplugins,然后在 manifest.json 的 App 原生插件配置中选择这个本地插件。

import * as sfiora from '@/nativeplugins/Sandrox-Sfiora/js_sdk/index.js';

下面经典 uni-app 示例只需替换这一行导入,API 与 UTS 版本一致。原生模块名是 Sfiora,但业务代码使用公共 SDK,不需要直接调用 uni.requireNativePlugin。

权限、签名与自定义基座

Android

Android 插件声明 android.permission.NFC,并将 NFC 硬件声明为可选,因此无 NFC 的设备也可以安装应用。NFC 没有运行时权限弹框;使用前通过 getCapabilities() 检查设备支持和系统 NFC 开关。

iOS

iOS 应用需要 NFC 用途说明、TAG entitlement,以及包含 Near Field Communication Tag Reading 能力的 App ID 和签名描述文件。用途说明应描述实际应用用途。

经典 uni-app 在 manifest.json 中合并以下配置,保留已有的其他权限:

{
  "app-plus": {
    "distribute": {
      "ios": {
        "privacyDescription": {
          "NFCReaderUsageDescription": "读取和写入您选择的 NFC 标签。"
        },
        "capabilities": {
          "entitlements": {
            "com.apple.developer.nfc.readersession.formats": ["TAG"]
          }
        }
      }
    }
  }
}

uni-app x 在项目根目录的 Info.plist 中添加用途说明:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NFCReaderUsageDescription</key>
    <string>读取和写入您选择的 NFC 标签。</string>
</dict>
</plist>

在 nativeResources/ios/UniApp.entitlements 中添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.nfc.readersession.formats</key>
    <array>
        <string>TAG</string>
    </array>
</dict>
</plist>

已有这两个文件时合并键值,不要覆盖其他插件的配置。原生资源目录规则见 DCloud iOS 原生配置。使用 FeliCa 轮询时,还需在 Info.plist 的 com.apple.developer.nfc.readersession.felica.systemcodes 中填写应用支持的真实系统码;ISO 7816 AID 配置也由宿主按目标标签提供。

构建与运行

安装插件、修改原生权限或升级插件后,制作包含当前插件的自定义基座,或重新打包完整 App,再选择该基座运行到真机。基座需要匹配 HBuilderX、平台和插件版本。只修改页面布局或调用参数时,可以在同一匹配基座上重新运行页面;原生模块、权限和签名变化需要重新打包。

接口一览

所有方法均返回 Promise。参数、结果和错误的完整类型见包内 js_sdk/index.d.ts;直接 UTS 入口使用对应的 UTS 类型。下文分别给出各操作的调用示例。

方法 用途
getCapabilities 查询设备 NFC 支持和启用状态
startScan 读取一次标签快照
writeNdef 替换并验证完整 NDEF 消息
initializeNdef 保留匹配内容,或写入并验证初始化消息
formatNdef 在 Android 上格式化兼容标签,并在再次贴近后验证
cancelScan 请求取消当前读取
cancelWrite 请求取消当前写入、初始化或格式化
isScanning 查询原生读取是否仍在进行
isWriting 查询原生写入、初始化或格式化是否仍在进行
waitForIdle 在期限内等待本桥接实例空闲
acquireForegroundDispatch / releaseForegroundDispatch 按 owner 申请/释放 Android 前台接管
getForegroundDispatchState 查询前台接管的原生状态及诊断
getPresentationState 查询插件当前显示的 Android 面板;iOS 标明不支持
waitForPresentationEnd 在期限内等待已显示的 Android 面板结束;iOS 返回不支持

开始读取

先由用户启动操作,再靠近标签;读写期间保持应用前台并稳定贴住标签。每次 startScan 只完成一次读取,不是持续事件订阅。

下面经典 uni-app 示例是业务 JS 模块中的辅助函数,由页面的点击事件调用。页面负责展示结果、处理 Promise 失败,并按生命周期说明管理按钮和取消操作:

import * as sfiora from '@/uni_modules/Sandrox-Sfiora/js_sdk/index.js';

export async function readTag() {
  const capability = await sfiora.getCapabilities();
  if (!capability.supported || !capability.enabled) {
    throw new Error('当前设备不支持 NFC,或 NFC 尚未开启。');
  }
  try {
    return await sfiora.startScan({ mode: 'automatic', timeoutMilliseconds: 30000 });
  } catch (error) {
    if (error instanceof sfiora.SfioraError && error.code === 'USER_CANCELLED') {
      return null;
    }
    throw error;
  }
}

uni-app x Vapor 的 .uvue 页面可使用下面的 UTS 方法。调用方将返回值绑定到自己的页面,并处理失败结果:

import * as sfiora from '@/uni_modules/Sandrox-Sfiora/js_sdk/index.js'

async function readTag(): Promise<UTSJSONObject> {
  const capability: UTSJSONObject = await sfiora.getCapabilities()
  const supported = capability['supported'] as boolean | null
  const enabled = capability['enabled'] as boolean | null
  if (supported != true || enabled != true) {
    throw new Error('当前设备不支持 NFC,或 NFC 尚未开启。')
  }
  return await sfiora.startScan({ mode: 'automatic', timeoutMilliseconds: 30000 })
}

三种读取模式:

mode 行为
automatic 默认模式;发现标签,并在可用时读取 NDEF
ndef 要求 NDEF;iOS 使用系统 NDEF 兼容读取会话,结果可能没有标签 ID
discover 只获取标签信息,不主动读取 NDEF

结果包含 id、technologies、warnings 和可选的 ndef。先检查 tag.ndef 是否存在,再从其 records 中读取文本 text、URI uri 或二进制内容 payloadBase64。经典 uni-app 可用可选链访问;UTS 使用 UTSJSONObject 的访问方法或明确的类型转换。

发现标签成功并不代表 NDEF 读取成功,需检查 ndef.status、ndef.readError 和 warnings。无值或读取错误不能被当成业务上的“空标签”。两端可读的技术信息和 ID 可能不同;没有标识时仍有 id 对象,但 hex、base64 为空字符串,length 为 0。

Android 的读取和初始化判断均使用实时 NDEF 内容,当前空消息不回退到发现时缓存。

替换 NDEF 消息并验证

下面的方法沿用前面的 sfiora 导入,从用户点击事件调用,并在调用处处理 Promise 失败:

export async function replaceTag() {
  return await sfiora.writeNdef({
    records: [{ kind: 'text', text: 'Hello from Sfiora', languageCode: 'en' }],
  }, { timeoutMilliseconds: 30000 });
}

这是替换整条 NDEF 消息,旧记录会被覆盖。标签必须已经支持 NDEF、可写且容量足够。成功结果包含 verified: true、bytesWritten、recordCount 和回读的 tag;验证比较完整 NDEF 消息的序列化字节。

写入没有事务回滚保证。写入开始后失去接触、取消或验证失败,标签可能保留旧内容,也可能变成新内容、部分内容或空内容。失败后重新读取,才能判断标签实际状态;不能把“报错”理解成“原内容一定没变”。

NDEF 记录格式

records 必须非空,可包含多条记录。下面四种记录既可单独使用,也可组合成一条消息:

const records = [
  { kind: 'text', text: '你好', languageCode: 'zh-Hans', encoding: 'UTF-8' },
  { kind: 'uri', uri: 'https://example.com' },
  { kind: 'mime', mediaType: 'application/octet-stream', payloadBase64: 'AQID' },
  { kind: 'external', domain: 'example.com', type: 'initialized', payloadBase64: 'AQID' },
];

Text 需要 languageCode,默认 UTF-8,也支持 UTF-16。语言码属于 NDEF 格式,不是正文;某些工具的原始值会把语言码和正文一起显示,业务应读取解析后的 text。网址建议使用带 https:// 等协议的 URI 记录。

MIME 和 External Type 的 payloadBase64 表达原始字节。例如 AQID 写入三个字节 01 02 03,不是写入四个字符。若要写入一段字符串,先按应用选择的编码转成字节,再转 Base64。各类记录都支持可选的 identifierBase64。

保留或初始化

export async function initializeTag() {
  const marker = { domain: 'example.com', type: 'initialized' };
  return await sfiora.initializeNdef({
    records: [{
      kind: 'external',
      domain: marker.domain,
      type: marker.type,
      payloadBase64: 'AQID',
    }],
  }, marker, { timeoutMilliseconds: 30000 });
}

将 example.com 换成应用自己的小写域名和类型约定。待写入消息必须包含恰好一条匹配 marker 的 External Type 记录,插件不会自动补标记。

  • 已有消息包含相同 domain/type:返回 action: 'preserved',不写入,返回已有的 tag;没有 verified 和写入字节数字段。标记记录的 payload 不参与匹配,也不会因传入新 payload 而更新。
  • 没有匹配标记:替换整条消息并回读验证,返回 action: 'initialized' 和 verified: true。已有其他内容但没有这个标记的标签也会被覆盖。
  • 无法检查现有消息时返回错误,不把读取失败当成空白。未格式化的出厂标签不会由此自动格式化。

这是一种应用约定的保留机制,不会把标签设为只读,也不是密码保护或身份认证。其他写入工具仍然可以覆盖标记;身份校验、授权、加密和业务格式由宿主负责。

在 x Vapor 中,写入和初始化使用相同消息对象,方法返回 Promise<UTSJSONObject>。例如将方法声明为 async function replaceTag(): Promise<UTSJSONObject>,读取 action 时用 result['action'] as string | null。

NDEF 格式化

需要 Sfiora 1.3.0 或更新版本,并使用匹配的 JS / UTS 包与重新构建的原生宿主。

经典 uni-app legacy、经典 UTS 和 uni-app x 均提供 formatNdef(options?)。仅 Android 支持;iOS 调用返回 NFC_UNSUPPORTED,不会打开扫描会话。getCapabilities().features 包含 ndefFormat 表示桥接提供此能力,不保证当前手机能格式化每一种标签。具体支持范围取决于 Android 对该标签是否提供 NdefFormatable。

格式化可能改写非 NDEF 数据和存储访问配置,应由用户明确发起,并在操作页面说明风险。它不是恢复出厂,也不会修复损坏、解除永久只读或破解受保护的标签。已经具备 NDEF 的标签不会被格式化或重写,只读标签仍保持只读。

沿用上面的 sfiora 导入,在页面的操作与错误处理流程中调用:

async function formatTag() {
  const capabilities = await sfiora.getCapabilities();
  if (!capabilities.supported || !capabilities.enabled
      || !capabilities.features.includes('ndefFormat')) {
    throw new Error('NDEF 格式化不可用或 NFC 未开启。');
  }
  return await sfiora.formatNdef({ timeoutMilliseconds: 60000 });
}

此调用无需传入写入内容。按钮调用方应处理返回值和拒绝,并按下一节管理取消与忙碌状态;不能把未格式化的读取结果自动连接到此操作。

  • 首次贴近时,保持标签不动,直到原生面板提示移开。
  • 移开后,再次贴近同一标签。插件核对标签 ID、可写状态,并回读验证空的 NDEF 记录。ID 比对不构成身份认证;应使用 ID 稳定的标签,且每次只贴近一个标签。
  • Promise 在最终结果或错误时结束;等待再次贴近期间仍处于 isWriting() === true。

成功结果有 operation: 'formatNdef'、action、verified 和 tag:

结果 含义
action: 'formatted'、verified: true 已完成格式化,并在再次贴近后验证通过
action: 'preserved'、verified: true 已有 NDEF,取得了实时内容,没有写入
action: 'preserved'、verified: false 已有 NDEF,但系统未返回实时消息;没有写入,内容尚未确认

不要只凭 Promise 成功就显示“格式化验证成功”。preserved 分支通常一次贴近即可返回,不需要再次贴近验证新格式。uni-app x 的调用返回 Promise<UTSJSONObject>,可通过 result['action'] as string 和 result['verified'] as boolean 读取结果。

preserved 且 verified: false 时,快照中的 tag.ndef.status 为 readError,并附有警告;返回的空记录列表不表示标签为空,应另行读取以确认内容。preserved 且 verified: true 也不保证标签可写,写入前仍需检查快照的 NDEF 状态。

timeoutMilliseconds 为 1000–60000 的整数,默认 30000;覆盖格式化和再次贴近验证的完整操作,不会在第一次贴近后重新计时。messages 可选,默认使用原生中英文文案。自定义时必须完整提供 NfcFormatMessages 的 16 个非空字符串字段:title、instruction、cancel、done、success、verificationRequired、preserved、preservedUnverified、failureTitle、timeout、nfcDisabled、nfcUnsupported、tagLost、unsupportedTag、formatFailed、verificationFailed。格式化使用托管面板,不接受无界面配置。

用 cancelWrite() 取消格式化,用 waitForIdle() 等待会话释放;Android 面板结束还需 waitForPresentationEnd()。取消、超时、丢失标签或验证失败不会回滚已执行的格式化指令,标签可能已经改变;应重新读取确认现状,不能直接认定未发生写入。

取消、页面生命周期与按钮状态

取消没有进行中的操作不会报错。取消方法完成只代表请求已送达,原始读写 Promise 的失败仍需处理,也不代表系统 NFC 面板已经收起。

页面应同时禁用读取、写入、初始化和格式化入口,条件是“本地有未完成调用,或 isScanning() / isWriting() 任一为 true”。iOS 成功结果可能在系统面板收起前返回,因此不能在 finally 中直接恢复按钮;继续查询状态,确认两项都为 false 后再恢复。状态查询失败应显示错误,不按 idle 处理。

await sfiora.waitForIdle({ timeoutMilliseconds: 5000 }) 可统一完成等待,legacy 的 JS SDK、UTS JS SDK 和直接 UTS API 均支持。超时参数为 1–60000 的整数,默认 5000;查询失败原样返回错误,超时返回 SESSION_CLOSE_TIMEOUT,即使查询没有回调也会结束等待。它不取消当前操作、不预留下一次会话,也不会在超时后释放原生占用;用户可刷新真实状态后再试。

原生关闭超过 5 秒时可先交付待处理结果,实际关闭前仍保持忙碌。

拥有当前操作的页面在 onHide / onUnload 中取消操作,并忽略页面离开后的迟到结果。不要让没有发起操作的页面随意取消另一个页面的会话。原生读写在进程内互斥,重复启动会返回忙错误。

可选的 Android 前台标签接管

前台接管及面板结束等待要求插件为 1.2.0 或更新版本,JS/UTS 代码与基座中的原生模块必须匹配。

默认不接管空闲标签。如果标签会一直贴在手机上,可在 NFC 页面显示期间开启前台接管,避免空闲标签打开其他标签处理页面。接管会忽略这些标签;读取、写入、初始化和格式化仍需主动调用各自的方法。

用 acquireForegroundDispatch(ownerId) 申请,用 releaseForegroundDispatch(ownerId) 释放。重复申请同一个 ID 只保留一份请求,释放一次即可;释放不存在的 ID 不影响其他持有者,不同页面应使用不同 ID。ID 长度为 1–128 个 ASCII 字符,首字符为字母或数字,其余可使用字母、数字、.、_、:、-;每个原生控制器最多保留 128 个不同 ID,非法 ID 返回 INVALID_OPTIONS。

经典 uni-app 可将下面的生命周期和字段合入自己的 Vue 2 / Vue 3 页面。将 foregroundState、foregroundError 绑定到页面的状态与错误区域;读写结果使用独立字段。legacy 只需替换导入路径。

import * as sfiora from '@/uni_modules/Sandrox-Sfiora/js_sdk/index.js';

let ownerSequence = 0;

export default {
  data() {
    return { foregroundOwner: null, foregroundState: null, foregroundError: null };
  },
  onShow() {
    if (uni.getSystemInfoSync().platform !== 'android' || this.foregroundOwner !== null) return;
    const ownerId = `nfc-page:${Date.now()}:${++ownerSequence}`;
    this.foregroundOwner = ownerId;
    this.foregroundError = null;
    sfiora.acquireForegroundDispatch(ownerId).then(state => {
      if (this.foregroundOwner === ownerId) this.foregroundState = state;
      else return sfiora.releaseForegroundDispatch(ownerId);
    }).catch(error => {
      if (this.foregroundOwner === ownerId) this.foregroundError = error;
      else console.error('NFC foreground cleanup failed', error);
    });
  },
  onHide() { this.releaseForeground(); },
  onUnload() { this.releaseForeground(); },
  methods: {
    releaseForeground() {
      const ownerId = this.foregroundOwner;
      this.foregroundOwner = null;
      this.foregroundState = null;
      if (ownerId !== null) {
        sfiora.releaseForegroundDispatch(ownerId)
          .catch(error => console.error('NFC foreground cleanup failed', error));
      }
    },
  },
};

退出时先释放;如果申请结果迟到,再用原 ID 释放一次。释放是幂等的,不会影响新一轮页面显示所用的 ID。onHide 与 onUnload 都可调用清理;拥有读写操作的页面还应按上一节取消自己的操作。

uni-app x Vapor 可把同一处理封装在独立的 nfc-foreground.uts 模块中。计数器留在模块作用域,使不同页面实例共享它。示例使用直接 UTS 入口,查询结果按 UTSJSONObject 取字段:

import * as sfiora from '@/uni_modules/Sandrox-Sfiora'

let ownerSequence = 0

export function attachNfcForeground(
  onState: (state: UTSJSONObject) => void,
  onError: (error: any) => void,
): () => void {
  if (uni.getSystemInfoSync().platform != 'android') return () => {}
  const ownerId = 'nfc-page:' + Date.now().toString() + ':' + (++ownerSequence).toString()
  let active = true
  const report = (error: any) => {
    if (active) onError(error)
    else console.error('NFC foreground cleanup failed', error)
  }
  sfiora.acquireForegroundDispatch(ownerId).then(async (state: UTSJSONObject) => {
    if (active) onState(state)
    else await sfiora.releaseForegroundDispatch(ownerId)
  }).catch(report)
  return () => {
    active = false
    sfiora.releaseForegroundDispatch(ownerId).catch(report)
  }
}

在页面的 <script setup lang="uts"> 中接入下面的生命周期。首次进入等到 onReady 再申请,确保新页面的原生 Activity 已就绪;后续返回页面仍由 onShow 申请。示例用字符串保存接管诊断,实际页面可按 state['state'] as string 显示状态;它们与原始读写结果分开:

import { ref } from 'vue'
import { onShow, onReady, onHide, onUnload } from '@dcloudio/uni-app'
import { attachNfcForeground } from './nfc-foreground.uts'

const foregroundJson = ref('')
const foregroundError = ref('')
let releaseForeground: (() => void) | null = null
let pageReady = false
let visible = false

function leaveForeground() {
  const release = releaseForeground
  releaseForeground = null
  if (release != null) release()
}
function enterForeground() {
  if (!pageReady || !visible || releaseForeground != null) return
  foregroundError.value = ''
  releaseForeground = attachNfcForeground(
    (state: UTSJSONObject) => { foregroundJson.value = JSON.stringify(state) },
    (error: any) => { foregroundError.value = JSON.stringify(error) },
  )
}
onShow(() => {
  visible = true
  enterForeground()
})
onReady(() => {
  pageReady = true
  enterForeground()
})
onHide(() => {
  visible = false
  leaveForeground()
})
onUnload(() => {
  visible = false
  pageReady = false
  leaveForeground()
})

申请成功返回 { platform, revision, state, error },不代表 NFC 一定启用。active 表示已接管,paused 表示暂时没有前台 Activity,nfcDisabled 表示 NFC 开关关闭,unavailable 表示无 NFC 硬件,disabled 表示没有请求,failed 表示失败且 error 包含诊断。应用从后台或设置页返回时,可通过 getForegroundDispatchState() 刷新快照;快照不是持续监听,查询失败也应单独显示。

Activity 暂停期间接管停用,请求保留,恢复前台或 NFC 开关变化后由插件重新处理。无需自行编写原生模块或接收器,也不要同时启用其他 NFC 前台分发实现。iOS 查询固定返回 { platform: 'ios', revision: 0, state: 'unavailable', error: null };申请和释放先校验 ID,再返回 NFC_UNSUPPORTED,所以上述示例只在 Android 申请。

error 为诊断字符串或 null;revision 仅在同一原生控制器的状态或错误变化时递增,控制器重建后重新计数。多个控制器位于同一 Activity 时共享注册,释放一个不影响其他请求;不同 Activity 同时申请会返回 failed,不会替换已有注册,应等先前 Activity 暂停后重试。

等待 Android 操作面板结束

需要完整展示成功反馈后再跳转时,在原始操作返回后分别等待会话空闲和面板结束。getPresentationState() 返回 { platform, supported, activePresentationIds };Android 的集合包含进程内 Sfiora 当前实际显示的面板,包括成功反馈和关闭动画。

waitForPresentationEnd() 在原生端开始执行时捕获这个集合,全部关闭后完成 Promise,没有返回值;空集合立即完成,后续新面板不延长等待。ID 只供临时识别,不应保存为业务标识;无面板读取不产生 ID,提前调用也不会等待未来才显示的面板。它只接受可选的 timeoutMilliseconds,范围为整数 1–60000,默认 5000 毫秒。非法参数返回 INVALID_OPTIONS,超时返回 PRESENTATION_TIMEOUT;超时不取消操作、不强制关闭面板、不释放 NFC 占用。

iOS 查询返回 supported: false、activePresentationIds: [],等待在校验参数后返回 NFC_UNSUPPORTED。空数组不代表系统面板已经消失;iOS 仍按会话状态安排下一次操作。

下面是经典 uni-app 的 JS 辅助函数。调用方通过两个回调分别保存读写结果与带方法名的错误,等待错误追加显示,不能覆盖原结果。返回值只表示等待是否成功,不表示读取成功;页面已离开或操作已被替换时,应忽略它的回调和返回值。

// 沿用上面的 sfiora 导入;由页面点击事件调用。
export async function readAndWaitForUi(onResult, onError) {
  let ready = true;
  const observe = async (step, task) => {
    try {
      await task;
    } catch (error) {
      ready = false;
      onError(step, error);
    }
  };
  try {
    onResult(await sfiora.startScan());
  } catch (error) {
    onError('startScan', error);
  } finally {
    const waits = [observe('waitForIdle', sfiora.waitForIdle())];
    if (uni.getSystemInfoSync().platform === 'android') {
      waits.push(observe('waitForPresentationEnd',
        sfiora.waitForPresentationEnd({ timeoutMilliseconds: 10000 })));
    }
    await Promise.all(waits);
  }
  return ready;
}

在 UTS 中使用同样的执行顺序;将 onResult 声明为 (tag: UTSJSONObject) => void,onError 为 (step: string, error: any) => void,函数返回 Promise<boolean>。局部 observe 的签名为 (step: string, task: Promise<void>): Promise<void>,waits 为 Promise<void>[]。直接 UTS 与 JS SDK 都使用同一组等待方法,错误按字段处理。

等待期间保持按钮禁用。仅当页面和操作仍然有效且返回 true,才结合设备可用状态恢复按钮;返回 false 时重新查询会话及面板,查询失败不能按空闲处理。写入、初始化、格式化和取消使用相同的结束观察流程;尤其不能把等待错误解释成写入失败并自动重写。

如果希望保留完整动画,等待期间不要启动替换操作或用取消方法隐藏已完成的面板。同一控制器的新操作及宿主销毁可能直接关闭旧面板。

参数

读取的通用参数:

参数 默认值 说明
mode automatic automatic、ndef 或 discover,行为见读取示例
timeoutMilliseconds 30000 1000–60000 的整数,单位毫秒
messages 内置提示语 自定义时提供完整的 NfcScanMessages

Android 读取设置放入 android 对象:

参数 默认值 说明
presentation managed none 关闭读取面板
deepReadEnabled false 对支持的标签启用额外只读协议探测
presenceCheckDelayMilliseconds 250 50–5000 的整数

iOS 的 ios.pollingTechnologies 默认为 ['iso14443', 'iso15693'],必须为非空数组;添加 iso18092 需配置 FeliCa 系统码。它控制 automatic、discover 的轮询,ndef 模式使用系统 NDEF 读取会话。Android、iOS 两组参数在两端都会校验,但只影响对应平台的读取行为。

写入选项只有 timeoutMilliseconds(范围和默认值同上)及完整的 NfcWriteMessages,不接受读取专用选项。Android 桥接写入使用操作面板,iOS 使用系统 NFC 面板。读取或写入选项中的未知参数、错误类型或不完整的自定义提示语会返回 INVALID_OPTIONS。完整字段见包内 js_sdk/index.d.ts 和 contract/types.d.ts。

错误处理

错误包含 code、message、recoverable 和可选 nativeError。按字符串 code 分支,不依赖设备原生错误文案:

错误码 处理方向
NFC_UNSUPPORTED / NFC_DISABLED 检查硬件支持或引导开启 NFC
SCAN_BUSY / WRITE_BUSY 等当前会话释放,避免连续自动重试
USER_CANCELLED 正常结束这次交互
SESSION_CLOSE_TIMEOUT 保留原生状态门控,允许刷新,不能假定会话已关闭
PRESENTATION_TIMEOUT 保留原操作结果,刷新面板及会话状态后再继续
SCAN_TIMEOUT / WRITE_TIMEOUT / TAG_LOST 调整贴合位置后由用户重试
UNSUPPORTED_TAG / TAG_READ_ONLY / NDEF_CAPACITY_EXCEEDED 更换合适的可写 NDEF 标签或缩小消息
READ_FAILED 检查读取错误,不能按空标签继续处理
WRITE_FAILED / WRITE_VERIFICATION_FAILED 重新读取确认标签现状
INVALID_OPTIONS 修正参数
INTERNAL_ERROR 检查基座、模块加载、页面生命周期及原生错误

recoverable 只是重试提示,不保证标签未改变;写入期间的取消、超时和丢失标签也遵循前面的写入不确定性说明。

常见接入问题

  • 提示模块不存在:核对安装目录、legacy 原生插件勾选、导入路径,以及运行时选择的自定义基座。只更新 JS 不能补入缺失的原生模块。
  • iOS 会话无法开始:核对用途说明、最终签名中的 TAG entitlement、App ID 与描述文件;修改后重打基座或 App。
  • 手机打开了系统标签页面:先启动 Sfiora 操作再贴标签;若标签会一直贴在手机上,可在 Android 页面显示期间启用上面的前台标签接管。需要自行处理空闲标签内容时,由宿主选择自己的分发方案,勿与 Sfiora 接管同时注册。
  • 取消后按钮暂时不可点:确认正在等待真实会话释放;按钮状态应与原生状态一致。
  • 读到技术信息却不能写:能发现某种技术不等于能写该标签;写入只面向已支持 NDEF 的可写标签。

隐私、能力边界与许可

插件在设备上读取、处理并返回标签数据,按调用方提供的内容写入标签。它无广告、统计或推广 SDK,不向作者服务器上传标签内容,也不在宿主设备持久化标签内容;业务校验和后续存储由调用方负责。

Sfiora 的 UNI 接口不提供永久锁定、标签密码管理、门禁卡复制、卡模拟或任意 APDU/私有区写入。标记与读取出的 ID 不构成身份真实性证明。

源码与问题反馈 · 版本记录 · Apache License 2.0

反馈时提供插件版本、HBuilderX 版本、宿主模式、系统与设备、操作和结构化错误;标签私密内容请先去除。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android NFC 权限;iOS NFCReaderUsageDescription 和 TAG entitlement。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件在设备上读取、处理并返回 NFC 标签数据,按调用方提供的内容写入标签;不向作者服务器上传数据,不包含统计 SDK,也不在宿主设备持久化标签内容。数据的后续使用与存储由调用方负责。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

许可协议

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

  1. Definitions.

    "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

    "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

    "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

    "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

    "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

    "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

    "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

    "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

    "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

    "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

  2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

  3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

  4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

    (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

    (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

    (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

    (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

  5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

  6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

  7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

  8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

  9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

    END OF TERMS AND CONDITIONS

    APPENDIX: How to apply the Apache License to your work.

    To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

    Copyright [yyyy] [name of copyright owner]

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.