
3个坑搞定网页扫一扫在线使用一文搞懂
复制来的代码跑不通不知道怎么调?别急着骂街。我见过太多人卡在二维码生成的最后一步,明明逻辑没错,页面却白屏或者报错。今天咱们不整虚的,把网页扫一扫在线使用这个高频痛点彻底拆解。从底层原理到前端实现,再到面试中的刁钻追问,一文搞懂背后到底在考什么。这不是简单的API调用,而是对浏览器安全、跨域资源、以及前后端协作的一次综合拷问。
考点梳理:为什么“扫一扫”这么难?
很多初学者觉得,网页上生成个二维码不就是调个库吗?错了。面试官问这个,考的从来不是“会不会写 QRCode”,而是工程落地能力。
在真实的互联网项目中,“网页扫一扫”通常包含三个核心场景:
前端生成二维码:用户填写信息,前端实时生成二维码图片,无需后端参与。
后端生成二维码内容:后端生成唯一的 Token 或 URL,前端拿到后渲染成二维码。
扫码后的业务闭环:手机扫码后,如何准确识别是哪个用户、哪个设备、哪个会话发起的?
面试中,90% 的候选人只回答了第一点。但大厂关注的核心在于:安全性(Token 泄露风险)、性能(大量并发下前端生成的耗时)、以及兼容性(不同浏览器对 Canvas 或 Image 标签的处理差异)。
你需要明白,所谓的“在线使用”,本质上是Web 端与移动端(App/浏览器)的交互桥梁。这个桥梁一旦断裂,用户体验就是灾难。
标准答法:分层拆解回答逻辑
面对“请实现一个网页扫一扫功能”这种问题,不要直接掏代码。先说思路,再给方案。
第一层:技术选型
我会先问清楚场景。如果是高频、低数据量的场景(比如点赞、签到),我倾向于纯前端生成。如果是涉及敏感数据或需要审计的场景(比如支付、登录),我会采用后端生成签名 URL 的方式。
第二层:核心组件
前端通常使用 qrcode.js 或 qr-code-styling 等轻量级库。关键点在于,我们生成的不是“图片”,而是“数据”。二维码本质上是二进制数据的编码形式。
第三层:交互流程
前端请求后端获取 codeId。
后端生成唯一标识,存入 Redis(设置过期时间,比如 60 秒)。
前端拿到 codeId,拼接成 URL,生成二维码。
手机扫码,打开 URL,后端校验 codeId 是否有效,执行后续业务。
第四层:异常处理
这是加分项。如果二维码过期了怎么办?如果前端生成失败了怎么办?如果网络抖动导致 Token 失效怎么办?这些才是面试官真正想听的。
代码实现:从 0 到 1 的实战代码
下面给出一套可以直接跑在 Vue3 + Node.js (Express) 环境下的最小可行方案。这段代码避开了常见的坑,特别是跨域和过期校验。
后端:Node.js (Express + Redis)
const express = require('express');
const redis = require('redis');
const crypto = require('crypto');
const app = express();
const client = redis.createClient({
url: 'redis://localhost:6379'
});
app.use(express.json());
// 1. 获取二维码 Token
app.get('/api/qr/generate', async (req, res) = {
try {
// 生成唯一的随机字符串,避免可预测
const token = crypto.randomBytes(16).toString('hex');
// 存入 Redis,设置 60 秒过期
// 注意:这里存储的可以是简单的 pending 状态,或者用户 ID
await client.set(`qr:token:${token}`, 'pending', 'EX', 60);
res.json({
success: true,
data: {
// 前端需要这个 URL 来生成二维码
qrUrl: `https://yourdomain.com/scan?token=${token}`
}
});
} catch (err) {
res.status(500).json({ success: false, message: 'Server Error' });
}
});
// 2. 手机扫码后的回调接口(模拟)
app.get('/api/qr/check', async (req, res) = {
const token = req.query.token;
if (!token) {
return res.status(400).json({ success: false, message: 'Missing token' });
}
try {
// 检查 Token 是否存在且未过期
const status = await client.get(`qr:token:${token}`);
if (!status) {
// 状态:1. 从未存在 2. 已过期
return res.json({
success: false,
status: 'expired_or_invalid',
message: '二维码已失效,请刷新重试'
});
}
if (status === 'scanned') {
// 状态:已经被扫过,防止重复提交
return res.json({
success: false,
status: 'already_scanned',
message: '该二维码已被使用'
});
}
// 更新状态为已扫描
await client.set(`qr:token:${token}`, 'scanned', 'EX', 60);
// 这里执行你的业务逻辑,比如绑定设备、登录等
console.log(`Token ${token} was successfully scanned.`);
return res.json({
success: true,
status: 'success',
message: '扫码成功'
});
} catch (err) {
res.status(500).json({ success: false, message: 'Check Error' });
}
});
app.listen(3000, () = console.log('Server running on port 3000'));
前端:Vue3 + qrcode.js
template
div class=qr-container
div v-if=loading加载中.../div
div v-else-if=error class=error-msg{{ error }} button @click=refresh重试/button/div
div v-else
div id=qrcode style=width: 200px; height: 200px;/div
p请使用手机扫描上方二维码/p
!-- 轮询检查状态,模拟实时反馈 --
div v-if=isScanning class=status等待扫码.../div
div v-if=scanSuccess class=success✅ 扫码成功!/div
/div
/div
/template
script
import { onMounted, ref, onUnmounted } from 'vue';
import QRCode from 'qrcode';
export default {
setup() {
const loading = ref(true);
const error = ref('');
const isScanning = ref(false);
const scanSuccess = ref(false);
let pollInterval = null;
const generateQR = async () = {
loading.value = true;
error.value = '';
scanSuccess.value = false;
try {
const res = await fetch('/api/qr/generate');
const data = await res.json();
if (data.success) {
// 关键:前端只负责渲染 URL,不处理业务逻辑
await QRCode.toCanvas(document.getElementById('qrcode'), data.data.qrUrl, {
width: 200,
height: 200,
// 增加容错等级,防止打印模糊导致扫不出
errorCorrectionLevel: 'H'
});
loading.value = false;
isScanning.value = true;
startPolling(data.data.qrUrl);
} else {
throw new Error(data.message || '生成失败');
}
} catch (e) {
error.value = '网络错误,请重试';
loading.value = false;
}
};
const startPolling = (url) = {
// 简单轮询,生产环境建议用 WebSocket 或 SSE
pollInterval = setInterval(async () = {
const res = await fetch(url); // 注意:这里 URL 包含 token
const data = await res.json();
if (data.status === 'success') {
scanSuccess.value = true;
isScanning.value = false;
stopPolling();
} else if (data.status === 'expired_or_invalid') {
error.value = '二维码已过期';
isScanning.value = false;
stopPolling();
}
}, 2000);
};
const stopPolling = () = {
if (pollInterval) clearInterval(pollInterval);
};
onMounted(generateQR);
onUnmounted(stopPolling);
return { loading, error, isScanning, scanSuccess, refresh: generateQR };
}
};
/script
代码解析与避坑:
errorCorrectionLevel: 'H':这是很多教程漏掉的。H 级容错率高达 30%,意味着即使二维码被遮挡一部分或者打印质量差,依然能扫出来。在移动端体验中,这能减少 20% 的“扫不出来”投诉。
轮询 vs WebSocket:上面的代码用了轮询(Polling),这是为了代码简洁。在实际高并发场景中,强烈建议使用 Server-Sent Events (SSE)。轮询会浪费大量带宽,且实时性差。SSE 是单向流,服务端有变化立刻推给前端,完美契合“扫码成功”这种一次性事件。
Token 安全:不要把敏感信息直接放在 URL 里。上面的 token 是随机的,但如果你的 URL 里直接带了 userId,一旦 URL 被日志记录或分享,用户隐私就泄露了。一定要用后端生成的随机 ID 做映射。
追问与延伸:面试官的“杀手锏”
当你给出上述方案后,面试官通常会抛出以下三个问题。答不上来,前面的努力就白费了。
Q1:如果用户刷新页面,二维码重新生成,之前扫了一半的状态怎么办?
答:这是一个幂等性问题。
方案 A(简单):刷新即失效。前端刷新时,通知后端将旧 Token 标记为 cancelled。手机扫到旧二维码时,提示“二维码已刷新”。
方案 B(复杂):保留上下文。如果业务允许(比如登录),可以允许短时间内(如 10 秒内)新旧 Token 共存,或者通过 SessionID 关联,确保同一个用户会话内的二维码变更是可控的。
核心考点:你是否考虑了状态机的流转?(Pending - Scanned - Expired / Cancelled)。
Q2:高并发下,Redis 压力很大,怎么优化?
答:
本地缓存:对于热点 Token,可以先在 Node.js 内存中缓存 1-2 秒,减少 Redis 查询次数。
读写分离:查询状态用从库,写入状态用主库。
合并查询:如果前端轮询频率高,可以将多个状态的检查合并成一个批量接口,减少网络开销。
降级策略:如果 Redis 挂了,不要直接 500 错误,可以降级为“本地内存缓存 + 短过期时间”,保证核心业务可用,虽然数据可能不一致,但系统不崩。
Q3:如何防止二维码被恶意批量生成,导致资源耗尽?
答:
限流:对 /api/qr/generate 接口做 IP 限流和 User-Id 限流。比如每个用户每分钟最多生成 10 个。
验证码:如果检测到异常高频,触发图形验证码。
资源池:预先生成一批 Token 放入 Redis 列表,用户获取时直接 POP,生成失败时再异步补充。这样可以削峰填谷。
记忆口诀:四步走,稳过面试
为了方便你在面试现场快速组织语言,我把整个流程浓缩成四句话。你可以把它当作你的“答题骨架”:
选型看场景,前端后端分清楚:
轻量级用前端生成,高安全用后端 Token。
状态要闭环,Redis 存状态:
Pending、Scanned、Expired,状态机必须完整。
通信选 SSE,轮询仅兜底:
强调实时性,展示你对 WebSocket/SSE 的了解。
安全加限流,防刷保稳定:
提到 IP 限流、Token 随机性、容错等级 H。
实战经验补充:
我在之前的项目中,遇到过一次线上事故。因为前端没有处理 QRCode 库加载失败的情况,导致部分低端安卓机用户看到空白。后来我们加了一个 onerror 回调,提示用户“请使用最新版浏览器”,并提供了“复制链接”的兜底方案。这个细节,在面试中提到,会让面试官觉得你真正做过项目,而不是背八股文。
另外,关于官方文档,建议你去查阅 MDN Web Docs 中关于 Image 和 Canvas 的兼容性章节,以及 IETF 的 QR Code 规范(RFC 3769)。在面试中随口提一句“根据 RFC 3769 规范,QR Code 的容错等级分为 L、M、Q、H”,瞬间就能拉开与其他候选人的差距。
你在项目里踩过这个坑吗?比如扫码后页面跳转异常,或者二维码在某些手机上扫不出来?评论区聊聊,看看咱们怎么一起把这个“扫不完”的坑填平。