三重水的博客

在变化当中不断记录自己,充实自己,浏览自己

微信h5页面开发遇到的坑

一、 长按图片为base64的图片,识别图中的二维码时,base64的字符个数有限制

经过测试大于大概为258286字符(大约500KB)的时候,在识别图时可以弹出正常的菜单,但是点击菜单项“识别图中二维码”则无反应。

需求背景: 拓展业务时,都会生成一个二维码页面用于分享给别人; 而二维码页面包含有,二维码图片,公司logo图,背景图,动态产品介绍文案 等构成的一个页面。

用于分享出去的东西最好的预期是,能把整个页面转换为图片分享出去效果是最好的,而不单单只分享一个二维码图片。所以就有以下需求点需要解决:

  • 把页面转换为图片
  • 让微信能正常识别图中的二维码

利用微信内置的长按功能即可以实现以上需求。在长按后,弹出来的菜单中,可以自己选择“保存为图片”,或者点击“识别图中二维码”直接进入推广页,或者也可以直接发送给朋友。

根据以上需求点,那么问题来了。

页面如何转换为图片呢?

通过html2canvas把页面转换为canvas,然后在通过canvas.toDataURL方法转换为base64图即可

用到的技术为html2canvas

如何使用呢?

直接在自己项目中安装

1
yarn add html2canvas -D

在项目中引用

1
2
3
4
5
6
7
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 然后添加第二个参数,改变图片质量,把字符将到限制以下即可。

1
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路由时,首先会执行的逻辑判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 静默登录时,传给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里即可,然后在跟后端约定就好,拼接如下:

1
2
3
  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指定的前端回调页面连接里,加入任意参数即可。添加如下:

1
2
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项目中的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// <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’,{…}) 这种方式自己添加的。

参考

HTMLCanvasElement.toDataURL()

html2canvas

微信网页授权

评论