微信H5开发中的那些“坑“:实战经验与解决方案

前言

在移动互联网时代,微信作为国内最大的社交平台,承载了大量的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跳转被微信内置浏览器拦截

  • 微信支付完成后返回原页面时,页面可能处于冻结状态

  • 支付完成后的页面状态管理不当导致页面无法正常渲染

解决方案:

  1. 使用微信JSAPI支付时,确保在支付成功回调中使用WeixinJSBridge.call('closeWindow')先关闭支付页面,再尝试跳转

  2. 采用定时器轮询方式检测支付状态,而不是依赖微信的回调

  3. 使用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兼容性问题

解决方案:

  1. 正确引入微信JS-SDK并完成初始化配置

  2. 向服务器请求正确的签名配置

  3. 为不同的分享场景(朋友圈、好友、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数据无法正确同步

解决方案:

  1. 使用URL参数传递必要的临时数据

  2. 对于需要持久化或跨域共享的数据,使用后端存储方案

  3. 利用iframe实现不同域名间的数据传递

  4. 考虑使用微信开放平台的统一登录机制

// 使用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设备上的限制更为严格,必须由用户明确的交互操作才能播放音频

解决方案:

  1. 在用户首次交互时预加载并播放音频,然后暂停以便后续使用

  2. 使用微信JS-SDK的wx.ready事件来触发音频播放

  3. 为音频元素添加用户交互触发的播放按钮

  4. 考虑使用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和定位问题导致的事件冒泡异常

解决方案:

  1. 使用touch-action: manipulationCSS属性禁止双击缩放,减少点击延迟

  2. 对关键元素添加fastclick库来解决点击延迟问题

  3. 使用e.stopPropagation()阻止事件冒泡

  4. 为元素添加适当的触摸反馈,让用户明确知道点击已被触发

// 使用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开发常见问题概览

参考

HTMLCanvasElement.toDataURL()

html2canvas

微信网页授权

最后,创作不易请允许我插播一则自己开发的小程序广告,感兴趣可以访问体验:

【「合图图」产品介绍】

  • 主要功能为:本地添加相册图片进行无限长图高清拼接,各种布局拼接等
  • 安全:无后台服务无需登录,全程设备本地运行,隐私100%安全;
  • 高效:自由布局+实时预览,效果所见即所得;
  • 高清:秒生高清拼图,一键保存相册。

立即体验 → 合图图  或微信小程序搜索「合图图」

如果觉得本文有用,点个赞👍+收藏🔖支持我吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值