前端实现微信扫一扫的思路
在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。
tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步上篇 🎁前端如何打开摄像头拍照,这是你阅读本篇的必需前置知识。
一. 效果预览
这里先简单放一下整体界面效果,接下来带大家一步一步分析其中的功能如何实现。
本篇将重点讲解多张二维码识别的处理场景。
二. 简单了解二维码
现在流行使用的二维码是 qrcode,其中 qr 两个字母其实就是 quick response 的缩写,简单来说就是快速响应的意思。三个角用来定位,黑点表示二进制的1,白色点代表0。 (这里感兴趣可以自行了解) 它的本质其实就是将一个地址链接利用某种约定好的规则隐藏到一个图片当中,
我们可以利用 chrome 自带的创建当前网站二维码的功能快速体验一下。
你可以用手机自带的二维码扫码软件扫一下这个二维码,它将会将你引导到我掘金的个人主页。
细心的你可能会发现二维码下面已经给你提示了你准备保存的链接地址,现在你观察一下浏览器地址栏是否正对应下面这个地址呢?
三. 实现扫码本地图片功能
四. 理清思路
13.那这不巧了吗,scan 的返回值 rect 恰好就有这几个值。
话不多说,马上开始实践。⛽️
五. 处理存在多张二维码的图片
六. 弹出可以点击的小蓝块
有了坐标信息和位置信息,并且我们的 canvas 和 img 元素的坐标轴系统是一一对应的,那么我们就可以写一个函数来遍历这个 resultMap,然后根据位置信息在 img 元素所在的 div 上打印出我们想要的样式。
首先在 img 元素外面包一层 div,打上 ref 叫做 imgWrapper 。因为之后我们要用它当作小蓝块的定位元素,所以先设置 position:relative。
绘画代码如下,都是基础的方法,不再过多赘述。
//多个二维码时添加动态小蓝点function draw() {resultMap.forEach((rect, link) => {if (!imgWrapper.value) return;const dom = document.createElement("div");const { x, y, width, height } = rect;const _x = (x || 0) + width / 2 - 20;const _y = (y || 0) + height / 2 - 20;dom.className = "blue-chunk";dom.style.width = "40px";dom.style.height = "40px";dom.style.background = "#2ec1cc";dom.style.position = "absolute";dom.style.zIndex = "9999999";dom.style.top = _y + "px";dom.style.left = _x + "px";dom.style.color = "#fff";dom.style.textAlign = "center";dom.style.borderRadius = "100px";dom.style.borderBlockColor = "#fff";dom.style.borderColor = "unset";dom.style.borderRightStyle = "solid";dom.style.borderWidth = "3px";dom.style.animation = "scale-animation 2s infinite";dom.addEventListener("click", () => {console.log(link);});imgWrapper.value.appendChild(dom);});}然后再 for 循环以后开始绘画小蓝块。
让我们预览一下现在的效果:
让我们测试一下相对应的点击事件
七. 源码
<script lang="ts" setup>import { ref, onMounted } from "vue";import { scan } from "qr-scanner-wechat";const wrapper = ref<HTMLDivElement>();const videoEl = ref<HTMLVideoElement>();async function checkCamera() {const navigator = window.navigator.mediaDevices;const devices = await navigator.enumerateDevices();if (devices) {const stream = await navigator.getUserMedia({audio: false,video: {width: 300,height: 300,// facingMode: { exact: "environment" }, //强制后置摄像头facingMode: "user", //前置摄像头},});if (!videoEl.value) return;videoEl.value.srcObject = stream;// videoEl.value.play();}}function shoot() {if (!videoEl.value || !wrapper.value) return;const canvas = document.createElement("canvas");canvas.width = videoEl.value.videoWidth;canvas.height = videoEl.value.videoHeight;//拿到 canvas 上下文对象const ctx = canvas.getContext("2d");ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);wrapper.value.appendChild(canvas);}const src = ref("");const imgEl = ref<HTMLImageElement>();const resultMap = new Map();const imgWrapper = ref<HTMLDivElement>();async function getImageFromLocal(e: Event) {const inputEl = e.target as HTMLInputElement;if (!inputEl) return;if (!inputEl.files?.length) return;const image = inputEl.files[0];const url = URL.createObjectURL(image);src.value = url;const temCanvas = document.createElement("canvas");temCanvas.width = 300;temCanvas.height = 300;const ctx = temCanvas.getContext("2d", { willReadFrequently: true });if (!imgEl.value) return;imgEl.value.onload = async () => {if (!ctx) return;ctx.drawImage(imgEl.value!, 0, 0, 300, 300);wrapper.value?.appendChild(temCanvas);ctx.fillStyle = "black";for (let i = 0; i < 5; i++) {const result = await scan(temCanvas);console.log("result", result);if (!result?.rect || !result.text) continue;resultMap.set(result.text, result.rect);const { x, y, height, width } = result.rect;ctx.fillRect(x, y, width, height);}draw();};}//多个二维码时添加动态小蓝点function draw() {resultMap.forEach((rect, link) => {if (!imgWrapper.value) return;const dom = document.createElement("div");const { x, y, width, height } = rect;const _x = (x || 0) + width / 2 - 20;const _y = (y || 0) + height / 2 - 20;dom.className = "blue-chunk";dom.style.width = "40px";dom.style.height = "40px";dom.style.background = "#2ec1cc";dom.style.position = "absolute";dom.style.zIndex = "9999999";dom.style.top = _y + "px";dom.style.left = _x + "px";dom.style.color = "#fff";dom.style.textAlign = "center";dom.style.borderRadius = "100px";dom.style.borderBlockColor = "#fff";dom.style.borderColor = "unset";dom.style.borderRightStyle = "solid";dom.style.borderWidth = "3px";dom.style.animation = "scale-animation 2s infinite";dom.addEventListener("click", () => {console.log(link);});imgWrapper.value.appendChild(dom);});}onMounted(() => {checkCamera();});</script><template><div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center"><video ref="videoEl" /><div ref="imgWrapper" class="relative"><imgref="imgEl":src="src"alt="qrcode"class="w-300px h-300px object-contain"/></div><div><input @change="getImageFromLocal" type="file" accept="image/*" /></div></div></template>
八.总结
本篇文章的关键点就是讲解了我在实现处理多张二维码的场景时的思路,利用 canvas 遮挡识别过的二维码这个思路是 pbk-bin 大佬最先想到的,在实现这个需求以后还是很感叹这个思路的巧妙。👏
再次特别感谢pbk-bin🎁~
如果文章对你有帮助,不妨赠人玫瑰,手有余香,预计将会在下篇更新较为完整的微信扫一扫界面和功能。
作者:韩振方
链接:https://juejin.cn/post/7290813210276724771