1. 为什么我会选 HTML5 而不是原生 App
1.1 先说说这个工具到底要解决什么问题
我是在做一次线下活动签到方案时被“恶心”到的:现场志愿者手拿登记表,访客排长队,要么手写信息,要么打开手机调出二维码让工作人员扫。看起来很快,但遇到网络波动、光线太暗、二维码被折皱,体验就很糟。后来我想到,不少访客手里已经有工牌、门禁卡,甚至有些主办方会发 NFC 小卡片。如果手机能直接读卡片、把卡内的链接或编号转成一个标准二维码,再让负责签到的人扫这个码,就能把“找码”的步骤变成“刷一下卡”,体验会顺很多。
不过做这种工具,首先得想清楚交付形态:是做一个 Android 原生 App,还是网页?原生 App 权限全、能力上限高,但开发周期长,Android 和 iOS 各做一遍,光是系统适配、签名、真机测试就够呛。考虑到这只是个内部辅助工具,不是对外发布的产品,我更倾向用 HTML5 快速落地,直接让手机浏览器跑起来。
1.2 HTML5 技术选型:Web NFC 的边界与取舍
说到 HTML5 做 NFC,首先绕不开 Web NFC 这套 API。它的核心类是 NDEFReader,浏览器会在页面打开且用户点击按钮触发后,发起 NFC 扫描。兼容性目前比较明确:Android 平台的 Chrome 89 及以上版本可用,桌面端 Chrome 和 iOS 的 Safari 基本不支持,这个现实大家要提前接受。
Web NFC 能做的是读取写入了 NDEF 标准的 NFC 标签,比如常见的 NFC Forum Type 2/4 卡片、贴纸,上面封装的是 URL、文本这类记录。它不能做的是访问 UID、扇区认证、模拟卡片这类偏底层的操作,也不支持读取 Mifare Classic 加密扇区。所以如果你想做的是一个“读任意卡片的物理 ID”,Web NFC 帮不上忙,得走原生 App 或者 USB 读卡器。我的项目定位很清晰:只读取标准 NDEF 记录,把卡片里的网址或纯文本转换成二维码,这就足够了。
1.3 项目目标与功能清单
我给这个工具定下的核心要求很简单:打开页面、点按钮、把卡贴近手机、页面自动把读到的内容转成二维码,整个过程不经过后台服务器,不走任何外部接口,数据只留在当前设备里。这样就能避免数据外传的合规问题和后端开发成本。
最终功能清单我控制得很克制:
- 浏览器检测:判断是否支持 Web NFC,不支持就明确提示。
- NFC 读取:从 NDEF 标签中读取 URL 或文本记录。
- 二维码生成:读取成功后,在前端生成二维码。
- 二维码下载:方便现场复用或打印。
- 错误提示:权限拒绝、读取失败、卡内容为空等场景有清晰反馈。
这套功能看起来简单,但实际开发时踩了不少坑,尤其是浏览器兼容、权限交互、以及“二维码到底该用 <img> 还是 <canvas>”这种细节。下面我把完整思路和可复现代码整理出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用 GLM-5 把模糊想法翻译成代码
2.1 我是怎么写提示词的
一开始我没有从头写代码,而是先用 GLM-5 帮我搭骨架。很多开发者把 GLM-5 这类模型当成“搜索引擎 + 代码生成器”,其实效果好不好,关键看你给的提示词是否足够具体。我第一次给的比较粗糙:只说了“用 HTML5 读取 NFC 标签并生成二维码”,结果给了我一版没法直接跑的代码,问题出在浏览器 API 兼容判断、二维码渲染方式、以及空数据处理上都不完整。
后来我调整了提示词的写法,把需求拆成五个部分:页面目标、核心功能、运行环境、边界情况、视觉要求。实际使用的提示词大概是这样的:
code复制你是一名前端开发工程师。我需要一个纯前端的 HTML5 页面:
1. 调用 Web NFC API 读取 NFC 标签
2. 如果标签内容是 URL 或文本,就自动生成二维码
3. 页面要适配手机屏幕,在 Android Chrome 上运行
4. 不需要后端,所有逻辑都在浏览器完成
5. 请处理权限拒绝、读取失败、空内容等异常
请给出完整 HTML 文件代码,注释写全。
GLM-5 给出的代码整体可用,但我在细化时还是做了不少调整,比如加了二维码下载按钮、用了 canvas 导出图片、把读取状态做了更友好的中文提示。这说明一个道理:AI 给的代码是“骨架”,真正的落地优化还得靠自己的场景判断。
2.2 原理解读:NDEF 消息怎么变成二维码
NFC 标签的数据不是“一整块字符串”,而是按 NDEF 结构组织的。一个 NDEF 消息可以包含多条记录,每条记录有 recordType(比如 url、text、mime 等)和具体数据。Web NFC 的 reading 事件会返回 message.records,我们遍历这些记录,取出 URL 或文本,就拿到了核心内容。
二维码的原理和 NFC 其实挺像:它是一种把文本编码成图像符号的方式,扫描时解码回文本。所以“NFC 读卡转二维码”的本质,就是把 NFC 卡里保存的字符串取出来,再给另一个扫码终端使用。这个过程在浏览器里非常流畅,因为前端本身就有成熟的二维码库,比如 qrcode.js,直接调用即可。
顺带一提,二维码本身可承载的数据量有限,如果 NFC 标签里是一段长文本,直接生成二维码可能导致图案过密难以识别。所以我建议在读取后做长度判断,超过 500 个字符时提示用户内容过长,或者只取前 200 个字符生成。这个细节在实际使用中很关键,不然现场扫码时会反复失败。
3. 核心代码与实操步骤
3.1 HTML 页面骨架与二维码容器
整体页面我做得比较克制,核心元素就四个:标题、说明文字、读取按钮、状态提示区、二维码展示区。为了同时支持 qrcode.js 的 DOM 渲染和 canvas 导出,我保留了两种容器。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFC 读卡转二维码</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: system-ui, -apple-system, sans-serif;
background: #f5f7fa;
}
.container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.btn {
display: block;
width: 100%;
padding: 14px;
background: #2b7fff;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
.btn:disabled {
background: #a8b3c0;
cursor: not-allowed;
}
.status {
margin-top: 16px;
min-height: 24px;
color: #333;
font-size: 14px;
word-break: break-all;
}
.qr-box {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 240px;
background: #fafafa;
border-radius: 8px;
}
.download {
margin-top: 12px;
text-align: center;
}
.download a {
color: #2b7fff;
text-decoration: none;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>NFC 读卡转二维码</h1>
<p style="color:#666;">点击按钮后,将 NFC 卡片靠近手机背面感应区</p>
<button id="startBtn" class="btn">开始读取 NFC</button>
<div id="status" class="status">等待操作...</div>
<div id="qrcode" class="qr-box"></div>
<div class="download">
<a id="downloadLink" style="display:none;" download="qrcode.png">下载二维码</a>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
这里有一个很容易踩的坑:不要把 qrcode.js 的 script 标签放在页面底部,然后又动态生成二维码。建议像上面这样在 head 里提前加载,避免回调还没准备好就执行 new QRCode()。
3.2 JavaScript:NFC 读取逻辑
我单独建了一个 app.js,核心逻辑放在这里。第一步是做能力检测,因为很多测试人员第一次会用电脑浏览器打开,直接报“不支持”总比“点了没反应”清楚得多。
javascript复制const startBtn = document.getElementById('startBtn');
const statusDiv = document.getElementById('status');
const qrcodeDiv = document.getElementById('qrcode');
const downloadLink = document.getElementById('downloadLink');
async function startNfc() {
if (!('NDEFReader' in window)) {
statusDiv.textContent = '当前浏览器不支持 Web NFC,请使用 Android 版 Chrome';
return;
}
try {
const reader = new NDEFReader();
await reader.scan();
statusDiv.textContent = '正在扫描,请将 NFC 卡片靠近手机背面...';
startBtn.disabled = true;
reader.onreading = ({ message }) => {
let result = '';
for (const record of message.records) {
if (record.recordType === 'url') {
result = record.data;
} else if (record.recordType === 'text') {
result = record.data;
}
if (result) break;
}
result = (result || '').trim();
if (!result) {
statusDiv.textContent = '未读取到有效内容,请确认 NFC 卡是否写入了文本或网址';
startBtn.disabled = false;
return;
}
statusDiv.textContent = '读取成功:' + result;
qrcodeDiv.innerHTML = '';
// 新版 qrcode.js 推荐用 toCanvas,这里为了兼容性用了 new QRCode
new QRCode(qrcodeDiv, {
text: result,
width: 220,
height: 220,
correctLevel: QRCode.CorrectLevel.M
});
const qrCanvas = qrcodeDiv.querySelector('canvas');
if (qrCanvas) {
downloadLink.href = qrCanvas.toDataURL('image/png');
downloadLink.style.display = 'block';
}
startBtn.disabled = false;
};
} catch (err) {
statusDiv.textContent = 'NFC 读取失败:' + err.message;
startBtn.disabled = false;
}
}
startBtn.addEventListener('click', startNfc);
有几个细节我想多说两句。第一,record.recordType 在不同版本的 Chrome 里可能返回 'url' 或 'text',但也可能遇到 'mime' 这类情况,所以遍历时最好判断一下。第二,record.data 在某些浏览器实现中可能是 ArrayBuffer,这时候直接赋给 result 会显示为乱码。为了稳妥,可以先判断类型,再转换:
javascript复制if (record.recordType === 'url') {
if (record.data instanceof ArrayBuffer) {
result = new TextDecoder().decode(record.data);
} else {
result = record.data;
}
}
这个问题在 Android Chrome 旧版本上比较常见,我建议直接加上,避免线上使用时翻车。此外,权限机制也值得注意:Web NFC 的 scan() 必须在用户手势(按钮点击)之后调用,否则会被浏览器拦截。纯页面加载后直接扫描是不行的。
3.3 二维码生成与异常处理
二维码这块我直接用 qrcode.js,因为它是现在前端生成二维码的主流库,打包体积也小。生成时我调了 correctLevel 为 M,即约 15% 的容错率,足够应对常见的扫码距离和轻微污损,比高容错级别生成的图案小一些、扫起来更快。
很多教程会让你用 QRCode.toDataURL(),但我实测下来,在新版 qrcode.js 中直接 new QRCode() 生成 canvas,再用 toDataURL() 导出,兼容性最稳。要注意的是,生成完二维码后,页面上的 innerHTML 会被替换,所以每次读取前先 qrcodeDiv.innerHTML = '',避免多个二维码叠加在一起。
异常处理方面,除了常见的 NotAllowedError(用户拒绝权限)、NotSupportedError(浏览器不支持)之外,我还观察到一个很烦的问题:手机上的 Chrome 已经把 Web NFC 的权限弹窗显示出来了,但用户没有点“允许”,而是直接切到后台,再回页面时 scan() 就会卡死。所以我加了一个简单的超时提示:
javascript复制const timeoutId = setTimeout(() => {
statusDiv.textContent = '没有检测到卡片,请稍后再试或检查手机 NFC 是否打开';
startBtn.disabled = false;
}, 30000);
这个 30 秒超时在实际活动中很管用,至少不会让现场人员一脸懵地等着。
4. 上线部署与兼容性细节
4.1 部署到 HTTPS 服务器
Web NFC 有安全上下文要求,也就是说页面必须运行在 HTTPS 域名下,或者本地 localhost 环境。直接把 HTML 发给别人用 file:// 打开,这招是行不通的。所以我把这个单页应用托管到了 GitHub Pages 上,配置很简单:仓库里放 index.html 和 app.js,开启 Pages 后拿着生成的 HTTPS 地址就能在手机上访问。
如果你的使用场景是在某一场活动内部,不想到公网部署,还可以用局域网服务器加自签名证书,但这里有个坑:Chrome 对自签名证书通常不信任,需要在手机系统里手动安装 CA 证书,否则 Web NFC 依然无法启用。为了省事,我还是推荐直接用 GitHub Pages、腾讯云静态托管、或者任何支持 HTTPS 的对象存储网页功能,几分钟就能上线。
另外要提醒一点:因为功能很简单,我这个工具没有做任何后端接口,也没有数据库,二维码的内容完全来自 NFC 卡本身,所以不存在服务器数据泄露问题。不过如果后续要记录“谁刷了哪张卡”,那就得加后端了,复杂度会明显上升。
4.2 Android Chrome 配置与真机测试
真机测试时,我一开始用的是华为手机自带浏览器,结果点按钮没有任何反应。后来意识到问题在于:Web NFC 只有 Chrome 内核的浏览器实现了,华为自带浏览器、UC、夸克这些基本都是不支持的。所以我把测试环境锁定为“Android 手机 + 最新版 Chrome”。打开 Chrome 后,在地址栏输入 URL 前最好确认一下“桌面版网站”是关闭状态,不然 Chrome 会尝试用桌面 UA 渲染,可能影响 API 行为。
NFC 感应区的位置也很关键。不同手机差别很大,有些在手机背面中上部,有些在摄像头附近。如果贴卡片时距离太远或者角度不对,onreading 事件就不会触发。实际操作中,我会先让手机完全熄屏唤醒,然后打开页面,再拿卡片靠着手机背面慢慢移动,通常能在两秒内读出来。
我录了一段简单的流程:打开 HTTPS 页面 → 点击“开始读取 NFC” → 弹出权限请求 → 点击允许 → 将卡片贴近手机 → 页面出现读取到的链接 → 自动生成二维码。整套流程顺畅的话 5 秒内完成,比现场手动打开二维码再等待放大要快得多。
4.3 功能扩展:写入提示与批量操作
MVP 版本上线后,我马上遇到一个新的使用场景:现场有些人拿的不是已经写好 URL 的 NFC 卡,而是空白卡。他们问能不能直接通过网页把链接写进去?Web NFC 本身支持写操作,使用 NDEFReader.write() 就可以。但前提是卡片可写且未加密,这个操作同样只能在 Android Chrome 上完成。
我简单实现了一个写入区域:输入框 + 写入按钮。调用方式大概是:
javascript复制const writer = new NDEFReader();
await writer.write(inputValue);
这里要注意的是,如果要写的是 URL,规范建议用 URL 记录类型而不是文本记录类型。设置方式可以参考:
javascript复制await writer.write({
records: [{
type: 'url',
data: inputValue
}]
});
这样写进去后,其他支持 Web NFC 或原生读取的设备可以直接识别为网址。不过因为我这个项目定位是“读卡转二维码”,写入功能不是重点,所以我只在内部测试时加了,没有放进线上工具。
5. 常见问题与排查记录
5.1 常见报错与解决方式
我整理了这段时间真机测试和现场使用中遇到的几个典型问题,做成了一张速查表:
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 点击按钮后提示 NotSupportedError | 浏览器不是 Chrome,或系统版本过旧 | 更换 Android 版 Chrome,更新系统 WebView |
| 点击按钮后没有反应 | 页面不是 HTTPS,或者没有在手机前台 | 部署到 HTTPS 环境并保持页面在前台 |
| 权限弹窗不出现 | 用户手势条件未满足 | 确保调用 scan() 是在点击事件的回调里 |
| 卡片贴近后读不出 | 卡片未写入 NDEF 格式,或靠近位置不对 | 更换标准 NDEF 标签,调整贴卡位置 |
| 读取到乱码 | record.data 是 ArrayBuffer 未转字符串 |
用 TextDecoder().decode() 处理 |
| 二维码生成空白 | 读取到内容是空字符串或空格 | 先 .trim() 再判断,空值不生成 |
| 二维码模糊难扫 | 内容过长或容错级别太高 | 缩短内容,使用 CorrectLevel.M |
| iOS 手机无法使用 | Safari 不支持 Web NFC | 改用 Android Chrome,或扫描已生成的二维码 |
这些问题的核心都指向同一件事:Web NFC 还处于一个“能用但不够普及”的阶段。如果你要交付给非技术人员,最好把兼容性提示做得很直白,避免他们卡在第一步。
5.2 兼容性对比与建议
从实际使用看,Chrome for Android 是当前唯一比较成熟的 Web NFC 实现环境。Chrome 桌面版虽然也有 NDEFReader 的部分实现,但通常需要连接 USB NFC 读卡器,而且 API 行为不稳定。iOS 上的 Safari 至今没有开放 Web NFC,所以如果有人拿着 iPhone 要现场读取 NFC 卡,目前只能靠外设或者专用原生 App,这个限制短期很难绕开。
如果你负责的是一个长期项目,我建议不要只依赖浏览器,可以考虑两端结合:前端保留现在的 HTML5 页面用于快速展示和二维码生成,后端或原生壳再补一套完整的扫码和刷卡记录逻辑。不过对于“读卡转二维码”这个小场景,纯 HTML5 方案已经完全够用了。
5.3 我先踩过的一个坑
最后分享一个我印象最深的坑:第一次上线时,我把 qrcode.js 的 CDN 放在国外源,现场活动场地网络不好,结果页面加载了 3 分钟才把二维码库拉下来。后来我改成把 qrcode.min.js 下载到本地,和 index.html、app.js 放在同一个目录下,再部署一次。这个改动看起来不起眼,但在弱网环境下救了大命。
如果你要在内网环境使用,强烈建议全本地化,连字体图标、CSS 框架都不要引,全部内联或本地文件。毕竟活动现场网络不是你能控制的,一个 CDN 挂了,整个工具可能就瘫了。这套工具本身能力就不重,完全没必要依赖外部资源。
最终我看到的成果是:现场同事用 Android Chrome 打开页面,点一下读取按钮,刷一下 NFC 工牌,屏幕上就会出现一个对应链接的二维码,再用签到机扫一下,整个流程非常顺滑。虽然技术栈不复杂,但把 NFC 和二维码串起来,还是帮现场省了不少事。以后如果再遇到类似需求,我应该会在此基础上加入历史记录和批量导出功能,让工具从“现场玩具”变成“小型工作台”。
