遇到一个问题,H5调用摄像头扫码,安卓环境下基本所有机型都可以调起摄像头,但是IOS 只有少部分机型才能调用,参考了网上很多例子
比如以下两位老大哥的例子
Vue 移动端实现调用相机扫描二维码或条形码_无解的菜鸟晖的博客-CSDN博客_browsermultiformatreader
vue 实现扫条形码与二维码 H5 兼容 苹果IOS_→_→BéLieve的博客-CSDN博客
实际测试下来安卓确实可以扫码,但是识别率不高(具体不知道为什么,可能是因为扫码页面是集成在其他微信公众号的应用的工作台中)。
而且IOS大部分都不行,显示 “此浏览器不支持流API”或者什么都不显示。
后来经过咨询,发现可以通过调用微信JS-SDK进行扫码,
于是在官网找到了一篇文章,链接如下
注册公众号流程在此不做描述,下面接着说调用过程。
在初始化描述中,有这么一段话,如图所示,必须要有以下几个参数才能进行sdk初始化,进而进行后续操作。
需要自己申请公众号,获取appId和appKey,申请流程在此不做描述。
import wx from 'weixin-js-sdk'
handleClickScan() {
//url必须是不带#号的
//这里【url参数一定是去参的本网址】,请求后端接口换取signature
let ua = navigator.userAgent.toLowerCase();
if (/iphone|ipad|ipod/.test(ua)) {
this.newUrl = window.location.href.split("#")[0];
} else if (/android/.test(ua)) {
this.newUrl = window.location.href;
}
getWxScanConfig(this.newUrl).then(res => {
wx.config({
// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
debug: false,
// 必填,公众号的唯一标识
appId: res.data.data.appId,
// 必填,生成签名的时间戳
timestamp: res.data.data.timestamp,
// 必填,生成签名的随机串
nonceStr: res.data.data.nonceStr,
// 必填,签名
signature: res.data.data.signature,
// 必填,需要使用的JS接口列表
jsApiList: ['scanQRCode','checkJsApi']
})
})
wx.ready(() => {
// config信息验证成功后会执行ready方法,所有接口调用都必须在config接口获得结果之后
// config 是一个客户端的异步操作,所以如果需要在页面加载时调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行.对于用户触发是才调用的接口,则可以直接调用,不需要放在ready函数中
wx.checkJsApi({
// 判断当前客户端版本是否支持指定JS接口
jsApiList: ['scanQRCode'],
success: res => {
// 以键值对的形式返回,可用true,不可用false。如:{"checkResult":{"scanQRCode":true},"errMsg":"checkJsApi:ok"}
if (res.checkResult.scanQRCode === true) {
wx.scanQRCode({
// 微信扫一扫接口
desc: 'scanQRCode desc',
needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
// 可以指定扫二维码还是一维码,默认二者都有
scanType: ['qrCode', 'barCode'],
success: result => {
// 当needResult 为 1 时,扫码返回的结果
let result = res.resultStr;
//具体处理xxxx
}
})
} else {
this.$Toast('抱歉,当前客户端版本不支持扫一扫')
}
},
fail: res => {
// 检测getNetworkType该功能失败时处理
this.$Toast('fail' + res)
}
})
})
/* 处理失败验证 */
wx.error(res => {
// config 信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名
this.$Toast('配置验证失败: ' + res.errMsg)
})
}
不用说,生成签名及其他参数在后台最安全,所以,下面来写一个接口,按照微信文档签名过程进行签名即可,没什么复杂的逻辑。
/**
* 获取微信配置
*
* @author: liudongxin
* @date: 2022/11/10 - 19:12
*/
@ApiOperation(value = "获取微信配置")
@RequestMapping(value = "/getWxScanConfig", method = RequestMethod.GET)
public AjaxResult<WxConfigResponse> getWxScanConfig(String url) {
WxConfigResponse configResponse = wxUtils.getScanConfig(url);
if(StringUtils.isNotEmpty(configResponse)){
return AjaxResult.success("获取配置成功", configResponse);
}
return AjaxResult.errorStr("初始化配置失败!");
}
工具类如下
/**
* 微信工具
*
* @author: liudongxin
* @date: 2022/11/17 9:25
*/
@Slf4j
@Component
public class ByWxUtils {
@Autowired
private WxAppConfig wxAppConfig;
static ReentrantLock reentrantLock = new ReentrantLock();
/**
* 获取access_token
*/
private String getToken() {
String accessToken = RedisUtils.getCacheObject(RemoteVisitConstants.WX_ACCESS_TOKEN);
if (StringUtils.isBlank(accessToken)){
//尝试获取锁,获取到的执行没获取到的睡眠再次请求
if (reentrantLock.tryLock()) {
String url = String.format(RemoteVisitConstants.GET_ACCESS_TOKEN_URL, wxAppConfig.getAppId(), wxAppConfig.getAppSecret());
//请求工具类,代码放最后
JSONObject jsonObject = RestTemplateUtils.getJSONObject(url,null);
try {
if (StringUtils.isNotEmpty(jsonObject)) {
accessToken = jsonObject.getString("access_token");
String expires = jsonObject.getString("expires_in");
RedisUtils.setCacheObject(RemoteVisitConstants.WX_ACCESS_TOKEN, accessToken);
RedisUtils.expire(RemoteVisitConstants.WX_ACCESS_TOKEN, Long.parseLong(expires), TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
log.info("错误代码:" + jsonObject + ":" + e.getMessage());
} finally {
reentrantLock.unlock();
}
} else {
try {
Thread.sleep(100);
accessToken = getToken();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return accessToken;
}
/**
* 微信accessToken接口得到的accessToken可以保存2小时,为了
* 设置定时,将定时设置为 1小时55分钟 ~ 2小时之内
* access_token 的有效期目前为 2 个小时,需定时刷新,重复获取将导致上次获取的 access_token 失效;
* 建议开发者使用中控服务器统一获取和刷新 access_token,其他业务逻辑服务器所使用的 access_token 均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致 access_token 覆盖而影响业务;
* access_token 的有效期通过返回的 expires_in 来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。在刷新过程中,中控服务器可对外继续输出的老 access_token,此时公众平台后台会保证在5分钟内,新老 access_token 都可用,这保证了第三方业务的平滑过渡;
* access_token 的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新 access_token 的接口,这样便于业务服务器在API调用获知 access_token 已超时的情况下,可以触发 access_token 的刷新流程。
*/
public void refreshAccessToken() {
WxAccessToken token = JSONObject.parseObject(getToken(),WxAccessToken.class);
String accessToken = token.getAccess_token();
//微信token2小时过期,每小时重新获得一次,
RedisUtils.setCacheObject(RemoteVisitConstants.WX_ACCESS_TOKEN, accessToken);
RedisUtils.expire(RemoteVisitConstants.WX_ACCESS_TOKEN, Long.parseLong(token.getExpires_in()), TimeUnit.MILLISECONDS);
}
/**
* 获取扫描配置
* @param localUrl 调用页面的去参数路径
*/
public WxConfigResponse getScanConfig(String localUrl) {
String accessToken = getToken();
//获取ticket 地址
String url = String.format(RemoteVisitConstants.GET_JSAPI_TICKET_URL, accessToken);
//返回值
WxAccessToken wxAccessToken = RedisUtils.getCacheObject(RemoteVisitConstants.WX_JSAPI_TICKET);
ByWxConfigResponse ticket = null;
if (StringUtils.isNotEmpty(wxAccessToken)){
ticket = createTicket(wxAccessToken,localUrl);
}else {
//请求工具类,代码放最后
ResponseEntity<WxAccessToken> wxAccessTokenResponseEntity = RestTemplateUtils.get(url, WxAccessToken.class);
if (wxAccessTokenResponseEntity.getStatusCodeValue() == 200){
wxAccessToken = wxAccessTokenResponseEntity.getBody();
//判断返返回值
if (StringUtils.isNotNull(wxAccessToken) && wxAccessToken.getErrcode() == 0){
ticket = createTicket(wxAccessToken,localUrl);
}else {
log.error("获取ticket失败:"+wxAccessToken);
}
}else {
log.error("获取ticket失败:"+wxAccessTokenResponseEntity.getBody());
}
}
return ticket;
}
private WxConfigResponse createTicket(WxAccessToken wxAccessToken,String localUrl) {
WxConfigResponse wxConfigResponse = new WxConfigResponse();
//生成签名的随机串
String randomSalt = Md5Utils.getRandomSalt(16);
wxConfigResponse.setNonceStr(randomSalt);
wxConfigResponse.setAppId(byWxAppConfig.getAppId());
wxConfigResponse.setTimestamp(DateUtils.getNowStamp());
RedisUtils.setCacheObject(RemoteVisitConstants.WX_JSAPI_TICKET, wxAccessToken);
RedisUtils.expire(RemoteVisitConstants.WX_JSAPI_TICKET, Long.parseLong(wxAccessToken.getExpires_in()), TimeUnit.MILLISECONDS);
String toBeSigned = "jsapi_ticket="+wxAccessToken.getTicket()+"&noncestr="+wxConfigResponse.getNonceStr()+"×tamp="+wxConfigResponse.getTimestamp()+"&url="+localUrl;
wxConfigResponse.setSignature(StringUtils.getSha1(toBeSigned));
return wxConfigResponse;
}
}
/**
* @Description: 微信公众平台获取
* @author liudongxin
*/
@Configuration
@Data
public class WxConfigResponse {
/**
* 必填,appId
*/
@ApiModelProperty("必填,appId")
private String appId;
/**
* 必填,时间戳
*/
@ApiModelProperty("必填,时间戳")
private Long timestamp;
/***
* 必填,生成签名的随机串
*/
@ApiModelProperty("必填,生成签名的随机串")
private String nonceStr;
/***
* 必填,签名
*/
@ApiModelProperty("必填,签名")
private String signature;
}
本以为,调用接口返回参数之后,就可以进行初始化了。没想到,IOS还是有问题
签名错误!!! 好吧,不知道啥问题。看到wx.config 有 debug 参数,于是打开该参数,看看有什么数据返回。
结果返回的是URL不正确,真实签名的URL是这个URL。但是为什么安卓和IOS都是一样调用的,IOS不行?
于是又在网上搜了一番。
看到大家有类似的描述:
【IOS】:ios微信端,路由变化时,微信认为SPA的url是不变的。
【Android】:android微信端,路由变化时,SPA的url是会变的(官方在安卓6.2版本,才对SPA变化作了支持)
所以,发起签名的url必须是微信锁定的url。
于是想到在App.Vue里边就判断 页面路径然后直接缓存,后边直接调用。
就把下边这段代码放到了App.vue中,vue生命周期的created方法中。
//url必须是不带#号的
//这里【url参数一定是去参的本网址】,请求后端接口换取signature
let url;
let ua = navigator.userAgent.toLowerCase();
if (/iphone|ipad|ipod/.test(ua)) {
url = window.location.href.split("#")[0];
} else if (/android/.test(ua)) {
url = window.location.href;
}
getWxScanConfig(url).then(res => {
wx.config({
// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
debug: true,
// 必填,公众号的唯一标识
appId: res.data.data.appId,
// 必填,生成签名的时间戳
timestamp: res.data.data.timestamp,
// 必填,生成签名的随机串
nonceStr: res.data.data.nonceStr,
// 必填,签名
signature: res.data.data.signature,
// 必填,需要使用的JS接口列表
jsApiList: ['scanQRCode','checkJsApi']
})
})
然后在扫码页面进行扫码时候,直接调用微信扫码方法
wx.ready(() => {
// config信息验证成功后会执行ready方法,所有接口调用都必须在config接口获得结果之后
// config 是一个客户端的异步操作,所以如果需要在页面加载时调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行.对于用户触发是才调用的接口,则可以直接调用,不需要放在ready函数中
wx.checkJsApi({
// 判断当前客户端版本是否支持指定JS接口
jsApiList: ['scanQRCode'],
success: res => {
// 以键值对的形式返回,可用true,不可用false。如:{"checkResult":{"scanQRCode":true},"errMsg":"checkJsApi:ok"}
if (res.checkResult.scanQRCode === true) {
wx.scanQRCode({
// 微信扫一扫接口
desc: 'scanQRCode desc',
needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
// 可以指定扫二维码还是一维码,默认二者都有
scanType: ['qrCode', 'barCode'],
success: result => {
// 当needResult 为 1 时,扫码返回的结果
let result = res.resultStr;
//具体处理xxxx
}
})
} else {
this.$Toast('抱歉,当前客户端版本不支持扫一扫')
}
},
fail: res => {
// 检测getNetworkType该功能失败时处理
this.$Toast('fail' + res)
}
})
})
/* 处理失败验证 */
wx.error(res => {
// config 信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名
this.$Toast('配置验证失败: ' + res.errMsg)
})
}
事实证明,还是不行。依然是一样的错误,不过有了些许变化,就是在首页刚进去的时候就开始报错,点击跳转到扫码页面,就报了另一个错误。简直离谱(还是自己太菜了)
于是又开始了我的百度旅程
看到了如下描述:
- 建议按如下顺序检查:
- 确认config正确通过。
- 如果是在页面加载好时就调用了JSAPI,则必须写在wx.ready的回调中。
- 确认config的jsApiList参数包含了这个JSAPI。
但是我做的上述都没有,猜测可能是因为调用了两次 wx.config,一次扫码页面,一次 App.vue 初始化,而且URL路径不对。
又找到了一些描述,在 permission.js 获取URL路径并缓存,都尝试了一遍,还是不行
继续寻找,发现一个如下的描述
地址栏问题:push的跳转不能被写入ios微信浏览器的地址栏
处理: push跳转改为window.loaction.href跳转
window.loaction.href 跳转才能改变地址栏的变化,才能签名成功
好吧,再做一次尝试,因为我做的扫码是A为主页,B页有一个按钮可以点击扫码,
是通过A页面 this.$router.push 跳转的B页面,可能微信JS检测到的页面发生了变化与签名不一致。
改为 window.location.href = "/path"; 其中 path替换为B页面 具体的path。
此时再次点击扫码,发现全部成功了IOS和安卓均可打开摄像头。
最后需要注意的是 如果扫码中 存在特殊符号 如 #,IOS 返回结果之前可能会进行处理,注意打印查看。
wxJs.ready(function () {
wxJs.checkJsApi({
jsApiList: ['scanQRCode'],
success: function (res) {
if (res.checkResult.scanQRCode === true) {
wxJs.scanQRCode({ // 微信扫一扫接口
needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
success: function (result) {
// 当needResult 为 1 时,扫码返回的结果
// 注意如果扫码中 存在特殊符号 如 #,IOS 返回结果可能会被处理。
this.result = result.resultStr;
}
})
} else {
failedTip("抱歉,当前客户端版本不支持扫一扫");
}
},
fail: function (res) {
// 检测getNetworkType该功能失败时处理
failedTip('fail' + res)
}
});
});