最近接到一个非常有意思的需求,需要在前端实现一个仿 微信扫一扫 的功能。这其中,调用摄像头并实现拍照是核心基础。本文将详细讲解如何利用现代Web API在Vue3项目中实现这一功能。
一、检查设备:window.navigator
想要调用设备的摄像头,首先需要确认当前环境是否支持。浏览器全局对象 window 身上有一个 navigator 属性,它的 mediaDevices 属性是我们实现功能的关键。
我们可以先设计一个 checkCamera 函数,在页面初始化时执行,用于检查媒体设备。

图1:用于检查摄像头设备的代码片段
在控制台查看这个对象,你可能会发现它只有一个值为 null 的 ondevicechange 属性。别担心,真正有用的方法在其原型(__proto__)上。

图2:浏览器控制台中的MediaDevices对象
展开其原型属性,请注意 enumerateDevices 和 getUserMedia 这两个方法,它们是我们本章节的主角。

图3:MediaDevices原型上的关键方法
在这一步,我们只需用 enumerateDevices 函数检查当前有哪些媒体设备。该函数返回一个 Promise,我们可以用 async/await 简化代码。

图4:调用enumerateDevices函数的异步代码

图5:控制台输出的音频、视频输入设备列表
如上图所示,设备列表中包含 videoinput 类型的设备,这证明当前环境有可用的摄像头,我们可以继续进行下一步。
二、获取摄像头流数据
接下来,我们将使用 navigator.mediaDevices.getUserMedia() 这个核心API。它接收一个配置对象作为参数,我们可以预设视频的宽度、高度以及摄像头朝向。
这里我们重点关注 facingMode 属性。对于扫一扫功能,通常需要使用后置摄像头(environment),而前置摄像头则为 user。

图6:调用getUserMedia时传入的配置参数
当执行这个函数后,浏览器会弹出权限请求对话框。

图7:浏览器请求使用摄像头的权限提示
点击“允许”后,页面可能没有任何变化。这是因为 getUserMedia 仅仅返回了一个媒体流(MediaStream) 对象。可以这样理解:浏览器得到了使用摄像头的许可和原始数据流,但还不知道该把画面显示在哪里。
我们需要一个“显示器”来承载这个数据流,而原生的 <video> 标签正是最佳选择。
在Vue模板中创建一个 <video> 标签,并通过 ref 获取其DOM元素。

图8:在Vue模板中创建video元素并绑定ref
关键一步在于:将 getUserMedia 返回的流数据赋值给 video 元素的 srcObject 属性。这就相当于把数据线插到了显示器上。
注意: 是 video.srcObject,而非 video.src。

图9:获取流数据并赋值给video.srcObject
现在,你应该能在页面上看到摄像头捕获的实时画面了。

图10:摄像头画面成功在video标签中播放
三、截取当前画面(拍照)
我们添加一个按钮作为拍摄键,目标是点击时截取当前视频画面。

图11:包含拍摄按钮的界面
这里需要理解一个原理:尽管视频看起来是连续的,但浏览器渲染时其实是一帧一帧进行的。打开浏览器的 Performance 面板录制页面加载过程,可以看到渲染过程是由无数帧拼接而成的。

图12:Performance面板展示的逐帧渲染过程
同理,摄像头视频流也是由连续的帧构成。因此,拍照的本质就是:在按下按钮的瞬间,获取 video 标签当前显示的那一帧画面并保存。
实现这个功能,我们需要借助 <canvas> 的能力。别担心,这里只会用到其基础功能。
首先,动态创建一个 <canvas> 元素,并将其宽高设置为与 video 元素的视频宽高一致。

图13:创建与video等尺寸的canvas元素
接下来是重点:调用 canvas 的 getContext(‘2d’) 方法,获取一个2D渲染上下文对象。你可以简单理解为,这个方法为 canvas “激活”了绘图能力,并返回一个包含各种绘图方法的工具对象。

图14:Canvas API中getContext方法的文档说明
在这个上下文对象 ctx 身上,我们只需使用 drawImage 这一个方法。

图15:CanvasRenderingContext2D.drawImage()方法说明
drawImage 有多种重载,我们使用五参数的版本:drawImage(image, dx, dy, dWidth, dHeight)。

图16:drawImage方法的五参数用法
dx, dy:指在画布上放置图像的起点坐标(左上角),类似于设置 margin-left 和 margin-top。

图17:画布坐标系及dx,dy参数示意
dWidth, dHeight:指在画布上绘制图像的宽度和高度。
image:可以是多种图像源,包括 <video> 元素。当传入 <video> 时,它会自动捕获该元素的当前帧。
所以,完整的拍摄函数 shoot 如下:
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);//将 canvas 投到页面上
}
现在来测试一下拍照效果。

图18:点击按钮,成功将当前帧画面绘制到canvas并显示
四、完整示例代码
以下是整合了上述所有步骤的 Vue 3 单文件组件示例代码:
<script lang="ts" setup>
import { ref, onMounted } from "vue";
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);
}
onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center">
<video ref="videoEl" />
<div
@click="shoot"
class="w-100px leading-100px text-center bg-black text-30px"
>
拍摄
</div>
</div>
</template>
五、总结
实现前端拍照的整体思路非常清晰:首先通过 WebRTC 的 getUserMedia API 获取摄像头媒体流,并将其绑定到 <video> 标签进行实时预览。当需要拍照时,利用 <canvas> 的 getContext(‘2d’) 获取绘图上下文,再使用 drawImage 方法捕获 <video> 标签的当前帧并绘制到画布上,从而得到静态图片。
这仅仅是仿扫一扫功能的第一步。在此基础上,你可以进一步处理 canvas 上的图像数据,例如使用诸如 jsQR 等库进行二维码识别,或者将图片数据上传至服务器。希望这篇在 云栈社区 分享的教程能帮助你理解前端操作摄像头的核心原理。