前言
vue3项目中需要扫描二维码,使用了几个插件,但是各有各的不好用。所以决定使用微信强大的扫描二维码功能,期间也踩了很多坑,决定详细记录。
一、jssdk
1.JSSDK使用步骤
步骤一:绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
就是项目部署后的请求域名。
步骤二:引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js
如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.6.0.js (支持https)。
备注:支持使用 AMD/CMD 标准模块加载方法加载
在 index.html 中引入
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
步骤三:通过config接口注入权限验证配置
这一步需要做的事情比较多。
1.参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
2.用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
3.获取签名signature
步骤四:通过ready接口处理成功验证
wx.ready(function(){
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});
步骤五:通过error接口处理失败验证
wx.error(function(res){
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});
2.代码实例
index.html
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
App.vue
<script >
if (typeof window.entryUrl === 'undefined' || window.entryUrl === '') {
window.entryUrl = location.href.split('#')[0]
}
</script>
创建 wxsdk.js
import { getwxticket } from '@/util/index' // 此为后端请求接口,直接返回的是jsapi_ticket。也就是后端帮我调用了前两个接口。前端自己也可以写,但是把appsecret放在前端请求不太安全。
// 接口1
// GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
// 接口2
// GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
var sha1 = require("sha1"); // 获取签名的加密算法,安装: npm install sha1 --save
var nonstr = 'xxxxxxtest'// 随机,但搜索过程中发现其他使用者建议以test结尾,不知道是因为要求string型还是因为要求test结尾
var timestamp = 'xxxxxx' // 随机
var url = /(Android)/i.test(navigator.userAgent) ? location.href.split('#')[0] : window.entryUrl; // 判断是ios还是安卓,请求路径不同
var str = ''
var jsApiList = ["scanQRCode", "getLocation"]; //需要使用的微信JS接口列表
export default {
async wxconfig() {
let response = await getwxticket();
str = `jsapi_ticket=${response.jsapi_ticket}&noncestr=${nonstr}×tamp=${timestamp}&url=${url}`
let wxconfig = {
debug: false, // 开启调试模式,这个很有用,可以看到详细bug
appId: 'xxxxxxxx', // 必填,企业号的唯一标识,此处填写企业号corpid
timestamp: timestamp, // 必填,生成签名的时间戳
nonceStr: nonstr, // 必填,生成签名的随机串
signature: sha1(str), // 必填,签名,见附录1
jsApiList, // 必填,需要使用的JS接口列表
};
wx.config(wxconfig);
},
//封装微信JSSDK方法,采用闭包函数的原理将res值抛出到回调函数中
// 扫描二维码
scanQRcode({ success, fail, needResult = 1 }) {
this.wxconfig();
wx.ready(() => {
wx.scanQRCode({
needResult, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
// scanType: ["qrCode", "barCode"], // 可以指定扫二维码还是一维码,默认二者都有
success: res => {
// {errMsg: "scanQRCode:ok", resultStr: ""} 当needResult 为 1 时,resultStr扫码返回的结果
// {errMsg: "scanQRCode:ok"}当needResult 为 0 时,会导致页面跳转
//扫码成功,抛出扫码结果
success(res);
},
});
});
wx.error(res => {
console.log("config fail:", res);
//config fail,抛出失败原因
fail(res);
});
},
// 获取位置
getLocation({ success, fail }) {
this.wxconfig();
wx.ready(() => {
wx.getLocation({
type: 'wgs84',
success: res => {
// {errMsg: "scanQRCode:ok", resultStr: ""} 当needResult 为 1 时,resultStr扫码返回的结果
// {errMsg: "scanQRCode:ok"}当needResult 为 0 时,会导致页面跳转
//扫码成功,抛出扫码结果
success(res);
},
});
});
wx.error(res => {
console.log("config fail:", res);
//config fail,抛出失败原因
fail(res);
});
},
};
页面中使用
<template>
<el-button class="scan-btn wid100" type="primary" @click="getWX">点击扫码</el-button>
<el-dialog v-model="isShowDialog" title="位置信息" :show-close="false" :close-on-click-modal="false" :close-on-press-escape="false" width="18.75rem" align-center class="mark1-dialog">
<span style="margin-bottom: 0.625rem;padding: 0 1rem;">经度:{{ localObj?.longitude}}</span>
<span style="margin-bottom: 0.625rem;padding: 0 1rem;">纬度:{{ localObj?.latitude}}</span>
<div class="confirm wid100" @click="isShowDialog = false">我知道了</div>
</el-dialog>
</template>
<script setup>
import wxjs from "./wxsdk";
import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
let localObj = ref(null)
let isShowDialog = ref(false)
const getWX = () => {
// 获取位置
wxjs.getLocation({
success: res => {
localObj.value = res;
isShowDialog.value = true
},
});
});
}
watch(() => [isShowDialog.value, localObj.value], () => {
if (!isShowDialog.value && localObj.value?.longitude) {
// 扫描二维码
wxjs.scanQRcode({
success: res => {
console.log(res.resultStr) // 获取扫描结果
},
});
}
}, { immediate: true, deep: true })
</script>
二、扫描插件使用
1.vue3-qr-reader
安装
npm install --save vue3-qr-reader
页面使用:
<template>
<div class="canvasBox">
<div class="box">
<qr-stream @decode="onDecode" class="container" @init="onInit" style="position: absolute;top: 0;left: 0;">
<div class="contentInner wid100 hei100"></div>
</qr-stream>
<div class="line"></div>
<div class="angle"></div>
</div>
<el-button circle class="back-btn" @click="router.back()">
<el-icon>
<ArrowLeft />
</el-icon>
</el-button>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { QrStream } from 'vue3-qr-reader';
import { useRouter } from 'vue-router'
import { useStore } from 'vuex';
let store = useStore();
let router = useRouter()
const { proxy } = getCurrentInstance()
let error = ref('')
const onDecode = (data) => {
if (data) {
// 获取扫描结果
console.log(data)
}
}
const onInit = async (promise) => {
try {
await promise
} catch (error) {
if (error.name === 'NotAllowedError') {
error.value = "ERROR: you need to grant camera access permission"
} else if (error.name === 'NotFoundError') {
error.value = "ERROR: no camera on this device"
} else if (error.name === 'NotSupportedError') {
error.value = "ERROR: secure context required (HTTPS, localhost)"
} else if (error.name === 'NotReadableError') {
error.value = "ERROR: is the camera already in use?"
} else if (error.name === 'OverconstrainedError') {
error.value = "ERROR: installed cameras are not suitable"
} else if (error.name === 'StreamApiNotSupportedError') {
error.value = "ERROR: Stream API is not supported in this browser"
} else if (error.name === 'InsecureContextError') {
error.value = `ERROR: Camera access is only permitted in secure context.
Use HTTPS or localhost rather than HTTP.`;
} else {
error.value = `ERROR: Camera error (${error.name})`;
}
console.log(error.value)
return true
}
}
</script>
<style lang='scss' scoped>
.canvasBox {
width: 100vw;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-image: linear-gradient(
0deg,
transparent 24%,
rgba(32, 255, 77, 0.1) 25%,
rgba(32, 255, 77, 0.1) 26%,
transparent 27%,
transparent 74%,
rgba(32, 255, 77, 0.1) 75%,
rgba(32, 255, 77, 0.1) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
rgba(32, 255, 77, 0.1) 25%,
rgba(32, 255, 77, 0.1) 26%,
transparent 27%,
transparent 74%,
rgba(32, 255, 77, 0.1) 75%,
rgba(32, 255, 77, 0.1) 76%,
transparent 77%,
transparent
);
background-size: 3rem 3rem;
background-position: -1rem -1rem;
z-index: 10;
background-color: #1110;
.box {
width: 20.625rem;
height: 20.625rem;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: 0.0625rem solid rgba(0, 255, 51, 0.2);
z-index: 11;
.line {
height: calc(100% - 0.125rem);
width: 100%;
background: linear-gradient(
180deg,
rgba(0, 255, 51, 0) 43%,
#00ff33 211%
);
border-bottom: 0.1875rem solid #00ff33;
transform: translateY(-100%);
animation: radar-beam 2s infinite alternate;
animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);
animation-delay: 1.4s;
}
@keyframes radar-beam {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
}
.box:after,
.box:before,
.angle:after,
.angle:before {
content: "";
display: block;
position: absolute;
width: 3vw;
height: 3vw;
z-index: 12;
border: 0.1875rem solid transparent;
}
.box:after,
.box:before {
top: 0;
border-top-color: #00ff33;
}
.angle:after,
.angle:before {
bottom: 0;
border-bottom-color: #00ff33;
}
.box:before,
.angle:before {
left: 0;
border-left-color: #00ff33;
}
.box:after,
.angle:after {
right: 0;
border-right-color: #00ff33;
}
.back-btn {
cursor: pointer;
position: absolute;
top: 1.25rem;
left: 1.25rem;
z-index: 10;
}
}
</style>
bug:
1.ios第一次进入页面,同意访问摄像头后不能立刻调起摄像头,第二次进入才正常使用摄像头。
2.二维码边界周围是黑色背景时,扫描困难,难以识别。
2.@zxing/library
安装
npm install @zxing/library
页面使用(根据其他使用者代码整理,但找不到地方了)
<template>
<div class="page-scan">
<div class="scan-box">
<video ref="video" id="video" class="scan-video" autoplay></video>
<div class="qr-scanner">
<div class="box">
<div class="line"></div>
<div class="angle"></div>
</div>
</div>
<div class="scan-tip">{{ scanTextData.tipMsg }}</div>
</div>
</div>
</template>
<script setup>
import { BrowserMultiFormatReader } from "@zxing/library";
import { ref, reactive, onMounted, getCurrentInstance, defineProps, watch, onUnmounted } from 'vue';
import { useRouter } from 'vue-router'
import { useStore } from 'vuex';
import { camera, idverify, addcheckreport } from '@/util/index'
let store = useStore();
let router = useRouter()
const { proxy } = getCurrentInstance()
let scanTextData = ref({
codeReader: null,
tipMsg: "识别二维码",
// 这个,就是当前调用的摄像头的索引,为什么是6,我会在后面说的 华为手机是鸿蒙系统有8个摄像头
num: 5,
// 这个就是扫描到的摄像头的数量
videoLength: ""
})
let hasBind = ref(false)
let props = defineProps({
show: {
type: Boolean,
default: false
}
})
onMounted(() => {
scanTextData.value.codeReader = new BrowserMultiFormatReader();
openScan(); // 打开摄像头
})
watch(() => props.show, () => {
if (!val) {
// 关闭摄像头
if (!document.getElementById("video")) {
alert("请授权");
return;
}
let thisVideo = document.getElementById("video");
thisVideo.srcObject.getTracks()[0].stop();
scanTextData.value.codeReader.reset();
} else {
if (scanTextData.value.codeReader === null) {
scanTextData.value.codeReader = new BrowserMultiFormatReader();
}
openScan();
}
})
const openScan = async () => {
scanTextData.value.codeReader
.getVideoInputDevices()
.then(videoInputDevices => {
// 默认获取第一个摄像头设备id
let firstDeviceId = videoInputDevices[0].deviceId;
console.log(
"手机摄像头的数量",
videoInputDevices.length,
videoInputDevices
);
// 获取第一个摄像头设备的名称
const videoInputDeviceslablestr = JSON.stringify(
videoInputDevices[0].label
);
if (videoInputDevices.length > 1) {
// 华为手机有6个摄像头,前三个是前置,后三个是后置,第6个摄像头最清晰
if (videoInputDevices.length > 5) {
firstDeviceId = videoInputDevices[5].deviceId;
} else {
// 判断是否后置摄像头
if (videoInputDeviceslablestr.indexOf("back") > -1) {
firstDeviceId = videoInputDevices[0].deviceId;
} else {
firstDeviceId = videoInputDevices[1].deviceId;
}
}
}
decodeFromInputVideoFunc(firstDeviceId);
})
.catch(err => {
console.error(err);
});
}
const decodeFromInputVideoFunc = (firstDeviceId) => {
scanTextData.value.codeReader.reset();
scanTextData.value.codeReader.decodeFromInputVideoDeviceContinuously(
firstDeviceId,
"video",
(result, err) => {
if (result && result.text) {
handleResult(result.text);
}
if (err && !err) {
console.log(err);
// this.$toast.fail(err);
}
}
);
}
const handleResult = async (data) => {
// 获取扫码结果
console.log(data);
}
onUnmounted(() => {
scanTextData.value.codeReader.reset(); // 重置摄像头
})
</script>
<style lang="scss" scoped>
.scan-box {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100vw;
background-image: linear-gradient(
0deg,
transparent 24%,
rgba(32, 255, 77, 0.1) 25%,
rgba(32, 255, 77, 0.1) 26%,
transparent 27%,
transparent 74%,
rgba(32, 255, 77, 0.1) 75%,
rgba(32, 255, 77, 0.1) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
rgba(32, 255, 77, 0.1) 25%,
rgba(32, 255, 77, 0.1) 26%,
transparent 27%,
transparent 74%,
rgba(32, 255, 77, 0.1) 75%,
rgba(32, 255, 77, 0.1) 76%,
transparent 77%,
transparent
);
background-size: 48px 48px;
background-position: -16px -16px;
}
.scan-video {
height: 100vh;
width: 100vw;
object-fit: cover;
}
.qr-scanner .box {
width: 13.3125rem;
height: 13.3125rem;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: 1.6px solid rgba(0, 255, 51, 0.2);
/* background: url('http://resource.beige.world/imgs/gongconghao.png') no-repeat center center; */
}
.qr-scanner .line {
height: calc(100% - 0.125rem);
width: 100%;
background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%);
border-bottom: 0.1875rem solid #00ff33;
transform: translateY(-100%);
animation: radar-beam 2s infinite alternate;
animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);
animation-delay: 1.4s;
}
.qr-scanner .box:after,
.qr-scanner .box:before,
.qr-scanner .angle:after,
.qr-scanner .angle:before {
content: "";
display: block;
position: absolute;
width: 3vw;
height: 3vw;
border: 3.2px solid transparent;
}
.qr-scanner .box:after,
.qr-scanner .box:before {
top: 0;
border-top-color: #00ff33;
}
.qr-scanner .angle:after,
.qr-scanner .angle:before {
bottom: 0;
border-bottom-color: #00ff33;
}
.qr-scanner .box:before,
.qr-scanner .angle:before {
left: 0;
border-left-color: #00ff33;
}
.qr-scanner .box:after,
.qr-scanner .angle:after {
right: 0;
border-right-color: #00ff33;
}
@keyframes radar-beam {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
.scan-tip {
width: 100vw;
text-align: center;
margin-bottom: 5vh;
color: white;
font-size: 5vw;
position: absolute;
bottom: 3.125rem;
left: 0;
color: #fff;
}
.page-scan {
overflow-y: hidden;
}
</style>
bug:
这个比上一个插件好用,但是黑色边缘还是识别困难。
总结
在vue项目中使用 微信jssdk 的确麻烦,各种查错试错。但是真的很流畅,还能识别相册二维码。但是只能在微信浏览器中使用,也就是只能在微信打开。其他浏览器访问地址没有反应。
vue3-qr-reader 代码简单,但是第一次打不开导致用户体验很差。@zxing/library相比较而言比较好。两个插件都不能识别相册,只能扫描。