前阵子接了个现场活动的小需求——签到用的 NFC 胸卡是现成的,里面写好了参会者编号,但入场闸机只认二维码。现场没有专用读卡器,只有几台能刷 NFC 的 Android 手机。同事的第一反应是上原生 App,我看了一眼排期,决定先试试 HTML5。
没有任何原生代码、不需要装客户端,浏览器打开页面,手机靠近 NFC 卡就自动出二维码——这个流程听起来有点"邪门",但真做起来比想象中顺。整个过程我借助 GLM-5 做提示词辅助编码,从第一版代码到上线只花了一个下午。这篇文章把完整思路、提示词、核心代码和现场踩坑全部记录下来,给同样需要在 Web 端操作 NFC 的同学一个可直接参考的方案。不管你是想临时做个读卡工具,还是想评估 Web NFC 这条路能不能走,这篇都值得看完。
1. 方案选型:HTML5 + Web NFC 是怎么赢的
1.1 需求拆解:一个"刷卡出码"的轻量工具
先把需求拆干净。表面上需求是"读 NFC 卡转二维码",实际上有三个硬约束:
- 输入:NFC 标签,里面以 NDEF 格式写了一条文本记录,内容就是参会者 ID,比如
EVT-2024-00001。部分胸卡写的也可能是 URL。 - 输出:一张二维码,闸机扫码后能识别出这个 ID,与后台的参会名单联动,实现核验通过。
- 现场条件:操作人员手里只有几台 Android 手机,无线网络环境一般,不能要求现场用户安装 App,页面必须秒开,还得有离线可用的兜底方案。
把这个需求映射到技术选型,就是一句话:找一个能在 Android 手机浏览器里直接读 NFC,并生成二维码的方案。这直接排除了原生 App 和微信小程序之后,剩下的路就是 Web NFC API + 前端二维码生成库。项目听起来小,但每个环节都卡在"能用"和"好用"之间,选型比写代码更值得花时间。
1.2 为什么不用原生 App、小程序、蓝牙读卡器
我先列一下当时对比过的几个方案,方便你以后直接套用这个判断逻辑:
| 方案 | 开发成本 | 安装/分发 | 兼容性 | 现场体验 |
|---|---|---|---|---|
| 原生 App | 2~5 天 | 需要打包、分发、安装 | 高 | 最好,但太重 |
| 微信小程序 | 1~3 天 | 需要开发者账号、域名配置 | 中,NFC 能力限制多 | 一般 |
| Web NFC H5 | 0.5 天 | 零安装,扫码即用 | Android Chrome 可用 | 足够 |
| 专用蓝牙读卡器 | 硬件成本高 | 需要配对和驱动 | 高 | 稳定但成本高 |
从现场场景看,原生 App 的"权限控制最强、体验最好"并不是关键优势,因为我们的操作人员只有几个人,用不到分发渠道。小程序的问题在于 NFC 能力在不同系统、不同小程序基础库上表现参差,有时候还要走插件,调试成本不低。蓝牙读卡器倒是稳,但现场没有这设备,采购周期也来不及。
H5 的劣势当时也很清楚:iOS Safari 不支持 Web NFC。但这正好被现场条件化解了——操作人员手里的工作机清一色 Android,参会者只需要扫二维码,不需要自己刷 NFC。所以 H5 是性价比最高的选择,风险可控,成本最低。
注意:方案选型最重要的不是"哪个技术最好",而是"哪个技术最适合当前场景"。如果一个功能只在特定设备上用过,那"特定设备支持"就是第一优先级。
1.3 技术栈与整体架构
最终的技术栈非常简单,没有任何后端:
- HTML5 + JavaScript,无框架,纯静态页面
- Web NFC API(NDEFReader),负责读取 NFC 标签
- qrcode.js(node-qrcode 的浏览器构建版),负责把读到的内容生成二维码
- GLM-5,负责提示词辅助编码与代码迭代
- Bootstrap 4,负责移动端页面的基础样式
架构上就是一个单页面应用:页面加载后监听用户操作,点击"开始扫描"后创建 NDEFReader 实例,监听 read 事件;读到内容后,把文本或 URL 渲染到页面展示区,同时传给 QRCode 生成二维码。整个过程没有后端,数据不出浏览器,隐私上反而更干净,不用考虑服务器存储和日志脱敏的问题。
这里有个很重要的设计取舍:为什么不走后端生成二维码?因为静态页面加一个前端库就够了,后端会增加部署和运维成本,而且现场网络万一抖动,前后端分离的方案就容易被卡脖子。把二维码生成放在本地,等于把最后一步的故障面降到了零。这也是整体架构偏"保守"的原因——越接近现场,越要用最简单可靠的方式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 提示词工程:用 GLM-5 写核心代码的正确姿势
2.1 第一轮提示词:把需求翻译成代码
我写的第一版提示词是这样的。划重点:提示词写得越具体,返回结果越接近目标,尤其是像"移动端、按钮大、适合现场点击"这种现场约束,必须明确说出来,AI 才能体现在代码里。
提示语:
code复制帮我用 HTML5 + JavaScript 写一个移动端网页,功能如下:
1、页面里有一个按钮叫"读取 NFC 卡片",点击后调用 Web NFC 的 NDEFReader 扫描 NFC 标签。
2、读取成功后,把标签里的文本记录内容显示在页面上,显示卡片序列号。
3、使用 Bootstrap 4 做响应式样式,按钮要大,适合现场点击。
4、如果浏览器不支持 NFC,在页面上显示红色提示。
5、代码带上必要注释。
GLM-5 生成的代码骨架基本能跑,核心逻辑是 new NDEFReader() 调 scan(),然后在 read 事件里取 message.records 和 serialNumber。这套代码我在 Chrome 89+ 的 Android 手机上直接测试,能正常读到卡片文本。核心结构长这样:
javascript复制async function startScan() {
if (!('NDEFReader' in window)) {
statusEl.textContent = '当前浏览器不支持 NFC';
return;
}
const reader = new NDEFReader();
try {
await reader.scan();
statusEl.textContent = '请将 NFC 卡靠近手机';
} catch (err) {
statusEl.textContent = '扫描初始化失败:' + err.message;
}
reader.addEventListener('read', (event) => {
const { serialNumber, message } = event;
// 处理 message.records,取文本内容
});
}
这里有个值得说的点:scan() 返回的是 Promise,用户点击按钮后浏览器会弹出 NFC 权限提示,用户点击允许后才真正开始扫描。这正是 Web NFC 在权限设计上的优势——必须用户主动触发,网页不能偷偷扫描。第一次跑通这段代码时,我最大的感受是:AI 把 API 拼装这件事做得非常快,但如果我不懂 NDEFReader 事件的触发机制,后面加业务逻辑的时候一定会卡壳。
2.2 迭代提示:逐步补齐二维码与交互细节
第一版只是"能读",离"能用"还差二维码、日志、降级入口、空状态。这些我都是通过第二轮、第三轮提示词补进去的。实测下来,分步迭代的出错率远低于一次给全需求。
- 第二次提问:"继续改这段代码,读取成功后用 qrcode.js 在页面生成二维码,二维码内容就是读到的那条文本。"
- 第三次提问:"增加一个日志区,每次读卡的时间、序列号、内容都追加到列表里,最新记录显示在最上面。"
- 第四次提问:"增加手动输入卡号的输入框和生成二维码按钮,用于不支持 NFC 的设备或测试环境。"
如果一次把 5 个需求全写进一条提示词,生成的代码往往有隐藏 bug,而且很难定位是哪个需求引起的。每次只加一个功能点,让模型在上一版基础上迭代,出问题时你能立刻判断是上一次的改动还是新加的引入。这个思路跟手工写代码时的 git 最小提交如出一辙。
2.3 用 AI 辅助排查兼容性与原理问题
除了写代码,我还用 GLM-5 做了两件事:技术选型确认和报错排查。
比如我一开始不确定 Web NFC 在 Android Chrome 里是否需要 HTTPS,直接问它"Web NFC API 的浏览器兼容性和 HTTPS 要求",它给出的结论是:必须使用 HTTPS 或 localhost,Android Chrome 89 以上支持。这个结论我再到 MDN 官网核对了一遍,完全一致。
还有一次现场报错是 NotFoundError: NFC disabled,我直接把这行报错粘贴给它,它解释说这是手机系统 NFC 开关没打开,需要在系统设置里打开 NFC 硬件开关,和浏览器授权是两回事。这个定位非常快,省了查文档的时间。
提醒:AI 辅助编码不等于把代码全交给 AI。生成的关键代码必须人工 review 边界情况,比如权限拒绝、重复点击、事件重复绑定。AI 能帮你写 80%,剩下的 20% 恰恰是最容易出问题的现场适配。
3. 核心细节:NDEFReader 读卡原理与兼容性
3.1 NDEF 数据格式与读卡流程
NFC 卡片本身只是一块存储芯片和一个天线线圈,工作在 13.56MHz 频段,通信距离通常不超过 4 厘米。为什么这么近?因为它靠电磁感应供电,读卡器发出的射频场同时给卡片供电和传数据,距离越远能量衰减越严重,所以天然就是"贴一贴"的操作方式。这对现场操作反而是好事,误读旁边卡片的概率很低。
卡片里存的数据不是普通文件,而是遵循 NFC Forum 定义的 NDEF(NFC Data Exchange Format)格式。NDEF 消息由一条或多条记录组成,每条记录有 recordType(类型)、payload(数据)、id 等字段。最常见的类型是 text(纯文本)和 url(网址)。Web NFC 的 NDEFReader 就是浏览器对 NDEF 解析的封装,读卡后通过 read 事件把 message 对象抛出来,里面就是 NDEF 消息的完整解析结果。
理解了这一点,你就能明白为什么有时候 NFC 卡片靠近手机没反应——市面上很多门禁卡、电梯卡用的是非 NDEF 的私有数据格式,Web NFC 只能读 NDEF 标签,遇到私有格式它就会抛 readingerror 或者干脆不触发 read 事件。所以做这种工具前,第一步要确认你的卡片是不是标准 NDEF 格式,我用手机上的厂商 App 读了一下,确认内容是标准 NDEF 文本记录才继续。
3.2 核心 API:NDEFReader 详解
Web NFC 读取的代码核心就三个东西:检测、扫描、事件监听。直接看代码:
javascript复制// 检测浏览器是否支持
if ('NDEFReader' in window) {
// 支持
}
// 创建读卡器实例
const reader = new NDEFReader();
// 发起扫描,返回 Promise
const scanResult = await reader.scan();
// 监听读取成功事件
reader.addEventListener('read', (event) => {
const { serialNumber, message } = event;
console.log('卡号(序列号):', serialNumber);
for (const record of message.records) {
console.log('类型:', record.recordType);
console.log('内容:', record.data);
}
});
// 监听读取失败事件
reader.addEventListener('readingerror', () => {
console.log('读取失败:请重新靠近卡片');
});
这里有几个关键点值得展开:
reader.scan()返回一个 Promise,用户点击按钮后浏览器弹出 NFC 权限提示,用户点击允许后才真正开始扫描。这个"用户主动触发"的机制非常关键,它保证了网页不能后台偷偷读卡。serialNumber是卡片物理序列号(UID),但不同卡片的 UID 长度不同,有的 4 字节,有的 7 字节。而且许多新出厂的标签只有 NDEF 内容,序列号不一定适合当业务主键,最好以 NDEF 内容为准。message.records是 NDEF 消息里的所有记录。对text记录,record.data可能是ArrayBuffer,需要new TextDecoder().decode(record.data);对url记录,record.data本身就是字符串。所以代码里要判断类型再分别处理。
3.3 前置条件:HTTPS、系统开关、浏览器版本
Web NFC 能跑起来,必须同时满足四个前置条件,缺一个都跑不通:
| 条件 | 要求 | 原因 |
|---|---|---|
| 系统 | Android 6.0+,且开启 NFC 硬件开关 | NFC 硬件由 Android 系统管理,浏览器无权限直接打开 |
| 浏览器 | Chrome 89+(Android 版) | Web NFC API 从 Chrome 89 开始默认开启 |
| 传输协议 | HTTPS 或 localhost | 浏览器安全策略要求,避免页面在网络传输中被篡改 |
| 用户授权 | 每次 scan() 都要用户点击允许 |
防止恶意网页静默读取用户卡片 |
第一条经常被忽略。手机设置里 NFC 开关没开,Chrome 会直接抛 NotFoundError,提示信息跟"浏览器不支持 NFC"是两回事,很容易混在一起。我在现场就遇到过一次,排查半天发现是 NFC 硬件开关没开。这与当年 HTML5 播放器在不同浏览器上的兼容问题如出一辙——标准有了,实现参差,必须自己做兼容判断。
3.4 从读卡到出码:qrcode.js 的接入
读卡拿到了文本内容后,剩下就是生成二维码。我选了 qrcode.js(node-qrcode 的浏览器构建版),加载方式:
html复制<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
生成二维码核心代码:
javascript复制QRCode.toDataURL(text, {
width: 256,
margin: 1,
errorCorrectionLevel: 'M'
}, (err, url) => {
if (err) {
log('二维码生成失败:' + err.message);
return;
}
qrImg.src = url;
});
这里有几个参数值得单独说:
errorCorrectionLevel建议选'M',而不是默认的'Q'。活动现场闸机扫码时二维码可能被部分遮挡,但也不需要过高的冗余('H'会让码变密、更难扫)。'M'是容错和密度的平衡点。width按实际显示大小来定,现场用 256 像素刚好,二维码四角不会被裁掉。toDataURL返回的是 base64 图片,可以直接设置img的 `
