要求实现用户上传身份证照片,自定义上传画面,自动截取身份证图片,并获取身份证上面的信息。如果机型不支持则留有保底手段:相册上传图片,获取身份证上的信息。
主要依赖 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 && close()} />{idCardType === 1 && ( )}{idCardType === 2 && ( )}请将身份证置于取景框内 {/* */} " 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;}}}}
`
//访问用户媒体设备的兼容方法
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中使用了immutable格式,不清楚的朋友可以先看一下文档,只是改变数据结构,其余的没什么变化。
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
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的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享