uni-app+vant开发小程序踩坑总结,持续更新中...

4,782 阅读13分钟

uni-app + vant 开发小程序踩坑总结

开发微信小程序技术选型

原生开发

优势

  • 小程序原生语言适配微信,专注开发即可
  • 只用阅读微信开发文档即可
  • 性能会得到系统开发优化

劣势

  • 原生语言比较小众,离开了微信生态没有太多生存空间
  • 组件库及其他插件生态不太好
  • 无法支持一套代码多端运行

uni-app

优势

  • 语法基本与 vue 一致,前端人员上手成本较低
  • 一套代码多端运行
  • DCloud的生态插件相对完善
  • 可选择的组件库及插件较多
  • 支持 vscode 编辑器开发,npm 构建
  • 开发效率更高

劣势

  • 小程序文档及 uni-app 文档都需要查看,还有一些兼容性问题

综合考虑,推荐使用 uni-app 开发小程序

项目框架搭建

  • 官方推荐使用 HBuildX 去搭建项目 (因无法使用vscode,难以接收)
  • 个人推荐使用 VSCode 编辑器开发,通过 npm 命令引入脚手架

安装 vue-cli (已安装跳过)

npm install -g @vue/cli

创建 uni-app 项目

vue create -p dcloudio/uni-preset-vue my-project

  • 选择 hello uni-app 模板即可
  • 下载依赖包 (强烈建议使用 yarn,之前使用 npm 下载依赖一直有问题,排查很久也没找到问题,后面换了 yarn 就好了)
  • 跑一下项目, npm run dev:mp-weixin 会在当前目录下有一个 dist 文件,里面有一个 mp-weixin 的文件夹
  • 使用微信开发者工具将它导入就可以看到编译好的页面

微信小程序开发前置工作

  • 小程序账号申请 (分为个人,企业,政府等类型,不同的类型会有功能限制)
  • 小程序认证 (微信中一些 api 必须要认证后的小程序才能使用,比如获取用户手机号,业务场景涉及的话需提前申请认证 费用 300)
  • 接口设置 (小程序对于部分 api 需要审核后才能使用,比如获取当前地理位置,需要提供完整的业务场景,通过之后才能使用)
  • 微信支付 (需要开通商户服务才行,如果涉及到平台还会有二级商户,服务商等概念)
  • ......

项目开发系统配置

配置 appid

找到 manifest.json 文件,在 mp-weixin 属性下面配置你小程序的 appid

image.png

好处

这样每次使用微信开发者工具打开项目都能识别你的小程序,方便后续的开发及版本上传

配置 alias

根目录下新建 vue.config.js 文件

const defaultSettings = require('./src/config') // 公共配置文件
const path = require('path')
function resolve(dir) {
  return path.join(__dirname, dir)
}
const name = defaultSettings.name
module.exports = {
  lintOnSave: process.env.NODE_ENV === 'development',
  configureWebpack: {
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  }
}

好处

方便文件引入,提高文件查找效率

配置公共请求

  • 小程序里无法使用 axios 这个库,所以只能自己根据 uni.request 进行封装,或者在 dcloud 里面找支持的第三方库

  • 下面为个人在项目中的使用,仅供参考

import { loginToast } from '@/utils'

const baseUrl = process.env.VUE_APP_BASE_API

let ajaxTimes = 0
export function request(options = {}) {
  ajaxTimes++
  options.header = {
    'openid': uni.getStorageSync('openid') || '',
    'timezone': 'Asia/Shanghai' // 默认国内
  }
  uni.showLoading({
    title: '加载中',
    mask: true
  })
  return new Promise((resolve, reject) => {
    uni
      .request({
        url: baseUrl + options.url || '',
        method: options.method || 'GET',
        data: options.data || {},
        header: options.header || {},
        success: (res) => {
          if (res.statusCode === 200) {
            resolve(res.data)
          } else {
            reject(res.data)
            switch (res.statusCode) {
              case 401:
                loginToast()
                break
              case 403:
                loginToast()
                break
                // 404请求不存在
              case 404:
              // showLoading 和 showToast 不能同时存在
              setTimeout(() => {
                uni.showToast({
                  title: '网络请求不存在',
                  icon: 'error',
                  duration: 1500
                })
              }, 0)
                break
                // 其他错误,直接抛出错误提示
              case 500:
              setTimeout(() => {
                uni.showToast({
                  title: res.data.msg,
                  icon: 'none',
                  duration: 2000
                })
              }, 0)
                break
              default:
                return
            }
          }
        },
        fail: (e) => {
          // reject调用后,即可传递到调用方使用catch或者async+await同步方式进行处理逻辑
          reject(e)
        },
        complete: () => {
          ajaxTimes--
          if (ajaxTimes === 0) {
            //  关闭正在等待的图标
            uni.hideLoading()
          }
        }
      })
  })
}

引入第三方组件库

第三方组件库根据个人喜好选择

推荐使用 vant-weapp 组件库

安装方案:

  • 使用 npm 引入
  • 下载组件库直接在项目中使用 (推荐第二种,有一些特性化的场景,可能需要对组件库进行二次开发)

使用:

  • pages.json 文件下找到 globalStyle 对象
  • 新增属性 usingComponents,然后引入需要的组件
  • 下面是常用的组件,基本都会用到
		"usingComponents": {
			"van-button": "/wxcomponents/vant/button/index",
			"van-card": "/wxcomponents/vant/card/index",
			"van-cell": "/wxcomponents/vant/cell/index",
			"van-cell-group": "/wxcomponents/vant/cell-group/index",
			"van-checkbox": "/wxcomponents/vant/checkbox/index",
			"van-checkbox-group": "/wxcomponents/vant/checkbox-group/index",
			"van-col": "/wxcomponents/vant/col/index",
			"van-dialog": "/wxcomponents/vant/dialog/index",
			"van-field": "/wxcomponents/vant/field/index",
			"van-goods-action": "/wxcomponents/vant/goods-action/index",
			"van-goods-action-icon": "/wxcomponents/vant/goods-action-icon/index",
			"van-goods-action-button": "/wxcomponents/vant/goods-action-button/index",
			"van-icon": "/wxcomponents/vant/icon/index",
			"van-loading": "/wxcomponents/vant/loading/index",
			"van-nav-bar": "/wxcomponents/vant/nav-bar/index",
			"van-notice-bar": "/wxcomponents/vant/notice-bar/index",
			"van-notify": "/wxcomponents/vant/notify/index",
			"van-panel": "/wxcomponents/vant/panel/index",
			"van-popup": "/wxcomponents/vant/popup/index",
			"van-progress": "/wxcomponents/vant/progress/index",
			"van-radio": "/wxcomponents/vant/radio/index",
			"van-radio-group": "/wxcomponents/vant/radio-group/index",
			"van-row": "/wxcomponents/vant/row/index",
			"van-search": "/wxcomponents/vant/search/index",
			"van-slider": "/wxcomponents/vant/slider/index",
			"van-stepper": "/wxcomponents/vant/stepper/index",
			"van-steps": "/wxcomponents/vant/steps/index",
			"van-submit-bar": "/wxcomponents/vant/submit-bar/index",
			"van-swipe-cell": "/wxcomponents/vant/swipe-cell/index",
			"van-switch": "/wxcomponents/vant/switch/index",
			"van-tab": "/wxcomponents/vant/tab/index",
			"van-tabs": "/wxcomponents/vant/tabs/index",
			"van-tabbar": "/wxcomponents/vant/tabbar/index",
			"van-tabbar-item": "/wxcomponents/vant/tabbar-item/index",
			"van-tag": "/wxcomponents/vant/tag/index",
			"van-toast": "/wxcomponents/vant/toast/index",
			"van-transition": "/wxcomponents/vant/transition/index",
			"van-tree-select": "/wxcomponents/vant/tree-select/index",
			"van-datetime-picker": "/wxcomponents/vant/datetime-picker/index",
			"van-rate": "/wxcomponents/vant/rate/index",
			"van-collapse": "/wxcomponents/vant/collapse/index",
			"van-collapse-item": "/wxcomponents/vant/collapse-item/index",
			"van-picker": "/wxcomponents/vant/picker/index"
		}

引入第三方库 mescroll

一个精致的下拉加载上拉刷新的js框架

优势:使用这个库会在移动端遇到下拉刷新上拉加载的业务场景,业务代码更简洁,而且针对原生环境做了一些优化

建议:

  • 不要使用 npm 的方式引入,建议直接下载下来放入公共组件中即可,否则会有一些坑(uni-app 打包时候会做一些差异化编译)
  • 这个库的文档参数说明不是很全,可以看一下源码

示例:

<template>
  <mescroll-body
    :up="upOption"
    :down="downOption"
    class="container flex-center"
    :sticky="true"
    @init="mescrollInit"
    @up="upCallback"
  >
    <view class="station-list">
      <StationCard v-for="item in dataList" :key="item.bizId" :info="item" />
    </view>
  </mescroll-body>
</template>

<script>
import StationCard from '@/components/stationCard'
import { getStationList } from '@/api/station'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins.js'

export default {
  components: { StationCard },
  mixins: [MescrollMixin],
  data() {
    return {
      dataList: [],
      upOption: {
        page: {
          size: 10 // 每页数据的数量,默认10
        },
        noMoreSize: 1 // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
      },
      downOption: {
        native: true
      },
      listQuery: {
        pageSize: 10,
        currentPage: 1
      }
    }
  },
  methods: {
    upCallback(page) {
        this.listQuery.currentPage = page.num
        getStationList({ ...this.listQuery })
          .then((res) => {
            this.mescroll.endBySize(res.data.records.length, res.data.total)
            if (page.num === 1) {
              this.dataList = []
            }
            this.dataList = this.dataList.concat(res.data.records)
          })
          .catch(() => {
            this.mescroll.endErr()
          })
    }
  }
}
</script>

引入 iconfont 图标

uni-app中引入iconfont字体图标使用教程

使用 sass 样式预处理器

安装 sass 和 sass-loader 即可

开发生产环境变量配置

...

使用 vuex 状态管理

...

配置 eslint

...

业务场景

微信登录及获取手机号

小程序中使用微信获取用户手机号一键登录是最常见的场景 直接上代码

<template>
  <view class="container">
    <button
      type="primary"
      open-type="getPhoneNumber"
      @getphonenumber="onGetPhoneNumber"
    >
      微信用户一键登录
    </button>
    <van-toast id="van-toast" />
  </view>
</template>

<script>
import { login } from '@/api/user'
import Toast from '@/wxcomponents/vant/toast/toast'
export default {
  data() {
    return {}
  },
  methods: {
    // 获取用户手机号的前提是小程序已经认证通过了
    onGetPhoneNumber(e) {
      if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
        // 用户拒绝授权
        Toast.fail('请授权手机号!')
      } else {
        // 允许授权
        const phoneCode = e.detail.code
        uni.login({
          provider: 'weixin',
          success: (result) => {
            if (result && result.code) {
              login({ code: result.code, phoneCode: phoneCode }).then((res) => {
                uni.setStorageSync('openid', res.data.openid)
                uni.setStorageSync('userInfo', {
                  nickname: res.data.nickname,
                  avatar: res.data.avatar
                })
                Toast({
                  type: 'success',
                  message: '登录成功',
                  duration: 800,
                  onClose: () => {
                    uni.navigateBack()
                  }
                })
              })
            }
          },
          fail: (result) => {
            Toast.fail('出错了,请重试!')
          }
        })
      }
    }
  }
}
</script>

在小程序中使用 websocket

小程序中无法使用 new webSocket() 的方式去创建

export default {
  data() {
    return {
      timer1: null,
      timer2: null, // 心跳
      flag: false,
      wsurl: '',
    }
  },
  onLoad() {
    this.initWebSocket()
  },
  destroyed() {
    this.clearTimer()
    this.flag = false
    uni.closeSocket() // 离开路由之后断开websocket连接
  },
  methods: {
    // 初始化weosocket
    initWebSocket() {
      this.wsurl = 'xxxx'
      uni.connectSocket({
        url: this.wsurl,
        success: () => {
          this.clearTimer()
          uni.onSocketOpen((res) => {
            console.log('打开连接')
          })
          // 方式断开,持续给服务端发送信息
          this.timer2 = setInterval(() => {
            this.websocketsend('')
          }, 60 * 1000)
        },
        fail: () => {
          // 失败重连机制
          this.clearTimer()
          this.timer1 = setTimeout(() => {
            this.initWebSocket()
          }, 5000)
        }
      })
      uni.onSocketMessage((e) => {
        // 监听服务端推送过来的数据
        const resdata = JSON.parse(e.data)
      })
      // 断开重连机制
      uni.onSocketClose(() => {
        if (this.flag) {
          this.clearTimer()
          this.timer1 = setTimeout(() => {
            this.initWebSocket()
          }, 5000)
        }
      })
    },
    // 数据发送
    websocketsend(Data) {
      uni.sendSocketMessage({
        data: Data
      })
    },
    // 清空定时器
    clearTimer() {
      clearInterval(this.timer1)
      clearInterval(this.timer2)
      this.timer1 = null
      this.timer2 = null
    }
  }
}

获取地理位置及拉起地图导航

获取地理位置

  • 前提是已经在小程序申请通过了 getLocation api
  • 使用,需要用户授权,授权通过并打开了定位
      uni.getLocation({
        type: 'gcj02',
        success: (res) => {
          console.log('当前位置的经度:' + res.longitude)
          console.log('当前位置的纬度:' + res.latitude)
        },
        fail: (err) => {
          console.log('err =', err)
        },
        complete: () => {
          this.getList()
        }
      })

拉起地图导航

  • 经纬度一定需要是 number 类型,否则会报错
      uni.openLocation({
        latitude: parseFloat(info.lat), // 目的地的纬度
        longitude: parseFloat(info.lng), // 目的地的经度
        name: info.stationName // 打开后显示的地址名称
      })

普通二维码跳转页面

使用 map 地图组件

image.png

  • 微信的文档真的一言难尽,map地图的组件和操作 api 不放在一起...
  • 引入地图,一定必要参数是经纬度,否则会有问题,如果用户没开启定位,可以设置一个默认的位置
  • 添加 markers,有两种方法,一种是直接双向绑定,还有一种是拿到map实例然后添加进去
  • uni-app 拿到实例有一个坑点,需要传入第二个参数,绑定 this,否则后面的拿到实例之后也无法调用方法
  • this.mapCtx = uni.createMapContext('map', this)
  • 气泡的用法,这个是最坑的,cover-view已经被微信废弃了,但是这个你还是得使用cover-view,否则显示不出来
  • 模拟器也会有问题,有时候模拟器可以显示,真机不行,有时候真机可以模拟器不行,还有机型兼容问题,这个要慢慢去调
  • 然后非地图组件必须使用view组件,否则模拟器可以显示,真机不行
  • 贴一段代码吧,可以参考一下
<template>
  <view class="map">
    <map
      id="map"
      :longitude="center.lng"
      :latitude="center.lat"
      :scale="scale"
      show-location
      :markers="markers"
      style="width: 100vw; height: 100vh"
      @callouttap="onMarkerTap"
      @tap="currentStation = {}"
    >
    //气泡
      <cover-view slot="callout">
        <block v-for="(item,index) in stationList" :key="index">
          <cover-view class="customCallout" :marker-id="index">
            <cover-view class="content">
              <cover-view class="left cm">闲 {{ item.freeGunNum }}</cover-view>
              <cover-view class="right fsm">¥{{ fixed(item.servicePrice+ item.chargePrice,2) }}</cover-view>
            </cover-view>
          </cover-view>
        </block>
      </cover-view>
    </map>
  </view>
</template>
<script>

import { fixed } from '@/utils'

export default {
  name: 'MapBox',
  props: {
    positon: {
      type: Object
    },
    stationList: {
      type: Array
    },
    currentMode: {
      type: String
    }
  },
  data() {
    return {
      markers: [],
      customCalloutMarkerIds: [],
      center: {
        lng: '',
        lat: ''
      },
      mpCtx: null,
      scale: 12,
      currentStation: {}
    }
  },
  mounted() {
    // 获取附近点位
    this.$nextTick(() => {
      // 获取地图实例
      this.mpCtx = uni.createMapContext('map', this)
      if (this.positon.lng) {
        this.center = this.positon
      } else {
        this.center.lng = 114.43
        this.center.lat = 30.50
      }
      this.addMarkers()
    })
  },
  methods: {
    fixed,
    changeMode() {
      this.$emit('changeMode')
    },
    // 气泡点击事件
    onMarkerTap(e) {
      this.currentStation = this.stationList[e.markerId]
      //移动中心点位
      this.mpCtx.moveToLocation({
        longitude: this.stationList[e.markerId].lng,
        latitude: this.stationList[e.markerId].lat,
        success: (s) => {
          console.log(s)
        }
      })
    },
    // 添加点位
    addMarkers() {
      let obj = {}
      this.stationList.map((item, index) => {
        obj = {
          id: index, // 标记点 id
          longitude: item.lng,
          latitude: item.lat,
          width: 20,
          height: 20,
          iconPath: '../../../static/image/triangle.png',
          customCallout: {
            anchorY: 4,
            anchorX: 0,
            display: 'ALWAYS'
          }
        }
        this.markers.push(obj)
      })
    }
  }
}
</script>
<style lang="scss">
.customCallout {
  .content {
    background-color: #fff;
    width: 80px;
    height: 36px;
    padding: 5px;
    display: flex;
    align-items: center;
    border-radius: 5px;
    .left {
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background-color: rgba(#ed773a, 0.1);
      font-size: 10px;
      text-align: center;
      line-height: 36px;
      flex-shrink: 1;
    }
    .right{
      flex-grow: 1; // 必须要,否则模拟器显示正常,真机会有部分缺失
    }
  }
}
</style>

微信通知(消息订阅)

很多业务场景需要微信通知,比如用户点餐排号,到他了需要在微信通知他

这个功能需要前后端配合,一定要分清前后端的职责

  • 需要在小程序管理后台开通消息订阅功能
  • 添加你需要的模板,推送给用户的消息都是固定模板
  • 目前小程序要给用户推送消息,都需要用户授权才行
  • 用户授权的前提是用户在小程序进行了交互
  • 而且还需要 formId
  • 必须使用 form 组件才能拿到 formId
  • 后端需要这个 formId 才能给用户推送模板消息

消息订阅官方文档

使用 vant 的 button 组件,无法触发 form submit

解决方案---小程序 vant button 组件不触发 form submit 问题及解决

自定义 tabbar

uni-app 之 自定义 tabbar

uni-app 中使用蓝牙通信

蓝牙连接、读写数据

坑点

  • 启动小程序的时候不要挂 VPN 接口会一直 502
  • 使用 vue-cli 搭建项目框架,引入依赖的时候,不要使用 npm,使用 yarn
  • 业务场景需要提前申请小程序的权限,否则会阻塞业务开发
    • 微信支付申请企业商户 3-7 天
    • 微信认证一般 1-2 天showLoading 和 showToast 不能同时存在
    • 申请使用定位信息 api, 非必填资料一定要填,否则会被拒绝
    • ... (尽量多看文档,很多 api 微信团队也一直在修改)
  • 接口地址是内网时,发布体验版之后,无需配置域名,但要记得使用内网网络才能正常访问
  • 接口地址未配置域名,在体验版中会调用不到接口,打开小程序调试之后才可以访问
  • 获取时区的时候使用 Intl 这个类在本地开发不会有问题,发布之后,ios 正常,安卓会报错
  • showLoadingshowToast 不能同时存在
    • 在模拟器上可以同时存在,但是在真机上不行,如果使用 hideLoading 会立马把 toast 取消掉
    • 解决方案: 采用事件循环, setTimeout ,在 hideLoading 后再展示 toast

切换input时,软键盘需要二次点击才弹出

  • 解决:最佳解决方案是键盘保持,但是看decloud社区一直没有解决这个问题,只好退而求其次,就是让键盘消失之后再拉起
        <van-field
          id="companyAddr"
          :focus="focusId === 'companyAddr'"
          :value="form.companyAddr"
          label="公司地址"
          placeholder="填写公司地址"
          @tap="inputTap"
          @change="(e) => onChange(e, 'companyAddr')"
        />
        <van-field
          id="companyTelephone"
          :focus="focusId === 'companyTelephone'"
          :value="form.companyTelephone"
          label="公司电话"
          placeholder="填写公司电话"
          @tap="inputTap"
          @change="(e) => onChange(e, 'companyTelephone')"
        />
        
      methods:{
        inputTap(e) {
          uni.hideKeyboard() // 切换input之后隐藏
          setTimeout(() => {
            this.focusId = e.currentTarget.id 然后给下一个input焦点,自动拉起键盘
          }, 200)
        },
    }

小程序环境切换问题

  • 小程序上线的流程是在本地开发环境开发,然后切到测试环境提交到体验版,然后体验版提交审核上线
  • 这样就导致,打包的时候需要频繁的去改环境配置,在开发环境还好,上到体验环境提交后审核前再打一个生产环境就会影响到体验版的使用,而且有时候会忘记切换环境,导致二次审核

解决: 代码逻辑判断自动切换环境配置

微信没有提供获取当前环境的api,但是有一个全局的参数可以获取当前在哪个环境

let baseUrl = ''

if (typeof __wxConfig === 'object') {
  const version = __wxConfig.envVersion // 根据这个参数来判断
  console.log('当前环境:' + version)
  if (version === 'develop') {
    // 工具或者真机 开发环境
    baseUrl = 'http://192.168.150.92:30344'
  } else if (version === 'trial') {
    // 测试环境(体验版)
    baseUrl = 'http://192.168.150.92:31623'
  } else if (version === 'release') {
    // 正式环境
    baseUrl = 'https://xxxx.com'
  }
}

小程序隐私权限问题

近几个月,微信对于小程序得隐私权限管理越来越严格,说一下其中的坑点吧

如调用相关权限,需在后台配置,否则直接禁用小程序相关权限

关于小程序隐私保护指引设置的公告

image.png

  • 初次获取用户隐私权限的时候需主动明示用户,用户同意后才可继续获取相应权限,如手机号,地理位置等
  • 本来微信是要求用户自己去开发,后又发通告不需要开发者开发了(等等党的胜利),后续微信官方统一出台解决方案
  • 官方最新相关通告
  • 小程序不能在用户未实际体验对应权限的功能或服务时,要求授权相关权限
  • 不可强制要求用户授权相应权限,用户拒绝依旧可以使用小程序
  • 新小程序必须备案后才能上架,所以需要预留时间搞备案这个事情
  • 小程序备案相关指引