起攝像頭識(shí)別條形碼實(shí)戰(zhàn):getUserMedia+jsQR完整指南)
簡介手機(jī)攝像頭實(shí)時(shí)識(shí)別條形碼的H5實(shí)踐資源面向Web前端與移動(dòng)端開發(fā)者演示不依賴原生App、在瀏覽器環(huán)境中完成掃碼的實(shí)現(xiàn)思路適用于電商、物流、庫存管理等場景對希望低成本接入掃碼能力的前端用戶尤其友好。壓縮包共3個(gè)文件包括1個(gè)HTML示例頁與2個(gè)JavaScript腳本——jquery庫提供基礎(chǔ)DOM操作Quagga掃碼庫負(fù)責(zé)視頻流解析與條碼識(shí)別總大小僅291KB輕量易用下載后可直接打開頁面體驗(yàn)。目前已有1681人學(xué)習(xí)/下載。示例圍繞HTML5的video標(biāo)簽、getUserMedia API和QuaggaJS展開包含攝像頭視頻流獲取、掃碼區(qū)域配置、Code 128條碼解碼及onDetected回調(diào)處理等關(guān)鍵代碼較為完整地呈現(xiàn)了實(shí)時(shí)掃碼流程讀者可據(jù)此快速搭建可運(yùn)行Demo并進(jìn)一步擴(kuò)展其他條碼類型、適配移動(dòng)端UI或?qū)⒆R(shí)別結(jié)果通過后臺(tái)接口實(shí)時(shí)同步滿足庫存盤點(diǎn)、快遞掃碼等業(yè)務(wù)需要。 前段時(shí)間接了個(gè)需求在 H5 頁面里調(diào)起手機(jī)攝像頭識(shí)別條形碼。聽起來不算復(fù)雜但真做起來才發(fā)現(xiàn)坑不少——權(quán)限、兼容性、識(shí)別率、性能每一環(huán)都可能翻車。這篇就完整拆一遍從技術(shù)選型到核心實(shí)現(xiàn)到避坑給后來人一個(gè)可以直接參考的方案。這個(gè)需求的實(shí)際場景很典型倉庫盤點(diǎn)、門店核銷、自助機(jī)頁面、醫(yī)療耗材掃碼等。網(wǎng)頁端要掃條形碼以前只能靠第三方 App 或者原生殼現(xiàn)在用 HTML5 的能力就能實(shí)現(xiàn)。核心鏈路其實(shí)就一句話調(diào)起攝像頭獲取視頻流截幀后交給解碼庫識(shí)別條形碼內(nèi)容。1. 項(xiàng)目概述與需求拆解1.1 這個(gè)需求背后的真實(shí)場景在動(dòng)手寫代碼之前先把需求想明白。表面上是“識(shí)別條形碼”但業(yè)務(wù)上往往有更多隱含要求識(shí)別速度用戶把手機(jī)對準(zhǔn)條碼如果 2 秒內(nèi)不出結(jié)果體驗(yàn)就會(huì)直線下降。識(shí)別類型是 EAN-13、Code128、Code39還是 QR 碼條形碼和二維碼的解碼庫側(cè)重點(diǎn)不同。連續(xù)識(shí)別是掃一次就停還是需要連續(xù)掃碼比如批量盤點(diǎn)場景用戶會(huì)連續(xù)掃多個(gè)條碼。使用環(huán)境是普通瀏覽器、微信內(nèi)置瀏覽器、企業(yè)微信還是被 App 的 WebView 嵌套不同容器的權(quán)限策略差別很大。這些沒搞清楚就開寫后面大概率返工。所以我一般先把“識(shí)別什么碼、在什么環(huán)境用、掃完干什么”這三件事問清楚再進(jìn)入技術(shù)方案。1.2 技術(shù)選型幾條路線的對比H5 識(shí)別條形碼業(yè)界主流方案有這么幾種方案實(shí)現(xiàn)思路優(yōu)點(diǎn)缺點(diǎn)原生getUserMedia jsQR自己調(diào)攝像頭截幀后用 jsQR 解碼輕量、可控性強(qiáng)、無額外請求需要自己處理權(quán)限和兼容性代碼量偏大封裝庫html5-qrcode內(nèi)部封裝了攝像頭調(diào)用和識(shí)別邏輯API 簡單、上手快、支持掃碼槍模擬定制性稍弱移動(dòng)端性能一般ZXing 的 JS 移植版Java 庫轉(zhuǎn)成 JS支持的碼制更全庫體積較大維護(hù)活躍度一般商業(yè)庫如 Dynamsoft成熟商用方案識(shí)別率和碼制覆蓋最穩(wěn)商用收費(fèi)個(gè)人小項(xiàng)目不建議我的選擇是原生拖幀 jsQR。原因很簡單條形碼識(shí)別這個(gè)場景jsQR 對 Code128、EAN-13、EAN-8 這些常見碼制支持得很好而且純前端解析視頻幀不經(jīng)過服務(wù)器也沒有額外的網(wǎng)絡(luò)延遲。相比用html5-qrcode這種封裝庫原生方案在幀率控制、識(shí)別區(qū)域裁剪上更靈活排查問題也更直接。注意jsQR 對 QR 碼的支持也很好但如果你只需要識(shí)別一維條形碼并且對識(shí)別率要求極高可以考慮把jsQR換成BarcodeDetector瀏覽器原生 API不過它的兼容性目前還一般后面會(huì)詳說。2. 環(huán)境準(zhǔn)備與前置條件2.1 HTTPS 和瀏覽器權(quán)限在本地localhost上調(diào)試時(shí)getUserMedia是允許的一旦上線所有頁面必須走 HTTPS否則瀏覽器會(huì)直接拒絕攝像頭權(quán)限。很多新手在這里踩了第一坑本地好好的部署到測試服就黑屏。微信、支付寶這些內(nèi)置瀏覽器以及 App 的 WebView對權(quán)限的處理也各有差異。以微信為例iOS 端的 WebView 在請求攝像頭權(quán)限時(shí)會(huì)彈系統(tǒng)授權(quán)框Android 端部分舊版本 X5 內(nèi)核則可能需要單獨(dú)配置權(quán)限申請。所以在項(xiàng)目啟動(dòng)初期就要把“能用 HTTPS、能彈授權(quán)框”這兩個(gè)前提先驗(yàn)證掉別等代碼寫完才發(fā)現(xiàn)環(huán)境不支持??梢杂孟旅孢@段代碼快速驗(yàn)證當(dāng)前環(huán)境是否支持?jǐn)z像頭調(diào)用if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { alert(當(dāng)前瀏覽器不支持?jǐn)z像頭調(diào)用); }2.2 攝像頭畫面的獲取流程H5 拿攝像頭畫面靠的是navigator.mediaDevices.getUserMedia它返回一個(gè)MediaStream里面包含視頻軌道。拿到視頻流之后把它塞給video標(biāo)簽就能實(shí)時(shí)預(yù)覽。但從“看到畫面”到“識(shí)別條形碼”中間還有一步很關(guān)鍵視頻流不能直接傳給解碼器必須先截一幀到canvas再把canvas的圖像數(shù)據(jù)交給 jsQR 解析。這相當(dāng)于給解碼器喂一張靜態(tài)圖片。有一個(gè)容易被忽略的點(diǎn)在手機(jī)瀏覽器里前置攝像頭的視頻流默認(rèn)是鏡像的后置攝像頭一般正常。掃描條形碼場景我們需要后置攝像頭通過facingMode: environment來指定const constraints { video: { facingMode: { exact: environment } // 強(qiáng)制后置攝像頭 } };這里我用的是exact表示嚴(yán)格匹配后置。如果某些設(shè)備拿不到后置請求會(huì)失敗如果改成facingMode: environment不帶 exact瀏覽器會(huì)盡量匹配實(shí)在沒有就用默認(rèn)攝像頭。實(shí)際項(xiàng)目中建議先用不帶 exact 的寫法做降級(jí)保證兼容性。3. 核心實(shí)現(xiàn)三步完成條形碼識(shí)別3.1 調(diào)起攝像頭并顯示預(yù)覽先寫一個(gè)最基礎(chǔ)的 HTML 結(jié)構(gòu)video idvideo autoplay playsinline muted/video canvas idcanvas styledisplay:none/canvas這里playsinline很重要iOS Safari 默認(rèn)會(huì)嘗試全屏播放視頻加上這個(gè)屬性才能保持內(nèi)聯(lián)預(yù)覽。muted也是 iOS 的要求之一靜音視頻播放才不會(huì)被瀏覽器攔截。然后調(diào)起攝像頭async function initCamera() { try { const stream await navigator.mediaDevices.getUserMedia({ video: { facingMode: environment }, audio: false }); const video document.getElementById(video); video.srcObject stream; await video.play(); // 等 video 真正開始播放后再啟動(dòng)識(shí)別循環(huán) startScanLoop(); } catch (err) { console.error(攝像頭調(diào)用失敗:, err); } }有個(gè)細(xì)節(jié)video.play()返回的是 Promise在部分安卓瀏覽器上不 await 直接進(jìn)入識(shí)別循環(huán)videoWidth可能還是 0導(dǎo)致 canvas 畫出來是黑圖。所以代碼里我強(qiáng)烈建議await video.play()。3.2 截幀與解碼接下來是識(shí)別循環(huán)。核心思路是定時(shí)從 video 上抓一幀塞給 jsQR 解析。function startScanLoop() { const video document.getElementById(video); const canvas document.getElementById(canvas); const ctx canvas.getContext(2d, { willReadFrequently: true }); // 注意willReadFrequently 可以提升 getImageData 的性能 setInterval(() { if (video.readyState video.HAVE_ENOUGH_DATA) { // 縮小 canvas減少計(jì)算量 const targetWidth 480; const scale Math.min(1, targetWidth / video.videoWidth); canvas.width video.videoWidth * scale; canvas.height video.videoHeight * scale; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData ctx.getImageData(0, 0, canvas.width, canvas.height); const code jsQR(imageData.data, imageData.width, imageData.height); if (code code.data) { handleScanResult(code.data); } } }, 100); // 每 100ms 識(shí)別一幀 }這段代碼里有幾個(gè)優(yōu)化點(diǎn)值得說細(xì)一點(diǎn)。第一canvas 的寬高沒必要跟視頻原始分辨率一樣。手機(jī)上視頻流分辨率動(dòng)輒 1920×1080如果直接拿這個(gè)尺寸去解碼每一幀的計(jì)算量非常大手機(jī)會(huì)發(fā)熱識(shí)別率反而不穩(wěn)定。實(shí)踐中我習(xí)慣把最長邊縮到 480px 左右。這個(gè)值足夠 jsQR 識(shí)別條形碼同時(shí)把計(jì)算量降低好幾個(gè)量級(jí)。第二getImageData的性能優(yōu)化。Canvas 的getContext(2d)默認(rèn)是為了繪制設(shè)計(jì)的對頻繁讀取像素?cái)?shù)據(jù)沒有做特殊優(yōu)化。在創(chuàng)建 context 時(shí)傳入{ willReadFrequently: true }能讓瀏覽器知道你要反復(fù)讀像素從而選擇更適合的內(nèi)存布局。這個(gè)參數(shù)很實(shí)用算是一個(gè)小技巧。第三識(shí)別頻率不必太高。每秒 10 幀100ms 一次已經(jīng)足夠。掃碼的本質(zhì)是用戶把條碼對準(zhǔn)攝像頭畫面穩(wěn)定后識(shí)別成功往往就在一兩幀內(nèi)幀率再高不僅耗電還會(huì)讓 CPU 持續(xù)滿載。3.3 識(shí)別結(jié)果的去重與回調(diào)實(shí)際使用中還會(huì)遇到另一個(gè)問題連續(xù)識(shí)別時(shí)同一個(gè)條碼會(huì)被反復(fù)掃到。比如用戶掃一次貨架上的條碼jsQR 在畫面穩(wěn)定的那 200ms 里解碼成功了 3 次如果每次回調(diào)都觸發(fā)業(yè)務(wù)邏輯就會(huì)重復(fù)提交。所以要加一個(gè)簡單的“防抖”邏輯let lastResult ; let lastResultTime 0; const DEBOUNCE_MS 2000; // 2 秒內(nèi)同一個(gè)碼只觸發(fā)一次 function handleScanResult(data) { const now Date.now(); if (data lastResult now - lastResultTime DEBOUNCE_MS) { return; } lastResult data; lastResultTime now; // 這里再跳轉(zhuǎn)到你的業(yè)務(wù)處理邏輯 console.log(識(shí)別到條碼:, data); }這個(gè)邏輯很樸素但很管用。設(shè)置 2 秒的去重時(shí)間既不會(huì)讓用戶覺得“掃一次彈好幾下”又能在用戶連續(xù)掃兩個(gè)相同條碼時(shí)正常觸發(fā)第二次。3.4 關(guān)閉攝像頭一個(gè)容易被忽視的收尾很多人在掃碼成功后直接跳轉(zhuǎn)頁面根本不管攝像頭有沒有關(guān)。H 5 頁面如果不主動(dòng)停掉視頻流攝像頭指示燈會(huì)一直亮著用戶會(huì)非常不安。正確的做法是function stopCamera() { const video document.getElementById(video); if (video video.srcObject) { video.srcObject.getTracks().forEach(track track.stop()); video.srcObject null; } }注意要調(diào)用getTracks().forEach(track track.stop())把上面的所有軌道都停掉而不僅僅是停 video 標(biāo)簽。這個(gè)操作在掃碼成功跳轉(zhuǎn)前、頁面卸載前都要做一遍。4. 常見問題與排查技巧實(shí)錄4.1 攝像頭打不開權(quán)限與環(huán)境的排查路徑攝像頭打不開是遇到最多的問題原因通常有以下幾類現(xiàn)象可能原因排查方向返回NotAllowedError用戶拒絕了授權(quán)檢查是否有引導(dǎo)用戶開啟權(quán)限的邏輯返回NotFoundError設(shè)備沒有攝像頭或 constraint 不滿足改用不帶 exact 的 facingMode直接黑屏無反應(yīng)HTTPS 未配置或?yàn)g覽器版本不支持檢查頁面協(xié)議、內(nèi)核版本在微信里打不開微信內(nèi)置瀏覽器權(quán)限策略限制測試原生瀏覽器排除問題必要時(shí)引導(dǎo)用系統(tǒng)瀏覽器打開有一個(gè)特別容易踩的坑getUserMedia在用戶點(diǎn)擊事件之外調(diào)用某些瀏覽器會(huì)直接拒絕授權(quán)彈窗。比如頁面加載后就自動(dòng)調(diào)攝像頭Safari 可能不給彈權(quán)限框。解決辦法是把初始化攝像頭綁在“開始掃碼”按鈕的點(diǎn)擊事件里。4.2 識(shí)別率低畫面清晰度與光照的影響jsQR 的解碼效果高度依賴輸入圖像質(zhì)量。最常見的識(shí)別失敗原因不是算法不行而是條碼在畫面里太小、太暗或者反光。實(shí)際調(diào)優(yōu)時(shí)可以參考這幾個(gè)方向距離引導(dǎo)頁面加一條輔助線提示用戶把條碼放在畫面中央?yún)^(qū)域。禁止縮放有些瀏覽器會(huì)自動(dòng)對 video 做縮放導(dǎo)致條碼變虛??梢越o video 加object-fit: cover讓畫面鋪滿容器。裁剪識(shí)別區(qū)域優(yōu)先掃描畫面中央的條碼還可以預(yù)處理圖像比如去噪、增強(qiáng)對比度或做灰度化處理。jsQR 內(nèi)部會(huì)做灰度化但如果你在裁剪區(qū)域做了額外預(yù)處理比如用ctx.filter contrast(1.2)增強(qiáng)對比度在暗光環(huán)境下往往有意外驚喜。提示光線如果環(huán)境光不足建議在頁面上給出“光線不足”的提示。這個(gè)可以用視頻幀的平均亮度來判斷但不是必須根據(jù)自己的場景決定。4.3 瀏覽器原生掃碼 APIBarcodeDetector除了 jsQR瀏覽器現(xiàn)在提供了一個(gè)原生的BarcodeDetectorAPI可以直接識(shí)別條碼還支持指定碼制EAN-13、QR_CODE 等識(shí)別速度比純 JS 庫快不少。if (BarcodeDetector in window) { const detector new BarcodeDetector({ formats: [ean_13, code_128, qr_code] }); const barcodes await detector.detect(canvas); // barcodes[0].rawValue 就是識(shí)別結(jié)果 }但這個(gè) API 目前最大的問題是兼容性參差不齊Chrome 桌面端和 Android 上支持較好iOS Safari 截至最近仍未完整支持微信內(nèi)置瀏覽器就更不一定了。我的建議是把 BarcodeDetector 當(dāng)成一個(gè)漸進(jìn)增強(qiáng)的能力優(yōu)先用 jsQR 保證全端一致檢測到支持 BarcodeDetector 時(shí)再切換過去。4.4 微信小程序和 App 嵌套 H5 的場景注意點(diǎn)看熱搜詞里很多人關(guān)心“H5 能不能調(diào)用微信小程序當(dāng)前經(jīng)緯度”“小程序跳 H5 頁面”這類問題這里順帶說一下。H5 頁面在小程序 WebView 里運(yùn)行時(shí)攝像頭權(quán)限策略和小程序原生組件完全是兩套邏輯。小程序的camera組件權(quán)限在小程序側(cè)H5 的getUserMedia權(quán)限在 WebView 側(cè)。如果你遇到在小程序里 H5 調(diào)不起攝像頭大概率是 WebView 沒有把攝像頭權(quán)限授權(quán)給頁面需要在 App 或小程序的 web-view 配置里處理。同樣App 里嵌套 H5 時(shí)Android 端的 WebView 需要在原生層申請CAMERA權(quán)限并且設(shè)置WebChromeClient.onPermissionRequest回調(diào)否則 H5 調(diào)用getUserMedia時(shí)會(huì)靜默失敗。這些問題前端單獨(dú)排查不出來得拉上客戶端開發(fā)一起聯(lián)調(diào)。4.5 頁面報(bào)錯(cuò)排查技巧我把一些常見的報(bào)錯(cuò)信息整理成了一個(gè)速查表方便大家快速定位報(bào)錯(cuò)信息含義解決方向TypeError: Cannot read property getUserMedia of undefined瀏覽器不支持檢查 HTTPS、瀏覽器版本、內(nèi)核OverconstrainedError指定的 facingMode 無法滿足去掉exact限定NotReadableError攝像頭被其他程序占用關(guān)閉其他用到攝像頭的頁面或 AppAbortError用戶主動(dòng)取消授權(quán)提示用戶重新授權(quán)在 button 點(diǎn)擊回調(diào)中重新調(diào)用 getUserMedia5. 關(guān)于性能優(yōu)化和用戶體驗(yàn)的幾點(diǎn)心得最后分享一些交互層面的經(jīng)驗(yàn)。別讓用戶自己點(diǎn)“開始”。很多掃碼頁面把攝像頭調(diào)用放在一個(gè)“開始掃碼”按鈕上這其實(shí)多了一步操作。用戶點(diǎn)進(jìn)頁面目的就是掃碼直接在頁面加載后自動(dòng)拉起攝像頭記得通過用戶手勢觸發(fā)比如監(jiān)聽點(diǎn)擊后調(diào)用配合一個(gè)半透明遮罩框提示“將條碼置于框內(nèi)”體驗(yàn)順很多。識(shí)別成功要有明確的視覺和聲音反饋。視覺上可以做一個(gè)綠色的對勾框閃一下聲音上如果可以調(diào)用系統(tǒng)的震動(dòng)或悅耳的提示音就更好。用戶掃完條碼如果在 200ms 內(nèi)沒看到任何反饋會(huì)下意識(shí)反復(fù)晃手機(jī)反而讓后續(xù)識(shí)別更困難。識(shí)別區(qū)域別占滿整個(gè)屏幕。對齊輔助框通常放在屏幕中部寬度約為屏幕的三分之二。這個(gè)區(qū)域內(nèi)識(shí)別準(zhǔn)確率和速度最高。用戶把條碼對準(zhǔn)框內(nèi)比整個(gè)屏幕胡亂晃動(dòng)要穩(wěn)定得多。注意 H5 頁面在頁面切后臺(tái)時(shí)的處理。用visibilitychange事件在頁面隱藏時(shí)停止識(shí)別循環(huán)回到前臺(tái)時(shí)再恢復(fù)。否則用戶切到其他 App 再返回頁面還在瘋狂解碼耗電發(fā)熱不說還可能拿到一堆無效圖像。這一塊代碼很簡單但很能體現(xiàn)細(xì)節(jié)。6. 整套方案的最終形態(tài)把上面的步驟合在一起一個(gè)完整的 H5 掃碼頁面功能就成型了頁面加載后點(diǎn)擊按鈕調(diào)起getUserMedia視頻流實(shí)時(shí)預(yù)覽在video標(biāo)簽中setInterval定時(shí)截幀到canvasjsQR解碼canvas圖像解碼成功后去重、回傳業(yè)務(wù)處理跳轉(zhuǎn)或停止攝像頭釋放視頻流。我后來把這套方案沉淀成了一個(gè)公共組件業(yè)務(wù)側(cè)只需要傳入“識(shí)別成功后做什么”的回調(diào)函數(shù)。目前已經(jīng)用在掃碼核銷、設(shè)備巡檢幾個(gè)項(xiàng)目里整體識(shí)別成功率在室內(nèi)光照環(huán)境下能穩(wěn)定在 98% 以上解碼耗時(shí)單幀大約 20~40ms用戶體驗(yàn)是比較流暢的。有一點(diǎn)值得強(qiáng)調(diào)H5 掃碼方案本質(zhì)上是“用瀏覽器能力模擬掃碼槍”它在便利性上有無可替代的優(yōu)勢但與原生掃碼在弱光、強(qiáng)反光等極端場景下仍有差距。如果你們的業(yè)務(wù)對識(shí)別率有極致要求比如密集貨架、大幅面的 Code128 標(biāo)簽建議在前端方案之上再搭配原生掃碼組件兜底。兩者結(jié)合才是一個(gè)生產(chǎn)級(jí)掃碼功能的完整形態(tài)。本文還有配套的精品資源點(diǎn)擊獲取