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
好处
这样每次使用微信开发者工具打开项目都能识别你的小程序,方便后续的开发及版本上传
配置 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
优势:使用这个库会在移动端遇到下拉刷新上拉加载的业务场景,业务代码更简洁,而且针对原生环境做了一些优化
建议:
- 不要使用 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 图标
使用 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
}
}
}
获取地理位置及拉起地图导航
获取地理位置
- 前提是已经在小程序申请通过了
getLocationapi - 使用,需要用户授权,授权通过并打开了定位
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 地图组件
- 微信的文档真的一言难尽,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 中使用蓝牙通信
坑点
- 启动小程序的时候不要挂 VPN 接口会一直 502
- 使用 vue-cli 搭建项目框架,引入依赖的时候,不要使用 npm,使用 yarn
- 业务场景需要提前申请小程序的权限,否则会阻塞业务开发
- 微信支付申请企业商户 3-7 天
- 微信认证一般 1-2 天showLoading 和 showToast 不能同时存在
- 申请使用定位信息 api, 非必填资料一定要填,否则会被拒绝
- ... (尽量多看文档,很多 api 微信团队也一直在修改)
- 接口地址是内网时,发布体验版之后,无需配置域名,但要记得使用内网网络才能正常访问
- 接口地址未配置域名,在体验版中会调用不到接口,打开小程序调试之后才可以访问
- 获取时区的时候使用 Intl 这个类在本地开发不会有问题,发布之后,ios 正常,安卓会报错
- showLoading 和 showToast 不能同时存在
- 在模拟器上可以同时存在,但是在真机上不行,如果使用 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'
}
}
小程序隐私权限问题
近几个月,微信对于小程序得隐私权限管理越来越严格,说一下其中的坑点吧