引入js-sdk,调用扫描二维码和获取位置功能


前言

vue3项目中需要扫描二维码,使用了几个插件,但是各有各的不好用。所以决定使用微信强大的扫描二维码功能,期间也踩了很多坑,决定详细记录。


一、jssdk

1.JSSDK使用步骤

官方文档——JS-SDK说明文档

步骤一:绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“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):

官方文档——获取 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}&timestamp=${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相比较而言比较好。两个插件都不能识别相册,只能扫描。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值