IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> React HOOK 自定义拍照画面,自动截取指定画面 -> 正文阅读

[JavaScript知识库]React HOOK 自定义拍照画面,自动截取指定画面

需求

要求实现用户上传身份证照片,自定义上传画面,自动截取身份证图片,并获取身份证上面的信息。如果机型不支持则留有保底手段:相册上传图片,获取身份证上的信息。

大概思路

自定义上传画面

主要依赖 navigator.mediaDevices.getUserMedia API

自动截取身份证图片

根据 canvas.drawImage 去生成画布,随后进行 canvas.toDataURL 获取base64格式的图片

获取身份证上面的信息

这个先将base64图片上传oss,获取到图片url,再根据百度ocr身份证识别,获取到对应正反面的信息。

效果

实现

此功能为组件形式注入到上传图片的页面中,为保存一些数据一些utils内的方法,redux中的操作也都在下面展示。

核心代码

import React, { memo, useEffect, useRef, useState } from 'react'
import PropsType from 'prop-types'
import styled from 'styled-components'
import { withRouter } from 'react-router-dom'
import { detectDeviceType } from 'zzy-javascript-devtools'
import { connect } from 'react-redux'

import globalSty from '../../api/global-style'
import { getUserMedia, getXYRatio, cameraErrorMsg } from '../../api/utils'
import { getIdCardMsg, setCameraType } from './store/actionCreators'
import HeaderBar from '../../components/HeaderBar'
import Toast from '../../components/Toast'
import { uploadFileBase64Req } from '../../api'

let captrueTimer
let getStreamTimer
let isStart = false
let videoStreams
let pageTimer

const Camera = (props) => {const { type, close } = propsconst {setCameraTypeDispatch,getIdCardMsgDispatch,changeIdCardMsg,uploadFile} = props// photo这个值是用来开发测试时在截取区域上方黑色地方展示当前截取base64图片的。// const [photo, setPhoto] = useState('')// 1-正面 2-反面const [idCardType, setIdCardType] = useState(1)useEffect(() => {// 页面进入时,正反面状态以父组件传值为准setIdCardType(type)// 开始计时,超过指定时间就关闭页面,保证不会长期在此页面调用接口startTimeing()return () => {clearTimeout(pageTimer)}}, [])const videoRef = useRef()// 截取区域 refconst rectangle = useRef()// 弹窗组件ref,下面的方法都为弹窗提示组件方法,可忽略。const toastRef = useRef()useEffect(() => {if (videoRef.current && <img src="https下才可以使用,否则直接报错// 开始获取用户媒体流const getUserMediaStream = (videoNode) => {const params1 = {video: { facingMode: { exact: 'environment' } } // 设置true为获取前置,{ facingMode: { exact: 'environment' } } 为后置摄像头}const params2 = {video: {facingMode: 'user'}}return getUserMedia({audio: false,// 判断为移动端还是pc端...(detectDeviceType() === 'Mobile' ? params1 : params2)}).then((res) => {videoStreams = resreturn getStreamRes(res, videoNode)}).catch((error) => {console.log('访问用户媒体设备失败:', error.name, error.message, error)toastRef.current.warnToast({// cameraErrorMsg 为错误信息title: `${cameraErrorMsg(error.name)},请返回上级页面重新拍照上传`,cancel: '返回',confirm: '确认',onFinally() {// 失败后状态管理器内设置当前上传方式为图片上传,再次点击上传不进入此处,并销毁当前页面setCameraTypeDispatch('upload')destoryStates()close()}})return Promise.reject()})}// 调用成功const getStreamRes = (stream, video) => {return new Promise((resolve) => {video.srcObject = stream// 在指定视频/音频(audio/video)的元数据加载后触发video.onloadedmetadata = () => {// 获取成功之后等待元数据加载后进行播放video.play()resolve()}})}// 截取裁剪框const startCaptrue = () => {const _canvas = document.createElement('canvas')_canvas.style.display = 'block'// 根据video的xy比率,并提供外部比率进行换算const { YRatio, XRatio } = getXYRatio(videoRef.current)// 获取到截图面的数据const { left, top, width, height } =rectangle.current.getBoundingClientRect()_canvas.height = height_canvas.width = widthconst context = _canvas.getContext('2d')captrue()function captrue() {// 这里 drawImage 的参数有点问题,不是很严丝合缝,待后期调试,目前够用captrueTimer = setInterval(async () => {// 微信下沿偏多,但是其余浏览器中正好,保留参数// left,top 各扩展20px,width,height翻倍// top,height各累加20px 降低context.drawImage(videoRef.current,XRatio(left + window.scrollX - 20),YRatio(top + window.scrollY - 20) - 20,XRatio(width + 40),height + 40 + 20,0,0,width,height)// 获取当前截图的base64编码const base64 = _canvas.toDataURL('image/jpeg')console.log(base64, 'base64')clearTimeout(captrueTimer)// 上传base64获取oss地址const { url } = await uploadFileBase64Req(base64, true)// 从ocr中获取身份证信息,getIdCardMsgDispatch 方法里面就已经包含了数据的校验const idCardsMsg = await getIdCardMsgDispatch(url, true)if (idCardsMsg.type === type) {// 校对成功,属于正确的一面close()changeIdCardMsg(idCardsMsg)} else {// 否则继续校验captrue()}// setPhoto(base64)}, 1500)}}const startTimeing = () => {pageTimer = setTimeout(() => {destoryStates()toastRef.current.warnToast({title: '页面停留过久,请返回上级页面重新拍照上传',cancel: '返回',confirm: '确认',onFinally() {close()}})}, 3 * 60 * 1000) // 三分钟}// 关闭流const closeCameras = () => {videoStreams && videoStreams.getTracks()[0].stop()}// 销毁页面数据const destoryStates = () => {closeCameras()isStart = falseclearTimeout(getStreamTimer)getStreamTimer = nullclearInterval(captrueTimer)captrueTimer = nullvideoStreams = null}return (<CameraContainer id="Camera_component_Container"><HeaderBar clickArrow={() => close()} /><video id="video" ref={videoRef} autoPlay muted playsInline></video><div className="shadowView"><div className="rectangle" ref={rectangle}>{idCardType === 1 && (<imgsrc={require('./image/renxiang.svg')}alt=""className="renxiang"/>)}{idCardType === 2 && (<imgsrc={require('./image/guohui.svg')}alt=""className="guohui"/>)}<span className="say">请将身份证置于取景框内</span><div className="handleBar"><imgsrc={require('@/static/image/photo.svg')}alt=""className="photoIcon"onClick={clickUploadFileHandle}/></div></div>{/* <imgclassName="photo"src={photo}style={{ position: 'absolute', top: 0 }}></img> */}</div><Toast ref={toastRef} /></CameraContainer>" style="margin: auto" />
}

Camera.propTypes = {type: PropsType.number,close: PropsType.func,changeIdCardMsg: PropsType.func,uploadFile: PropsType.func,setCameraTypeDispatch: PropsType.func,getIdCardMsgDispatch: PropsType.func
}

const mapDispatchToProps = (dispatch) => ({setCameraTypeDispatch(type) {dispatch(setCameraType(type))},async getIdCardMsgDispatch(url, isCamera) {const data = await dispatch(getIdCardMsg(url, isCamera))return data}
})
export default connect(null, mapDispatchToProps)(withRouter(memo(Camera)))

const CameraContainer = styled.div`position: fixed;left: 0;right: 0;top: 0;bottom: 0;z-index: 11;overflow: hidden;background-color: #000;#video {position: absolute;left: 0;top: 0;width: 100%;height: 100%;z-index: 1;}.shadowView {/* ${globalSty.positionCenter()}; */position: absolute;left: 0;right: 0;top: 0;bottom: 0;z-index: 2;/* background-color: rgba(0, 0, 0, 0.4); */display: flex;align-items: center;justify-content: center;.rectangle {width: 80vw;/* 身份证件长宽比例 1.58:1 */height: calc(80vw / 1.58);border-radius: 1.5rem;border: 0.1rem solid rgba(243, 243, 243, 1);box-shadow: 0 0 0 2000rem rgba(0, 0, 0, 0.7);position: relative;.renxiang {position: absolute;right: 1rem;top: 45%;transform: translate(0, -50%);width: 15rem;}.guohui {position: absolute;top: 1.5rem;left: 2rem;width: 7.5rem;}.say {position: absolute;font-size: 1.5rem;font-family: PingFangSC-Regular, PingFang SC;font-weight: 400;color: rgba(255, 255, 255, 0.5);left: 50%;bottom: -3.5rem;transform: translate(-50%, -50%);}.handleBar {position: absolute;left: 0;right: 0;bottom: -9rem;.photoIcon {width: 4rem;height: 4rem;}}}}
` 

utils.js

//访问用户媒体设备的兼容方法
const getUserMedia = (constrains) => {if (navigator.mediaDevices?.getUserMedia) {//最新标准APIreturn navigator.mediaDevices.getUserMedia(constrains)} else if (navigator.webkitGetUserMedia) {//webkit内核浏览器return navigator.webkitGetUserMedia(constrains)} else if (navigator.mozGetUserMedia) {//Firefox浏览器return navigator.mozGetUserMedia(constrains)} else if (navigator.getUserMedia) {//旧版APIreturn navigator.getUserMedia(constrains)}
}

const hasUserMedia = () => {if (navigator.mediaDevices?.getUserMedia) {//最新标准APIreturn true} else if (navigator.webkitGetUserMedia) {//webkit内核浏览器return true} else if (navigator.mozGetUserMedia) {//Firefox浏览器return true} else if (navigator.getUserMedia) {//旧版APIreturn true}return false
}

const cameraErrorMsg = (name) => {if (name === 'AbortError') {return '操作被终止'} else if (name === 'NotAllowedError') {return '权限被拒绝'} else if (name === 'NotFoundError') {return '无法满足操作'} else if (name === 'NotReadableError') {return '读取失败'} else if (name === 'OverconstrainedError') {return '设备无法被满足'} else if (name === 'SecurityError') {return '权限被禁止'} else if (name === 'TypeError') {return '传值错误'} else {return '操作失败'}
}

// 获取video的xy比率,并提供外部比率进行换算
function getXYRatio(video) {// videoHeight为video 真实高度// offsetHeight为video css高度const {videoHeight: vh,videoWidth: vw,offsetHeight: oh,offsetWidth: ow} = videoreturn {YRatio: (height) => {return (vh / oh) * height},XRatio: (width) => {return (vw / ow) * width}}
}

// 判断身份证信息来自正/反
const isIdCardType = (msg) => {if (msg.name && msg.sex && msg.idcard) {return 1} else if (msg.authority && msg.validDate) {return 2} else return 0
} 

store

store中使用了immutable格式,不清楚的朋友可以先看一下文档,只是改变数据结构,其余的没什么变化。

reducer.js

import { fromJS } from 'immutable'
import {CHANGE_IDCARD_MSG,SET_CAMERA_TYPE,
} from './constants'

const defaultState = fromJS({cameraType: 'camera', // upload-拍照上传 camera-实时传输idCardMsg: {}
})

const reducer = (state = defaultState, action) => {switch (action.type) {case SET_CAMERA_TYPE:return state.set('cameraType', action.data)case CHANGE_IDCARD_MSG:return state.set('idCardMsg', action.data)default:return state}
}

export default reducer 

actionCreators.js

import { Toast } from 'antd-mobile'
import { fromJS } from 'immutable'
import { isIdCard, isName } from 'zzy-javascript-devtools'
import { idCardAndlysisReq } from '../../../api'
import { getAge, isIdCardType } from '../../../api/utils'
import { CHANGE_IDCARD_MSG, SET_CAMERA_TYPE } from './constants'

export const setCameraType = (data) => ({type: SET_CAMERA_TYPE,data: fromJS(data)
})

const changeIdCardMsg = (data) => ({type: CHANGE_IDCARD_MSG,data: fromJS(data)
})

export const getIdCardMsg = (url, isCamera = false) => {return async (dispatch) => {const res = await idCardAndlysisReq(url, isCamera)const type = isIdCardType(res)const { name, idcard, sex, authority, validDate } = resif (type === 1) {if (isName(name) && isIdCard(idcard)) {const age = getAge(idcard)const obj = { name, idcard, sex, age, type: 1, url }dispatch(changeIdCardMsg(obj))return obj} else {return isCamera ? '' : Toast.offline('信息获取错误,请重新上传!', 3)}} else if (type === 2) {const obj = { authority, validDate, type: 2, url }dispatch(changeIdCardMsg(obj))return obj} else {return isCamera ? '' : Toast.offline('信息获取错误,请重新上传!', 3)}}
}

export const clearIdCardMsg = () => {return (dispatch) => {dispatch(changeIdCardMsg({}))}
} 

插件版本

{"devDependencies": {"react-redux": "^7.2.4","redux": "^4.1.0","zzy-javascript-devtools": "^1.5.2"},"dependencies": {"html2canvas": "^1.4.1","immutable": "^4.0.0","redux-immutable": "^4.0.0","redux-thunk": "^2.4.1","styled-components": "^5.3.5"}
} 

想法

在之后用了一次html2canvas的节点截图,感觉可以将canvas.drawImage步骤改为html2canvas会更好一些,但是没有进行尝试,觉得可行。

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:43:36  更:2022-09-30 00:43:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 20:15:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码