小程序踩坑记录

8/18/2021 分享

# 背景

最近接了一个微信小程序的需求,于是乎走上了边学边做(踩坑)的道路。在开发过程中总结了一些自己遇到的问题和解决方案。现在把它分享出来,希望能给后面有需要的同学提供一定的帮助。

# 技术选型

# Taro vs uni-app

Taro 和 uni-app 目前是市面上最火的两个框架,Taro 我个人比较熟悉一点,对 React jsx 的支持比较好,社区生态和轮子也比较多,如果是 React 技术栈,建议无脑选 Taro。虽然 Taro3.0 现在已经支持 Vue 了,但在 Vue 相关生态上还是没有 uni-app 成熟

相同点:

  • 支持微信、百度、支付宝、今日头条等小程序,H5,以及 iOS 和 Android 的 App
  • 支持使用 npm/yarn 安装管理第三方依赖
  • 支持使用 ES6 甚至更新的 ES 规范
  • 支持使用 less/scss/ts 等预编译器
  • 支持进行应用状态管理,taro 支持 Redux/Mobx,uni-app 支持 vuex
  • 支持 weex 方式原生渲染,taro 支持 expo, uni-app 支持 nvue

维度对比:

  • 技术栈:使用的技术框架,对比技术上的差别
  • 开发工具:适合开发人员的工具,能为开发带来锦上添花的效果
  • 多端支持度:真实运行项目到各平台,对比平台差别抹平程度
  • 组件库/工具库:社区生态是否繁荣,是否有大量可用轮子
  • 运行性能:框架是否带来了额外的性能开销,下降用户体验

总结:

  • Taro vue 对比 uni-app 可能需要踩更多的坑
  • uni-app 在多平台的运行效果更好
  • uni-app 自带的 Hbuilder X,可以快速对项目进行构建和打包并实时热更新渲染到微信开发者工具
  • 从 vue 迁移到 uni-app 的学习成本、开发时间和风险都比较低

# 项目方案

由于整个项目的开发周期比较短,为了尽可能的节省开发时长和开发成本,前端页面架构主要采用了web-view嵌套之前公众号H5的方案去开发,通过web-view动态载入url地址的方式去渲染页面。微信小程序提供的web-view组件还是比较友好,基本能满足我们大部分的需求。但在 webview 通信上的支持却不是很好。

小程序官方web-view提供了bindmessage这个方法让网页向小程序传递消息,但是却会在特定时机(小程序后退/组件销毁/分享)触发并收到消息,这样异步的消息通信机制,如果对于需要实时通信的页面建议抛弃 web-view 嵌套 H5 的方案。

# web-view 相关

# 什么是 web-view

web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面

在 uni-app 的 nvue 中使用时需要手动设置宽高

# 为什么要使用 web-view

按正常的小程序开发流程,我们修改了代码都需要提交新版本到微信审核。这就会导致整个项目的灵活性不够,如果代码审核不通过,就需要重新改代码,再进行审核。而如果采用 web-view,我们在修改了一些简单的文案和代码时就不需要等待微信官方审核,这使得项目操作起来十分灵活。

# web-view 中遇到的问题

# 疑难杂症

  • 每个页面只能有一个web-view,且加载的web-view组件一定是全屏

  • 微信小程序端的web-view组件默认是微信原生的导航栏

    配置navigationStyle: customweb-view 组件无效,所以如果有自定义导航栏的需求无法实现

  • web-view在小程序中层级很高,会覆盖微信小程序大部分原生组件

    查了网上web-view的相关资料,发现有一种方式可以覆盖web-view组件。将cover-view覆盖在web-view上,并把cover-view写到web-view里面。cover-view样式 fixed,层级设为最大。这个方案在部分机型上不显示,需要在cover-view显示时加一个延时。

    微信开发者工具调试不会显示嵌套的 cover-view,只有真机可以渲染显示

      <web-view :src="https://www.baidu.com/" v-if="showView">
        <cover-view class="view-wrapper" aria-role="button">
          ...
        </cover-view>
      </web-view>
    
      <script>
        export default {
          data() {
            showView: false, //默认不显示
          },
          onLoad() {
            setTimeout(() => {
              this.showView = true;
            }, 300)
          }
        }
      </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    .view-wrapper {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      height: 100%;
      z-index: 9999;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

  • cover-view 是覆盖在原生组件之上的文本视图,只支持嵌套 cover-view、cover-image
  • cover-view 默认是不可以设置背景图及 box-shadow,需使用 cover-image 实现
  • cover-view 不支持 text-decoration

# web-view 通信

最初按官方的给的 demo 走了一遍发现无法通信,在控制台对 uni 对象进行打印也为空。后面尝试了改 uni 变量等各种方式,最后按原方式又莫名其妙的解决了这个问题,初步怀疑是环境配置引起的问题。后续有思路了会把这篇文档更新。基于现有的解决方案,最后总结了一套适用的通信方式

H5 主动通信

首先在 H5 中的 index.html 中引入微信的 sdk 和 uniapp 的 js 包(这个很重要)

<!-- 微信 JS-SDK 如果不需要兼容小程序,则无需引用此 JS 文件,这个sdk必须在uni之前引用 -->
<script
  type="text/javascript"
  src="//res.wx.qq.com/open/js/jweixin-1.4.0.js"
></script>
<!-- uni 的 SDK,必须引用 -->
<script
  type="text/javascript"
  src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"
></script>
1
2
3
4
5
6
7
8
9
10

在需要通信的页面进行 uni.webview 的初始化

webview只有在plusready后,才能对plus.storage进行操作,或者调用uni-app的代码

mounted() {
    this.$nextTick(() => {
      // 初始化uni.webview
      document.addEventListener("UniAppJSBridgeReady", function() {
        console.log("初始化uniapp的API成功");
      });
    });
}
1
2
3
4
5
6
7
8

在进行通信的地方使用 uni 提供的 API

handlePostMessage() {
  ...
  uni.postMessage({
    data: {
      action: "message"
    }
  })
  ...
}
1
2
3
4
5
6
7
8
9

也可以将 uni 实例挂载在main.js中的 Vue 实例上,全局进行调用

uni-app 端message事件回调方法的event.detail.data中接收传递过来的消息

<web-view :src="webUrl" @message="message"></web-view>
1
methods: {
  message(event){
    console.log(event.detail.data) //H5端通信的数据
  }
}
1
2
3
4
5

  • 只有在小程序后退/组件销毁/分享时,才会触发 message 的消息回调
  • event.detail.data 中的数据,以数组的形式接收每次 post 的消息
  • 传递的消息信息,必须写在 data 对象中

uni-app 主动通信

uni-appH5发起通信是通过web-view的 src 进行 url 参数的拼接,将需要传递的信息拼接在参数里面

uni-app 端

<web-view :src="webUrl"></web-view>
1
data() {
    return {
        webUrl: 'https://www.baidu.com?uerId=20210808&name=Jason&age=27',
    }
},
1
2
3
4
5

H5 端

export default {
  created() {
    const uerId = getParamsKey("uerId");
    const name = getParamsKey("name");
    const age = getParamsKey("age");
  },
  methods: {
    getParamsKey: function(name) {
      let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
      let key = window.location.search.substr(1).match(reg);
      if (key != null) {
        return decodeURIComponent(key[2]);
      }
      return null;
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 小程序登录实现

# 关于登录

微信小程序的登录【静默登录】,和我们传统 Web 应用的【单点登录】本质其实是一样的

# 什么是单点登录

单点登录:Single Sign On(SSO)

在很早之前,我们的系统一般都是单系统,特别庞大,所有的功能都在同一个系统上。后来,为了合理利用资源及降低系统的耦合性,于是把单个系统拆分为了多个子系统。所以,现在一般很多公司的大型应用在后端都会把用户授权的逻辑和获取用户信息的逻辑独立出来成为一个用户中心,用户中心不处理业务上的逻辑,只处理用户信息的管理和第三方应用的授权。第三方应用需要登录的时候,则把用户的登录请求转发给用户中心进行处理,用户处理完返回登录所需凭证,第三方应用验证凭证,通过后就登录用户。

简单点来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。也就是说,如果用户在 A 网站登录了,C 网站和 B 网站就能实现自动登录。

而在微信中,如果用户登录了微信账号,那么在整个小程序生态中,都可以实现【静默登录】

由于 HTTP 协议是无状态的,行业对于登录的处理一般是:

  1. cookie-session: 常用于浏览器应用
  2. access token:常用于移动端等非浏览器应用

而微信小程序并不是浏览器运行环境,没有Cookie,所以通常会使用 Access Token的方式登录

# 登录实现

从上面的流程图中我们总结微信的授权登录分为:

  • 前端调用wx.login()获取一次性加密凭证code并通过接口给到后端
  • 后端把code给到微信的接口服务,换取用户唯一标识的openIdsession_key授权凭证
  • 后端把从微信接口服务获取到的用户凭证与自行生成的登录状态凭证传给前端
  • 前端通过localStorage将从后端获取到的登录凭证保存起来
  • 下次请求的通过request请求将登录凭证带在 header 里面传给后端,就能识别是哪个用户以及登录状态是否过期

# 小程序视图层

# rpx( responsive pixel)响应单位

rpx 是微信小程序中独有的 css 尺寸单位,可以根据屏幕宽度进行自适应,官方规定屏幕宽为 750rpx。所以,我们只需要根据 rpx 设置元素和字体的大小,小程序在不同尺寸的屏幕上就可以实现自动适配

# rpx 和 px 之间的换算

以 iPhone6 为例,iPhone6 的屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素,所以得出公式

1 rpx = 0.5 px = 1 物理像素

如果不太明白可以看下移动 web 开发之像素和 DPR 详解 (opens new window)

比较方便的是 uni-app 的Hbuilder (opens new window)给我们提供了转换工具,可以根据像素单位自动转换为 rpx

Hbuilder IDE 配置

工具=》设置=》编辑器设置

根据设计稿的尺寸在上面的输入框中填入转换比例即可

# 网络请求封装

微信小程序中发起 request 请求的用是wx.request()uni-app中的uni.request()也是基于微信的 api 来做的。 在使用小程序 request api 时,有以下缺点:

  • 多个页面往往代表发送多个网络请求
  • 不能对后台接口返回的相同错误进行统一处理和拦截
  • 代码量大且臃肿

所以将其 Promisefy 是及其有必要的。 封装代码如下:

request.js

// 全局请求路径,也就是后端的请求基准路径
const BASE_URL = "https://www.baidu.com";
// 同时发送异步代码的次数,防止一次点击中有多次请求,用于处理
let ajaxTimes = 0;
// 封装请求方法,并向外暴露该方法
export const httpRequest = (options) => {
  // 解构请求头参数
  let header = {
    post: {
      "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
    },
    "X-Requested-With": "XMLHttpRequest",
    ...options.header,
  };
  ajaxTimes++;
  // 显示加载中 效果
  uni.showLoading({
    title: "处理中",
    mask: true,
  });
  return new Promise((resolve, reject) => {
    uni.request({
      url: BASE_URL + options.url,
      method: options.method || "POST",
      data: options.data || {},
      header,
      success: (res) => {
        if (res.data && res.data.code === "0") {
          resolve(res.data.data || {});
        } else {
          setTimeout(() => {
            uni.showToast({
              title: res.data.msg,
              icon: "none",
              duration: 3000,
            });
          }, 500);
          reject(res.data);
        }
      },
      fail: (err) => {
        reject(err);
      },
      // 完成之后关闭加载效果
      complete: () => {
        ajaxTimes--;
        if (ajaxTimes === 0) {
          //  关闭正在等待的图标
          uni.hideLoading();
        }
      },
    });
  });
};
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

main.js

import { httpRequest } from "common/request.js";
Vue.prototype.$httpRequest = httpRequest;
1
2

页面调用

// post请求默认不用写method
handleLogin() {
  this.$httpRequest({
    url: "xxx", //接口路径
    data: {
      code: 'abc' // 参数
    },
  });
}
1
2
3
4
5
6
7
8
9

# 小程序之间互相跳转如何实现

配置跳转目标小程序的 APPID

uni-app构建的根目录(如果是原生小程序的话,在app.json里面进行配置)

manifest.json =>源码视图 配置如下代码

两个小程序都要配置和自己有交互关系的小程序的 APPID(很重要)

 /* 小程序特有相关 */
"mp-weixin" : {
  "navigateToMiniProgramAppIdList": [
    "XXXXXXXXX" //跳转的目标小程序的APPID
  ]
},
1
2
3
4
5
6

页面中使用

...
  uni.navigateToMiniProgram({
    appId: "XXXX", //跳转的目标小程序的APPID
    path: urlPath, //跳转的目标小程序的路径
    envVersion: "trial", //指定跳转版本develop(开发版),trial(体验版),release(正式版)
    success(res) {
      // 打开成功处理逻辑
    },
    fail(res) {
      // 打开失败后处理逻辑
    },
  });
...
1
2
3
4
5
6
7
8
9
10
11
12
13

# 其他问题

# 微信小程序在部分安卓手机时间格式显示为英文

js 的toLocaleDateString()方法可根据本地时间把 Date 对象的日期部分转换为字符串,并返回结果,而这个方法在不同浏览器返回的格式有一定差异

比如将一个日期对象转化为 YYYY/MM/DD 这样的格式,在微信开发者工具里面是这样的

在部分安卓机型上测试时,会发现时间显示为了英文

解决方案:

  • 当月和日为一位时进行补零
  • 使用 moment 进行转换(推荐使用 moment,很方便)

# 微信小程序 showToast 真机下一闪而过

场景一:请求接口时需要调用uni.showLoading(),给用户显示一个处理中的 loading,请求结束后调用uni.hideLoading()隐藏该 loading。如果需要给用户提示错误信息,调用 uni.showToast(),在微信开发者工具上能正常显示,但是在真机上会出现提示信息闪烁一下就立马消失

场景二uni.showToast()显示后进行页面跳转,此时 toast 闪一下,直接跳转

出现原因

  1. uni.hideLoading()会关闭同级中的uni.showLoading()uni.showToast() 所以要在 showToast 之前调用uni.hideLoading()
  2. toast 不会出现在打开的新的页面中

解决方案:

场景一

setTimeout(() => {
  uni.showToast({
    title: msg,
    mask: true,
    icon: "none",
    duration: 2000,
  });
}, 500);
1
2
3
4
5
6
7
8

场景二

uni.showToast({
  title: '发送成功',
   icon: "none",
   duration: 2000,
   mask: true
 })
 setTimeout(()=> {
   uni.navigateTo({
     url: 'https://www.baidu.com',
   })
 }, 1000)
})
1
2
3
4
5
6
7
8
9
10
11
12

# 微信公众号相关问题

由于这个项目大多数页面都是挪用之前的微信公众号的页面,所以也记录下公众号相关的一些问题

# 微信 H5 页 IOS 下软键盘弹起后,页面下方留白

在微信公众号内嵌的 H5 页面以及 safiri 浏览器中,IOS 端在输入框获取焦点后,软键盘弹起,输入完成后,软键盘隐藏,下方会留有一大片空白。页面不能正常显示,整个页面处于上移。这个问题只在 IOS 端会出现,所以要针对 IOS 做特殊处理

初步解决方案

监听软键盘弹起时窗口变化,设置页面滚动

...
const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
if(isIOS) {
  window.scrollTo(0, 0);
}
...
1
2
3
4
5
6

进阶解决方案

上面的解决方案在页面中有上传组件时(例如上传照片),会导致系统弹出的上传组件(图中标红的区域)跟着滚动一起滚到页面顶部

所以在软键盘弹起时,我们需要记录下弹起时的滚动条位置,回滚时回滚到记录滚动的位置,这样系统组件就还是在当前位置,不会出现上面位置错位的现象

...
const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
if(isIOS) {
  const scrollTop = window.document.scrollingElement.scrollTop || 0
  window.scrollTo(0, scrollTop)
}
...
1
2
3
4
5
6
7

# 微信公众号内嵌网页出现底部导航栏

微信公众号在 IOS 端打开且页面有历史记录时,微信内嵌的浏览器会很贴心(鸡肋)的在浏览器底给用户添加一个导航条,这个导航条的出现会影响我们原有的页面布局,导致页面被压缩和遮挡。

如果是企业号可以通过微信官方提供的 API 关闭底部导航条隐藏微信中网页底部导航栏 (opens new window)

为什么会出现这个问题?

微信公众号中当页面跳转时就会产生历史记录,有页面历史记录就会出现导航条

初步解决方案

  • 在页面路由跳转时使用location.replace()代替location.href = ''

  • Vue中可以使用this.$router.replace()代替this.$router.push()

进阶解决方案

在页面 dom 加载完成后再获取高度

mounted(){
    //等微信多出来底部的返回条后,再获取高度,解决iOS新版微信底部返回横条问题
    this.$nextTick(() => {
        setTimeout(()=> {
            //计算滚动高度(滚动区域高度 = 小白条出来后窗口的高度 - 顶部不滚动区域的高度)
            this.$refs.wrapperBox.$el.style.height = (window.innerHeight - this.$refs.topHead.$el.style.height) + 'px';
        },2000)
    });
},
1
2
3
4
5
6
7
8
9

建议在编写布局的过程中,将页面底部的导航条考虑进去

# 一点建议

在开发的过程中遇到问题时多打断点看看是哪里出了问题,如果没有报错但是数据有没拿到,先仔细检查一下调用时有没有出现拼写错误,然后再找后端同学。开发过程中时间一般都比较紧张,尽可能的减少大家不必要的沟通成本和时间是很有必要的。处理复杂业务逻辑时不妨先想清楚,再动手开始写也不迟。设计页面的时候多看看微信小程序官方文档 (opens new window)

# 结语

写小程序的过程对我来说是一个挑战,项目中遇到的各种奇奇怪怪的 bug 会很伤脑筋,但是解决了这些问题还是非常有成就感,希望这篇文档能给有需要的小伙伴提供一些帮助。最后,也欢迎给我提供一些建议,大家一起交流学习小程序。

后续如果遇到了新的小程序相关的问题会再来更新这篇文档

未完,待续。。。

Last Updated: 11/21/2022, 6:50:20 AM