uni-app是一个使用Vue.js开发前端应用的框架,编写的应用可以在Android、iOS、Web以及各种小程序上运行。
Dynamsoft Barcode Reader是使用C++编写的高性能二维码/条码扫描SDK,可以在Android、iOS和Web等各种平台运行。
下面我们会讨论如何在uni-app中集成Dynamsoft Barcode Reader来进行扫码。
针对不同平台,在uni-app里集成Dynamsoft Barcode Reader有多种方式。
- Web:可以直接使用Dynamsoft Barcode Reader的JavaScript版
- 原生应用:编写原生语言插件或者UTS插件使用Dynamsoft Barcode Reader的Android版和iOS版,或者在WebView中使用JavaScript版。本文会使用WebView
- 小程序:使用相机插件打开相机,传送视频帧给服务端解码
集成到Web
安装依赖
新建一个uni-app后,我们需要初始化npm项目以导入第三方库:
npm init -y
之后安装Dynamsoft Barcode Reader:
npm install dynamsoft-javascript-barcode
建立一个二维码扫描组件
-
新建一个components目录,在里面建立一个新的组件,命名为
QRCodeScannerWeb.vue
。 -
在template中添加扫描界面,包含相机视频容器、相机选择器、分辨率选择器等元素。
<div ref="elRefs" class="component-barcode-scanner"> <svg class="dce-bg-loading" viewBox="0 0 1792 1792"> <path d="M1760 896q0 176-68.5 336t-184 275.5-275.5 184-336 68.5-336-68.5-275.5-184-184-275.5-68.5-336q0-213 97-398.5t265-305.5 374-151v228q-221 45-366.5 221t-145.5 406q0 130 51 248.5t136.5 204 204 136.5 248.5 51 248.5-51 204-136.5 136.5-204 51-248.5q0-230-145.5-406t-366.5-221v-228q206 31 374 151t265 305.5 97 398.5z"></path> </svg> <svg class="dce-bg-camera" viewBox="0 0 2048 1792"> <path d="M1024 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z"> </path> </svg> <div class="dce-video-container"></div> <div class="dce-scanarea"> <div class="dce-scanlight"></div> </div> <div class="div-select-container"> <select class="dce-sel-camera"></select> <select class="dce-sel-resolution"></select> </div> </div>
-
在
onMounted
生命周期中,初始化Dynamsoft Barcode Reader并开启相机进行扫码。如果完成一帧的处理,就触发scanned
的事件,把扫码结果传递给父组件。这里定义了一个license
属性用于激活Dynamsoft Barcode Reader。可以访问这里申请一个license。props: ['license'], setup(props,context){ const pScanner = ref(null); const elRefs = ref(null); onMounted(async () => { try { if (BarcodeScanner.isWasmLoaded() === false) { BarcodeScanner.license = props.license ?? "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; BarcodeScanner.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.21/dist/"; } const scanner = await (pScanner.value = BarcodeScanner.createInstance()); await scanner.setUIElement(elRefs.value); scanner.onFrameRead = (results) => { for (let result of results) { console.log(result.barcodeText); } context.emit("scanned", results); }; await scanner.open(); } catch (ex) { let errMsg = ex.message||ex; if (errMsg.includes("network connection error")) { errMsg = "Failed to connect to Dynamsoft License Server: network connection error. Check your Internet connection or contact Dynamsoft Support (support@dynamsoft.com) to acquire an offline license."; } alert(errMsg); } }); onBeforeUnmount(async () => { if (pScanner.value) { (await pScanner.value).destroyContext(); console.log('BarcodeScanner Component Unmount'); } }); return { elRefs } }
-
组件卸载时,销毁Dynamsoft Barcode Reader。
onBeforeUnmount(async () => { if (pScanner.value) { (await pScanner.value).destroyContext(); console.log('BarcodeScanner Component Unmount'); } });
-
在
index.vue
中,使用这个扫码组件,扫到码后关闭扫描界面并显示结果。<template> <view class="content"> <button @click="startScan">Start Scanning</button> <view class="fullscreen" v-if="scanning"> <QRCodeScannerWeb @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScannerWeb> </view> <view v-for="(result,index) in barcodeResults"> <view>{{ (index+1) + ". " + result.barcodeFormatString + ": " + result.barcodeText }}</view> </view> </view> </template> <script> import QRCodeScannerWeb from "../../components/QRCodeScannerWeb.vue"; import { ref } from "vue"; export default { components: { QRCodeScannerWeb }, setup(){ const scanning = ref(false); const barcodeResults = ref([]); const startScan = () => { scanning.value = true; } const scanned = (results) => { if (results.length>0) { barcodeResults.value = results; scanning.value = false; } } return { startScan, scanned, barcodeResults, scanning } }, data() { return { } }, onLoad() { }, methods: { } } </script> <style> .content { display: flex; flex-direction: column; align-items: center; justify-content: center; } .fullscreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } </style>
集成到原生应用
我们需要编写一个网页应用,在WebView中运行,通过postMessage
接口和WebView通讯,传递扫描结果。
编写一个网页扫码应用
-
新建一个html文件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>QR Code Scanner in uniapp</title> <style> </style> </head> <body> </script> </body> </html>
-
在head中包含Dynamsoft Barcode Reader和Dynamsoft Camera Enhancer的库。
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.4/dist/dce.js"></script> <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.20/dist/dbr.js"></script>
-
添加一个触发扫描的按钮、扫描界面的容器和一个状态条。
<div class="app"> <div class="controls"> <button class="scanButton" onclick="startScan();">Scan From Camera</button> </div> <div class="status"></div> </div> <div class="scanner"> </div>
-
在页面加载后,如果按下了扫描的按钮或者
startScan
的URL参数为true
,用Dynamsoft Camera Enhancer打开相机,之后用Dynamsoft Barcode Reader处理视频帧解码。let enhancer; let reader; let interval; let processing = false; let hasCamera = true; init(); async function init(){ hideControls(); updateStatus("Initializing..."); try { await requestCameraPermission(); }catch (e) { console.log(e); hasCamera = false; document.getElementsByClassName("scanButton")[0].style.display = "none"; alert("No camera detected."); } if (getUrlParam("startScan") !== "true") { revealControls(); } if (getUrlParam("license")) { Dynamsoft.DBR.BarcodeScanner.license = getUrlParam("license"); }else{ Dynamsoft.DBR.BarcodeScanner.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; // one-day trial } reader = await Dynamsoft.DBR.BarcodeScanner.createInstance(); if (hasCamera) { enhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(); enhancer.on("played", (playCallbackInfo) => { console.log("camera started"); startProcessingLoop(); }); enhancer.on("cameraClose", playCallBackInfo => { stopScan(); }); await enhancer.setUIElement(Dynamsoft.DCE.CameraEnhancer.defaultUIElementURL); let container = document.getElementsByClassName("scanner")[0]; container.appendChild(enhancer.getUIElement()); if (getUrlParam("startScan") === "true") { startScan(); } updateStyleForiOS(); } updateStatus(""); } function updateStyleForiOS(){ document.getElementsByClassName("dce-sel-camera")[0].parentElement.style = "position: absolute;left: 0;top: 0;top: env(safe-area-inset-top);"; document.getElementsByClassName("dce-btn-close")[0].style = "position: absolute;right: 0;top: 0;top: env(safe-area-inset-top);"; } function hideControls(){ document.getElementsByClassName("controls")[0].style.display = "none"; } function revealControls(){ document.getElementsByClassName("controls")[0].style.display = ""; } function startProcessingLoop(isBarcode){ stopProcessingLoop(); interval = setInterval(captureAndDecode,100); // read barcodes } function stopProcessingLoop(){ if (interval) { clearInterval(interval); interval = undefined; } processing = false; } async function captureAndDecode() { if (!enhancer || !reader) { return } if (enhancer.isOpen() === false) { return; } if (processing == true) { return; } processing = true; // set processing to true so that the next frame will be skipped if the processing has not completed. let frame = enhancer.getFrame(); if (frame) { let results = await reader.decode(frame); if (results.length > 0) { updateStatus("Found "+results.length+((results.length>1)?" results":" result")); stopScan(); } processing = false; } }; function startScan(){ if (!enhancer || !reader) { alert("Please wait for the initialization of Dynamsoft Barcode Reader"); return; } document.getElementsByClassName("scanner")[0].classList.add("active"); enhancer.open(true); //start the camera } function stopScan(){ stopProcessingLoop(); enhancer.close(true); document.getElementsByClassName("scanner")[0].classList.remove("active"); revealControls(); } function updateStatus(info){ document.getElementsByClassName("status")[0].innerText = info; } async function requestCameraPermission() { const constraints = {video: true, audio: false}; const stream = await navigator.mediaDevices.getUserMedia(constraints); const tracks = stream.getTracks(); for (let i=0;i<tracks.length;i++) { const track = tracks[i]; track.stop(); // stop the opened camera } } function getUrlParam(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; }
下面我们还需要把扫码结果返回给WebView。
-
引入uni-app的webview js库。
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script> <script> document.addEventListener('UniAppJSBridgeReady', function() { uni.webView.getEnv(function(res) { console.log('当前环境:' + JSON.stringify(res)); }); // uni.webView.navigateTo(...) }); </script>
-
扫到码后,就发送结果给应用。
async function captureAndDecode() { //... if (frame) { let results = await reader.decode(frame); if (results.length > 0) { updateStatus("Found "+results.length+((results.length>1)?" results":" result")); stopScan(); sendBarcodeResults(results); } processing = false; } //... } function sendBarcodeResults(results){ const message = { data: { action: 'scanned', results: JSON.stringify(results) } } if (window.uni) { window.uni.postMessage(message); } }
新建一个基于WebView的扫码组件
-
建立一个新的组件,命名为
QRCodeScannerWebView.vue
。 -
组件挂载时,安卓端需要申请相机权限。
setup(props,context) { const hasPermission = ref(false); const requestCameraPermission = () => { let platform=uni.getSystemInfoSync().platform if(platform === 'ios'){ hasPermission.value = true; }else if(platform=='android'){ plus.android.requestPermissions(['android.permission.CAMERA'], function(e){ if(e.deniedAlways.length>0){ console.log(e.deniedAlways.toString()); } if(e.deniedPresent.length>0){ console.log(e.deniedPresent.toString()); } if(e.granted.length>0){ hasPermission.value = true; } }, function(e){ console.log('Request Permissions error:'+JSON.stringify(e)); }); } } onMounted(()=>{ //#ifdef APP requestCameraPermission(); //#endif }) }
-
在template中添加WebView,URL指向上一步的网页应用。
<template> <view> <web-view v-if="hasPermission" @message="handlePostMessage" cache="true" src="https://tony-xlh.github.io/Vanilla-JS-Barcode-Reader-Demos/uniapp?startScan=true"></web-view> </view> </template>
-
处理网页发来的消息,如果有扫码结果就触发
scanned
事件。const handlePostMessage = (e) => { if (e.detail.data && e.detail.data.length>0) { if (e.detail.data[0].results) { context.emit("scanned", JSON.parse(e.detail.data[0].results)); } } }
在页面中使用扫码组件
使用条件编译,如果是Web端就用Web版的扫码组件,如果是应用端就用基于WebView的扫码组件。
<view class="fullscreen" v-if="scanning">
<!-- #ifdef H5 -->
<QRCodeScannerWeb @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScannerWeb>
<!-- #endif -->
<!-- #ifdef APP -->
<QRCodeScanner @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScanner>
<!-- #endif -->
</view>
集成到小程序
小程序也支持WebView,可以用集成到原生移动应用一样的方法。但在微信小程序中,postMessage需要使用复制链接、分享等操作触发,扫描到结果后需要在执行上述操作之一才能返回主界面。所以,我们采用服务端解码的方案。
新建一个小程序用的扫码组件
-
建立一个新的组件,命名为
QRCodeScannerMP.vue
。 -
使用相机插件打开相机。
<template> <view> <camera device-position="back" flash="off" @error="error" style="width: 100%; height: 300px;"></camera> </view> </template> <script> export default { name:"QRCodeScannerMP", setup(props,context){ }, data() { return { }; } } </script> <style> </style>
-
新建一个解码函数,获取视频帧,将其转换为base64后发送给服务端解码,并将扫码结果返回给父组件。这里我们使用之前一篇文章写的Python后端提供解码API。这一服务部署在Vercel上,国内可能需要翻墙。小程序正式上架的话,需要部署在国内已经备案的主机上。
const ctx = uni.createCameraContext(); const scanning = ref(false); const captureAndScan = () => { if (scanning.value === true) { console.log("skip"); return; } scanning.value === true; ctx.takePhoto({ quality: 'high', success: (res) => { let url = res.tempImagePath; uni.getFileSystemManager().readFile({ filePath: url, encoding: 'base64', success: res => { let base64 = res.data decode(base64); },fail: (e) => { console.log("Failed"); scanning.value = false; }, } ) }, fail: ()=> { scanning.value = false; } }); } const decode = async (base64) => { uni.request({ url: 'https://barcode-reading-server.vercel.app/', method: 'POST', data: { base64: base64 }, header: { 'content-type': 'application/json' }, success: (res) => { console.log(res.data); let results = []; let parsedResults = res.data.results; for (var i = 0; i < parsedResults.length; i++) { let parsedResult = parsedResults[i]; let result = {}; result.barcodeText = parsedResult.barcodeText; result.barcodeFormatString = parsedResult.barcodeFormat; result.localizationResult = {}; result.localizationResult.x1 = parsedResult.x1; result.localizationResult.x2 = parsedResult.x2; result.localizationResult.x3 = parsedResult.x3; result.localizationResult.x4 = parsedResult.x4; result.localizationResult.y1 = parsedResult.y1; result.localizationResult.y2 = parsedResult.y2; result.localizationResult.y3 = parsedResult.y3; result.localizationResult.y4 = parsedResult.y4; results.push(result); } context.emit("scanned", results); }, complete: (res) => { scanning.value = false; } }); }
这里用
scanning
这一变量控制扫码,如果上一次解码没有完成,则等待它完成。 -
组件挂载时,开启一个interval进行实时扫描。
onMounted(() => { startScanning(); });
-
组件卸载时,则停止扫描。
const interval = ref<any>(null); onBeforeUnmount(() => { stopScanning(); });
-
在
index.vue
的条件编译中,添加小程序的判定。<!-- #ifdef MP --> <QRCodeScannerMP @scanned="scanned"></QRCodeScannerMP> <!-- #endif -->
测试下来,服务端扫码的性能还可以,对准后一秒内就能扫出码。
源代码
欢迎下载源代码并尝试使用: