前言
在移动互联网时代,微信作为国内最大的社交平台,承载了大量的H5应用场景。从营销活动到企业应用,从支付功能到用户授权,微信环境下的H5开发已经成为前端工程师必备的技能。然而,微信内置浏览器和原生应用的特性差异,往往让开发者在实际开发中遇到各种棘手的问题。
本文将分享在微信H5开发过程中遇到的几个典型问题及解决方案,涵盖二维码识别、静默登录、iOS兼容性等多个方面,希望能帮助开发者们在微信开发中少走弯路,提高开发效率。
一、 长按图片为base64的图片,识别图中的二维码时,base64的字符个数有限制
经过测试大于大概为258286字符(大约500KB)的时候,在识别图时可以弹出正常的菜单,但是点击菜单项“识别图中二维码”则无反应。
需求背景: 拓展业务时,都会生成一个二维码页面用于分享给别人; 而二维码页面包含有,二维码图片,公司logo图,背景图,动态产品介绍文案 等构成的一个页面。
用于分享出去的东西最好的预期是,能把整个页面转换为图片分享出去效果是最好的,而不单单只分享一个二维码图片。所以就有以下需求点需要解决:
-
把页面转换为图片
-
让微信能正常识别图中的二维码
利用微信内置的长按功能即可以实现以上需求。在长按后,弹出来的菜单中,可以自己选择“保存为图片”,或者点击“识别图中二维码”直接进入推广页,或者也可以直接发送给朋友。
根据以上需求点,那么问题来了。
页面如何转换为图片呢?
通过html2canvas把页面转换为canvas,然后在通过canvas.toDataURL方法转换为base64图即可
用到的技术为html2canvas
如何使用呢?
直接在自己项目中安装
yarn add html2canvas -D
在项目中引用
import html2canvas from 'html2canvas';
// 把页面生成为base64图
html2canvas(document.body).then((canvas) => {
const base64Image = canvas.toDataURL('image/png');
console.log(`转换出的base64图为:${base64Image}`);
});
把转换出来的base64图,替换掉页面即可,用户是无感知的。
那么问题来了,通过canvas.toDataURL
转换出来的base64字符过长,超出了微信base64字符限制,这怎么办呢?
可以利用canvas.toDataURL 转换为image/jpeg 然后添加第二个参数,改变图片质量,把字符将到限制以下即可。
canvas.toDataURL('image/jpeg', 0.3) // 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量
更多详细的toDataURL方法的使用可以参考MDN文档
二、页面首次登录发起的微信静默登录后,后台重定向回到当前页面,要点击两次返回才能回到上一个页面问题。
要分析整个问题,得先理解微信的静默登录的流程是如何的,流程如下: 微信静默登录的地址为: https://open.weixin.qq.com/connect/oauth2/authorize?appid=[企业号appId]&redirect_uri=[后台回调url]&response_type=code&scope=snsapi_base&state=[前端页面url]#wechat_redirect
-
第一 ,当页面请求某个后台接口时,后台接口会返回一些东西,让你知道用户未登录;通常的做法是,后台会返回状态码401,然后前端根据401状态吗,执行location.replace([微信静默登录地址]),让页面加载这连接就会执行静默登录。
-
第二,加载以上连接后,微信会把响应的一些参数code和参数state指定的前端页面拼接到传入的后台回调url,然后执行这个后台url请求。
-
第三,请求后台时,后台会根据拼接传给他的参数,去获取用户的openid,和access_token,成功后,会重定向回到state参数指定的前端页面url。
原因分析:
后台重定向是无法替换掉历史记录的,所以导致这个问题。 对于这个问题的误解:当发起微信静默登录时,采用location.replace()时,一直以为,为什么不会替换掉历史记录,问题的考虑点就错了,问题点不在这,因为问题点是重定向不会替换掉历史记录。所以当用户登录成功后,点击第一次返回,会回到当前页,再次点击返回时,才能回到上一个页面,所以会出现,点击2次返回才能回到上一个页面问题。
那么问题来了,如何解决这个问题呢?
假如有两个页面A(不需要登录态),B(需要登录态)
首先得分析一下页面的历史记录如下:
-
当进入页面A时,历史记录为1,当点击跳转到B页面时,执行了微信静默登录,此时历史记录+1 即为2。
-
然后登录成功后重定向回到B页面,此时历史记录再+1 即为3了。
我们可以重定向回到B页面时,还未进入到B页面,就让历史回退history.go(-2)
到A页面(此时历史记录为1),然后A页面在根据一些参数来判断,跳转到B页面(此时的历史记录为2),即可实现当点击B页面可以直接回到A页面。
ps:这里我的项目主要用到了中间页进行登录了。没有中间页的情况,直接
history.go(-1)
即可。
还是直接上代码吧,代码(截取的是自己vue项目的一个实现,部分代码)如下:
vue项目为单页面应用,所以把逻辑放到全局路由守卫(当进入A,B页面时都会执行,即进入A或B路由时,首先会执行的逻辑判断
// 静默登录时,传给state的前端url参数,后边加了一个参数"?isLogin=true" 添加了这个参数,同时也解决了另一个静默登录遇到的坑,后续会讲到。
// const stateUrl = encodeURIComponent('http://www.demo1.com/?isLogin=true#/pageA');
// 连接如: `https://open.weixin.qq.com/connect/oauth2/authorize?appid=[企业号appId]&redirect_uri=[后台回调url]&response_type=code&scope=snsapi_base&state=${stateUrl}#wechat_redirect`
Vue.beforeEach((to, from, next) => {
if (!window.localStorage) {
next();
}
const prevUrl = localStorage.getItem('prevUrl');
if (prevUrl) { // 假如存在这个参数,说明是从静默登录成功后的页面跳回来的。即这里,判断为是从B页面跳回来的。
localStorage.removeItem('prevUrl');
next({ // 跳回静默登录成功后的页面,这里指的即是跳回到B页面。
path: `${prevUrl.split('#')[1]}`,
query: {
cache: false
}
});
}
if (/\?isLogin=true/g.test(location.href)) {
localStorage.setItem('prevUrl', `${location.hash.split('?')[0]}`); // 添加这prevUrl参数用于判断是否存在是从B跳到A的,相当于保存了B页面的连接,即静默登录成功后进入的页面连接。
history.go(-2); // 回退到A页面
}
next();
})
三、iphone6和iphone6s中发现,静默登录后页面不刷新,或者白屏
假如进入A页面时发起静默登录,成功登录后回调到A页面,要是A页面连接不变,A页面就不会刷新,导致,假如刚进入A页面,还未渲染DOM时,就发起静默的登录,重定向回来时,看起来就是白屏效果,或者假如你传给state的参数大于128字节时(即字符长度为64)。超过的话,会被截取掉。
想要预防state大于限制被截取掉,可以把前端的回调地址不要放在state参数里,而是自己取一个参数拼接放在后端的回调参数redirect_uri里即可,然后在跟后端约定就好,拼接如下:
const feUrl = 'http://www.demo1.com/?isLogin=true#/pageA'; // 前端地址
const authCallbackUrl = `${apiSourceDomain}/wechat/zlAuthCallback.jhtml?redirectUrl=${encodeURIComponent(state)}`; // 后端地址
location.replace(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=[企业号appId]&redirect_uri=${authCallbackUrl}&response_type=code&scope=snsapi_base&state=${stateUrl}#wechat_redirect`); //发起微信静默登录
即静默登录成功后,后台到时候想要302重定向回来前端页面,取的参数值应该为上边的redirectUrl参数了,而不是微信拼接的state参数了,这个跟后台约定好即可。
解决方案:
发起静默登录时,在参数state指定的前端回调页面连接里,加入任意参数即可。添加如下:
const feUrl = encodeURIComponent('http://www.demo1.com/?isLogin=true#/pageA');
location.replace(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=[企业号appId]&redirect_uri=[后台回调url]&response_type=code&scope=snsapi_base&state=${feUrl}#wechat_redirect`);
四、ios左右边缘滑动返回前进页面动画与h5用户自定义页面切换动画冲突问题
导致滑动切换页面时执行了ios的动画之后又执行自定义动画。看起来就是闪了一下自定义动画。
解决方案:
监听左右滑动手势和touchstart事件清除掉自定义动画即可。
同时加入滑动后多少ms后恢复动画,不然滑动后就真的没有自定义动画了 ,已防止用户不是边缘滑动时清除掉的自定义动画恢复。
多少ms 后恢复 建议设置为切换动画执行时间的两倍即可。
上边为什么要监听touchstart事件呢,因为经过测试发现滑动边缘返回或前进时,不一定执行滑动事件,因为封装的滑动事件是有最小距离的,超过才执行的。
然后又为了防止用户触摸屏幕时会执行touchstart事件从而导致切换动画清除掉,在click事件发生时要回复动画。
截取vue项目中的部分代码如下:
// <transition :name="transitionName">
// <router-view @click.native="onClick" v-swipe="{fn:onSwipe, trigger: 'touchstart'}" ></router-view>
//</transition>
export default {
data: {
return {
routeMap: {
count: 0,
'/': 0,
},
transitionName: '',
touchHandler: null,
}
}
watch: {
$route(to, from) {
// 切换路由,判断是前进页面,或后退页面,相应改变 this.transitionName = 'next';
// 或 this.transitionName = 'prev';
const toIndex = this.routeMap[to.path];
const fromIndex = this.routeMap[from.path];
if (toIndex !== undefined) {
if (!fromIndex || parseInt(toIndex, 10) > parseInt(fromIndex, 10)) {
this.transitionName = 'next';
} else {
this.transitionName = 'prev';
}
} else {
this.routeMap.count += 2;
this.routeMap[to.path] = this.routeMap.count;
// 第一个页面不需要动画,因为每个子路由的from都是根路由(即使你第一个访问的是子路由)
if (from.path !== '/') {
this.transitionName = 'next';
}
}
// 清除掉自定义动画
this.touchHandler && this.touchHandler();
}
}
methods: {
...
// 监听点击事件
onClick() {
this.touchHandler = null; // 恢复自定义动画
},
//监听向左向右滑动,和touchstart事件
onSwipe() { // 解决ios系统版本在11.0以上,滑动会执行原生动画和vue自定义动画冲突问题
if (Device.isIOS() && (Device.iosVersionCompare('11.0') !== -1)) { // 大于11.0
this.touchHandler = () => {
this.transitionName = ''; // 清除掉自定义动画
};
this.Timer && clearTimeout(this.Timer);
this.Timer = setTimeout(() => { // 恢复自定义动画
this.touchHandler = null;
}, 2000);
}
},
...
}
}
ps: 这个不是微信里的坑,但是也一起记录了,触摸事件可以自己添加,vue-touch触摸插件,上边的v-swipe指令我是自己在项目里通过 Vue.derective('swipe',{...}) 这种方式自己添加的。
五、微信支付完成后回调页面无法正常跳转问题
在微信H5环境下发起支付后,支付完成页面无法按照预期跳转回指定页面,或者页面显示空白。
原因分析:
微信支付完成后的回调机制在不同版本的微信客户端中表现不一致,尤其是在iOS和Android设备上存在差异。主要原因包括:
-
微信支付成功页面的
window.location.href
跳转被微信内置浏览器拦截 -
微信支付完成后返回原页面时,页面可能处于冻结状态
-
支付完成后的页面状态管理不当导致页面无法正常渲染
解决方案:
-
使用微信JSAPI支付时,确保在支付成功回调中使用
WeixinJSBridge.call('closeWindow')
先关闭支付页面,再尝试跳转 -
采用定时器轮询方式检测支付状态,而不是依赖微信的回调
-
使用sessionStorage或URL参数保存支付前的页面状态,以便支付完成后恢复
// 支付成功后的处理逻辑示例
function onPaymentSuccess() {
// 1. 保存支付结果
sessionStorage.setItem('paymentResult', 'success');
// 2. 尝试关闭支付页面
if (typeof WeixinJSBridge !== 'undefined') {
WeixinJSBridge.call('closeWindow');
}
// 3. 设置定时器,确保页面能正确跳转
setTimeout(() => {
window.location.href = '/payment-success.html';
}, 1000);
}
// 页面加载时检查支付结果
window.onload = function() {
const paymentResult = sessionStorage.getItem('paymentResult');
if (paymentResult === 'success') {
// 执行支付成功后的操作
showSuccessMessage();
sessionStorage.removeItem('paymentResult');
}
};
六、微信内置浏览器分享功能无效问题
在微信环境下,自定义的分享标题、描述和图片不生效,默认显示URL或页面标题。
原因分析:
微信内置了分享机制,需要通过微信JS-SDK进行配置才能实现自定义分享内容。主要原因包括:
-
未正确引入微信JS-SDK或未完成SDK初始化
-
分享接口配置参数错误或签名验证失败
-
微信客户端版本差异导致的API兼容性问题
解决方案:
-
正确引入微信JS-SDK并完成初始化配置
-
向服务器请求正确的签名配置
-
为不同的分享场景(朋友圈、好友、QQ等)分别配置分享内容
// 微信分享配置示例
function initWechatShare() {
// 1. 从服务器获取签名配置
fetch('/api/wechat/sign?url=' + encodeURIComponent(location.href.split('#')[0]))
.then(response => response.json())
.then(config => {
// 2. 初始化微信JS-SDK
wx.config({
debug: false, // 生产环境关闭调试模式
appId: config.appId,
timestamp: config.timestamp,
nonceStr: config.nonceStr,
signature: config.signature,
jsApiList: ['updateTimelineMessage', 'updateAppMessageShareData', 'onMenuShareAppMessage', 'onMenuShareTimeline']
});
// 3. 配置分享内容
const shareData = {
title: '分享标题',
desc: '分享描述内容',
link: location.href,
imgUrl: 'https://example.com/share-icon.png',
success: function() {
// 用户确认分享后执行的回调函数
console.log('分享成功');
},
cancel: function() {
// 用户取消分享后执行的回调函数
console.log('分享取消');
}
};
// 兼容不同版本的微信
wx.ready(function() {
// 新接口
if (wx.updateAppMessageShareData && wx.updateTimelineMessage) {
wx.updateAppMessageShareData(shareData);
wx.updateTimelineMessage(shareData);
} else {
// 旧接口
wx.onMenuShareAppMessage(shareData);
wx.onMenuShareTimeline(shareData);
}
});
})
.catch(error => {
console.error('微信分享配置失败:', error);
});
}
// 页面加载时初始化分享功能
window.onload = function() {
initWechatShare();
};
七、微信环境下localStorage跨域限制问题
在微信内置浏览器中,当从一个域名跳转到另一个域名时,无法访问之前设置的localStorage数据。
原因分析:
微信浏览器对localStorage的限制比普通浏览器更为严格,主要原因包括:
-
微信的安全策略限制了不同域名之间的localStorage访问
-
微信内置浏览器在某些情况下会使用独立的存储空间
-
微信客户端的缓存机制可能导致localStorage数据无法正确同步
解决方案:
-
使用URL参数传递必要的临时数据
-
对于需要持久化或跨域共享的数据,使用后端存储方案
-
利用iframe实现不同域名间的数据传递
-
考虑使用微信开放平台的统一登录机制
// 使用URL参数传递数据的示例
function navigateWithData(targetUrl, data) {
// 将数据转换为查询字符串
const queryParams = new URLSearchParams();
for (const key in data) {
if (data.hasOwnProperty(key)) {
queryParams.append(key, JSON.stringify(data[key]));
}
}
// 拼接URL并跳转
const fullUrl = `${targetUrl}?${queryParams.toString()}`;
window.location.href = fullUrl;
}
// 在目标页面解析URL参数
function parseUrlParams() {
const params = new URLSearchParams(window.location.search);
const result = {};
for (const [key, value] of params.entries()) {
try {
result[key] = JSON.parse(value);
} catch (e) {
result[key] = value;
}
}
return result;
}
// 使用iframe实现跨域数据传递的示例
function sendDataToCrossDomain(targetOrigin, data) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${targetOrigin}/receiver.html`;
iframe.onload = function() {
iframe.contentWindow.postMessage(data, targetOrigin);
setTimeout(() => {
document.body.removeChild(iframe);
}, 1000);
};
document.body.appendChild(iframe);
}
// 在接收数据的页面添加事件监听
window.addEventListener('message', function(event) {
// 验证消息来源
if (event.origin !== 'https://trusted-domain.com') {
return;
}
// 处理接收到的数据
console.log('接收到跨域数据:', event.data);
// 可以将数据保存到localStorage
localStorage.setItem('crossDomainData', JSON.stringify(event.data));
});
八、微信环境下音频自动播放被禁止问题
在微信H5页面中,音频文件无法在页面加载后自动播放,需要用户交互才能触发播放。
原因分析:
为了提升用户体验并节省流量,微信内置浏览器和大多数现代浏览器都禁止了媒体的自动播放功能。主要限制包括:
-
音频必须由用户交互(如点击、触摸)触发播放
-
即使设置了
autoplay
属性,在微信环境中也不会自动播放 -
iOS设备上的限制更为严格,必须由用户明确的交互操作才能播放音频
解决方案:
-
在用户首次交互时预加载并播放音频,然后暂停以便后续使用
-
使用微信JS-SDK的
wx.ready
事件来触发音频播放 -
为音频元素添加用户交互触发的播放按钮
-
考虑使用Web Audio API实现更灵活的音频控制
// 微信环境下音频播放解决方案示例
let audioPlayer = null;
function initAudio() {
// 创建音频元素
audioPlayer = document.createElement('audio');
audioPlayer.src = 'path/to/audio.mp3';
audioPlayer.preload = 'auto';
// 尝试在页面加载时进行预加载
audioPlayer.load();
// 监听错误事件
audioPlayer.addEventListener('error', function(e) {
console.error('音频加载失败:', e);
});
}
// 预加载并准备音频播放
function prepareAudio() {
if (!audioPlayer) {
initAudio();
}
// 尝试播放然后立即暂停,为后续自动播放做准备
audioPlayer.play().then(() => {
audioPlayer.pause();
console.log('音频预加载成功');
}).catch(error => {
console.warn('音频预加载需要用户交互:', error);
});
}
// 自动播放音频的函数
function playAudio() {
if (!audioPlayer) {
initAudio();
}
audioPlayer.play().then(() => {
console.log('音频播放成功');
}).catch(error => {
console.error('音频播放失败:', error);
// 显示播放按钮,让用户手动触发
showPlayButton();
});
}
// 为页面添加首次用户交互事件监听
function setupUserInteraction() {
// 监听任何用户交互
function handleUserInteraction() {
prepareAudio();
// 移除监听,避免重复触发
document.removeEventListener('click', handleUserInteraction);
document.removeEventListener('touchstart', handleUserInteraction);
}
document.addEventListener('click', handleUserInteraction);
document.addEventListener('touchstart', handleUserInteraction);
}
// 使用微信JS-SDK触发音频播放
function initWechatAudio() {
if (typeof wx !== 'undefined') {
wx.ready(function() {
prepareAudio();
});
}
}
// 页面加载时初始化
window.onload = function() {
setupUserInteraction();
initWechatAudio();
};
九、iOS微信端点击事件穿透问题
在iOS设备的微信内置浏览器中,快速点击某些元素时,点击事件会穿透当前元素,触发下方元素的点击事件。
原因分析:
这个问题主要出现在iOS设备的微信浏览器中,与iOS的触摸事件处理机制有关。主要原因包括:
-
iOS的300ms点击延迟导致的事件处理问题
-
微信浏览器对触摸事件的特殊处理
-
元素的z-index和定位问题导致的事件冒泡异常
解决方案:
-
使用
touch-action: manipulation
CSS属性禁止双击缩放,减少点击延迟 -
对关键元素添加
fastclick
库来解决点击延迟问题 -
使用
e.stopPropagation()
阻止事件冒泡 -
为元素添加适当的触摸反馈,让用户明确知道点击已被触发
// 使用fastclick解决点击穿透问题
// 首先安装fastclick: npm install fastclick --save
import FastClick from 'fastclick';
// 初始化fastclick
function initFastClick() {
if ('addEventListener' in document && 'ontouchstart' in window) {
FastClick.attach(document.body);
console.log('FastClick初始化成功');
}
}
// 手动处理点击穿透问题的示例
function handleClickWithoutPropagation(element) {
element.addEventListener('click', function(e) {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 阻止默认行为
// 执行点击操作
console.log('元素被点击,事件不会穿透');
}, false);
// 同时处理触摸事件
element.addEventListener('touchstart', function(e) {
e.stopPropagation();
// 可以在这里添加触摸反馈
element.style.opacity = '0.8';
}, false);
element.addEventListener('touchend', function(e) {
e.stopPropagation();
// 恢复触摸反馈
setTimeout(() => {
element.style.opacity = '1';
}, 100);
}, false);
}
// 批量处理页面中的点击元素
function fixClickPropagation() {
const clickableElements = document.querySelectorAll('.button, .link, [data-prevent-propagation]');
clickableElements.forEach(element => {
handleClickWithoutPropagation(element);
});
}
// 页面加载时初始化
window.onload = function() {
initFastClick();
fixClickPropagation();
};
// 对于Vue项目的解决方案
// 在main.js中添加
import Vue from 'vue';
import FastClick from 'fastclick';
// 解决iOS上的点击穿透问题
FastClick.attach(document.body);
// 或者添加自定义指令处理点击事件
Vue.directive('no-propagation', {
inserted: function(el) {
el.addEventListener('click', function(e) {
e.stopPropagation();
});
el.addEventListener('touchstart', function(e) {
e.stopPropagation();
});
}
});
新增微信H5开发常见问题概览
参考
最后,创作不易请允许我插播一则自己开发的小程序广告,感兴趣可以访问体验:
【「合图图」产品介绍】
- 主要功能为:本地添加相册图片进行无限长图高清拼接,各种布局拼接等
- 安全:无后台服务无需登录,全程设备本地运行,隐私100%安全;
- 高效:自由布局+实时预览,效果所见即所得;
- 高清:秒生高清拼图,一键保存相册。
立即体验 → 合图图 或微信小程序搜索「合图图」
如果觉得本文有用,点个赞👍+收藏🔖支持我吧!