一、声明及注意事项
代码需要部署所在的服务器需要https;否则功能不能实现 需要的npm包:@ericblade/quagga2
,jsqr
二、具体实现
2.1 安装包
yarn add jsqr @ericblade/quagga2
2.2 封装一个组件Scanner,这里以Vue为例
2.2.1 template部分
< template>
< div class = " scaner" ref = " scaner" >
< div class = " banner" >
< p class = " text" > 若当前浏览器无法扫码,请切换其他浏览器尝试</ p>
</ div>
< div class = " cover" >
< p class = " line" > </ p>
< span class = " square top left" > </ span>
< span class = " square top right" > </ span>
< span class = " square bottom right" > </ span>
< span class = " square bottom left" > </ span>
< p class = " tips" > 将二维码放入框内,即可自动扫描</ p>
</ div>
< video
v-show = " showPlay"
class = " source"
ref = " video"
:width = " videoWH.width"
:height = " videoWH.height"
controls
> </ video>
< canvas v-show = " !showPlay" ref = " canvas" />
< button v-show = " showPlay" @click = " run" > 开始</ button>
</ div>
</ template>
2.2.2 script部分
import jsQR from 'jsqr' ;
import Quagga from '@ericblade/quagga2' ;
export default {
name: 'Scaner' ,
props: {
useBackCamera: {
type: Boolean,
default : true
} ,
stopOnScaned: {
type: Boolean,
default : true
} ,
drawOnfound: {
type: Boolean,
default : true
} ,
lineColor: {
type: String,
default : '#03C03C'
} ,
lineWidth: {
type: Number,
default : 2
} ,
videoWidth: {
type: Number,
default : document. documentElement. clientWidth || document. body. clientWidth
} ,
videoHeight: {
type: Number,
default : document. documentElement. clientHeight - 48 || document. body. clientHeight - 48
} ,
responsive: {
type: Boolean,
default : false
}
} ,
data ( ) {
return {
showPlay: false ,
containerWidth: null ,
active: false ,
stopOnScanned: true
}
} ,
computed: {
videoWH ( ) {
if ( this . containerWidth) {
const width = this . containerWidth;
const height = width * 0.75 ;
return { width, height } ;
}
return { width: this . videoWidth, height: this . videoHeight } ;
}
} ,
watch: {
active: {
immediate: true ,
handler ( active ) {
if ( ! active) {
this . fullStop ( ) ;
}
}
}
} ,
methods: {
drawLine ( begin, end ) {
this . canvas. beginPath ( ) ;
this . canvas. moveTo ( begin. x, begin. y) ;
this . canvas. lineTo ( end. x, end. y) ;
this . canvas. lineWidth = this . lineWidth;
this . canvas. strokeStyle = this . lineColor;
this . canvas. stroke ( ) ;
} ,
drawBox ( location ) {
if ( this . drawOnfound) {
this . drawLine ( location. topLeftCorner, location. topRightCorner) ;
this . drawLine ( location. topRightCorner, location. bottomRightCorner) ;
this . drawLine ( location. bottomRightCorner, location. bottomLeftCorner) ;
this . drawLine ( location. bottomLeftCorner, location. topLeftCorner) ;
}
} ,
tick ( ) {
let _this = this
if ( this . $refs. video && this . $refs. video. readyState === this . $refs. video. HAVE_ENOUGH_DATA ) {
this . $refs. canvas. height = this . videoWH. height;
this . $refs. canvas. width = this . videoWH. width;
this . canvas. drawImage ( this . $refs. video, 0 , 0 , this . $refs. canvas. width, this . $refs. canvas. height) ;
const imageData = this . canvas. getImageData ( 0 , 0 , this . $refs. canvas. width, this . $refs. canvas. height) ;
let code = false ;
let dataurl = this . $refs. canvas. toDataURL ( )
try {
Quagga. decodeSingle ( {
src: dataurl,
numOfWorkers: 0 ,
inputStream: {
size: 800
} ,
decoder: {
readers: [ "code_128_reader" ]
} ,
} , function ( result ) {
if ( result && result. codeResult && result. codeResult. code) {
let { code } = result. codeResult
console. log ( 'code' , code)
_this. found ( code) ;
} else {
let qrresult = jsQR ( imageData. data, imageData. width, imageData. height)
if ( qrresult && qrresult. data) {
_this. found ( qrresult. data) ;
}
}
_this. run ( ) ;
} ) ;
} catch ( e) {
console. error ( e) ;
}
}
} ,
setup ( ) {
if ( this . responsive) {
this . $nextTick ( ( ) => {
this . containerWidth = this . $refs. scaner. clientWidth;
} ) ;
}
if ( navigator. mediaDevices && navigator. mediaDevices. getUserMedia) {
this . previousCode = null ;
this . parity = 0 ;
this . active = true ;
this . canvas = this . $refs. canvas. getContext ( "2d" ) ;
const facingMode = this . useBackCamera ? { exact: 'environment' } : 'user' ;
const handleSuccess = stream => {
if ( this . $refs. video. srcObject !== undefined ) {
this . $refs. video. srcObject = stream;
} else if ( window. videoEl. mozSrcObject !== undefined ) {
this . $refs. video. mozSrcObject = stream;
} else if ( window. URL . createObjectURL) {
this . $refs. video. src = window. URL . createObjectURL ( stream) ;
} else if ( window. webkitURL) {
this . $refs. video. src = window. webkitURL. createObjectURL ( stream) ;
} else {
this . $refs. video. src = stream;
}
this . $refs. video. playsInline = true ;
const playPromise = this . $refs. video. play ( ) ;
playPromise. catch ( ( ) => ( this . showPlay = true ) ) ;
playPromise. then ( this . run) ;
} ;
navigator. mediaDevices
. getUserMedia ( { video: { facingMode } } )
. then ( handleSuccess)
. catch ( ( ) => {
navigator. mediaDevices
. getUserMedia ( { video: true } )
. then ( handleSuccess)
. catch ( error => {
this . $emit ( "error-captured" , error) ;
} ) ;
} ) ;
}
} ,
run ( ) {
if ( this . active) {
requestAnimationFrame ( this . tick) ;
}
} ,
found ( code ) {
if ( this . previousCode !== code) {
this . previousCode = code;
} else if ( this . previousCode === code) {
this . parity += 1 ;
}
if ( this . parity > 2 ) {
this . active = this . stopOnScanned ? false : true ;
this . parity = 0 ;
this . $emit ( "code-scanned" , code) ;
}
} ,
fullStop ( ) {
if ( this . $refs. video && this . $refs. video. srcObject) {
this . $refs. video. srcObject. getTracks ( ) . forEach ( t => t. stop ( ) ) ;
}
}
} ,
mounted ( ) {
} ,
beforeDestroy ( ) {
this . fullStop ( ) ;
}
}
2.2.3 style部分
.scaner {
background : #000000;
position : fixed;
top : 0;
left : 0;
width : 100%;
height : 100%;
}
.scaner .banner {
width : 340px;
position : absolute;
top : 16px;
left : 50%;
margin-left : -170px;
background : #FA74A2;
border-radius : 8px;
box-sizing : border-box;
padding : 12px;
opacity : 0.9;
box-shadow : 1px 1px 10px rgba ( 0, 0, 0, 0.2) ;
}
.scaner .banner .text {
padding : 0;
margin : 0;
color : #FFFFFF;
font-size : 12px;
text-align : center;
}
.scaner .cover {
height : 220px;
width : 220px;
position : absolute;
top : 50%;
left : 50%;
-webkit-transform : translate ( -50%, -50%) ;
-moz-transform : translate ( -50%, -50%) ;
-ms-transform : translate ( -50%, -50%) ;
-o-transform : translate ( -50%, -50%) ;
transform : translate ( -50%, -50%) ;
border : .5px solid #999999;
z-index : 1111;
}
.scaner .cover .line {
width : 200px;
height : 1px;
margin-left : 10px;
background : #5F68E8;
background : linear-gradient ( to right, transparent, #5F68E8, #0165FF, #5F68E8, transparent) ;
position : absolute;
-webkit-animation : scan 1.75s infinite linear;
-moz-animation : scan 1.75s infinite linear;
-ms-animation : scan 1.75s infinite linear;
-o-animation : scan 1.75s infinite linear;
animation : scan 1.75s infinite linear;
-webkit-animation-fill-mode : both;
-moz-animation-fill-mode : both;
-ms-animation-fill-mode : both;
-o-animation-fill-mode : both;
animation-fill-mode : both;
border-radius : 1px;
}
.scaner .cover .square {
display : inline-block;
height : 20px;
width : 20px;
position : absolute;
}
.scaner .cover .square.top {
top : 0;
border-top : 1px solid #5F68E8;
}
.scaner .cover .square.left {
left : 0;
border-left : 1px solid #5F68E8;
}
.scaner .cover .square.bottom {
bottom : 0;
border-bottom : 1px solid #5F68E8;
}
.scaner .cover .square.right {
right : 0;
border-right : 1px solid #5F68E8;
}
.scaner .cover .tips {
position : absolute;
bottom : -48px;
width : 100%;
font-size : 14px;
color : #FFFFFF;
opacity : 0.8;
}
@-webkit-keyframes scan {
0% { top : 0}
25% { top : 50px}
50% { top : 100px}
75% { top : 150px}
100% { top : 200px}
}
@-moz-keyframes scan {
0% { top : 0}
25% { top : 50px}
50% { top : 100px}
75% { top : 150px}
100% { top : 200px}
}
@-o-keyframes scan {
0% { top : 0}
25% { top : 50px}
50% { top : 100px}
75% { top : 150px}
100% { top : 200px}
}
@keyframes scan {
0% { top : 0}
25% { top : 50px}
50% { top : 100px}
75% { top : 150px}
100% { top : 200px}
}
3、把上面合在一起就OK
4、如果你觉得有用话,请点赞