为了一套代码,适应多个平台,我司决定用uni-app来开发微信小程序。

一、从头开始项目搭建

1、HBuilder官网下载开发工具,我下载的是全功能的200多M的那一版
2、下载微信开发者工具后,找到设置-安全设置,打开“服务端口”,否则无法与HBuilder连接。
image.png

3、HBulder新建uni-app项目,运行的时候会选择微信开发者工具的路径。

一般会安装在C盘:C:\Program Files (x86)\Tencent\微信web开发者工具

新建项目:
image.png

4、运行后可以查看项目,在微信开发工具中看到下面的界面,算是运行成功了。
image.png

4、查看新建的项目模板后,发现和官方demo中很多的UI样式都没有,于是打算直接下载官方demo,然后去删删改改,直接套用了。
地址:https://github.com/dcloudio/hello-uniapp
下载在HBuilder中导入项目,然后运行就好了。

二、项目开发基本API记录

1、需要了解的技术栈

在开发uni-app前,必须掌握的技术栈,vue。当然,前端基础知识也必不可少!
如果要做app开发,对weex要有一定了解。

开发中会发现,很多和vue一样的方式,比如this.$store.

2、开发中使用的API

项目开发中使用的组件和API在官方文档都有,东西不少,从头看两遍也需要几天。

3、封装全局请求接口

因为使用的是官方demo,首先先要查看项目架构和基本配置。

①项目架构

下面是官方给的一张架构图:https://uniapp.dcloud.io/frame

开发小程序主要注意下面几个文件:

┌─cloudfunctions 云函数目录(阿里云为aliyun,腾讯云为tcb,详见uniCloud)
│─components 符合vue组件规范的uni-app组件目录
│ └─comp-a.vue 可复用的a组件
├─hybrid 存放本地网页的目录,详见
├─platforms 存放各平台专用页面的目录,详见

├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
│ └─list
│ └─list.vue list页面

├─common 一些公共资源的存放,如公共样式等
│ ├─uni-css 公共样式
│ ├─utils.js 一些公共函数处理方法

├─static 存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─wxcomponents 存放小程序组件的目录,编译后的文件
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json 配置应用名称、appid、logo、版本等打包信息,详见
└─pages.json 配置页面路由、导航条、选项卡等页面类信息,里面会配置首页

②、uni.request封装

之前做vue项目,axios都会做全局的请求配置和拦截。发现uni-app中并没有提供。而且好像也无法做全局的接口拦截。
requestAPI地址:https://uniapp.dcloud.io/api/request/request

这样就不得不做一个全局的request,大致思路如下:

在common文件夹下创建 config.js

  1. let url_config = ""
  2. if(process.env.NODE_ENV === 'development'){
  3. // 开发环境
  4. url_config = 'https://taobao.com/'
  5. }else{
  6. // 生产环境
  7. url_config = 'https://taobao.com/'
  8. }
  9. export default url_config

在common文件夹下创建request.js,用来封装uni.request

  1. import urlConfig from './config.js'
  2. import store from '../store'
  3. import { deepClone } from './util.js'
  4. const request = {}
  5. const headers = {
  6. 'content-type': 'application/json'
  7. }
  8. request.globalRequest = (url, method, data, power) => {
  9. switch (power){
  10. case 1:
  11. headers['Authorization'] = 'Basic a3N1ZGk6a3N1ZGk='
  12. break;
  13. case 2:
  14. responseType = 'blob'
  15. break;
  16. default:
  17. headers['Authorization'] = store.getters.accessToken
  18. break;
  19. }
  20. // 经销商请求的接口都要携带openid作为身份标记,客服和销售有token
  21. if (store.getters.identity && Number(store.getters.identity) === 0 && data && store.getters.openid) {
  22. data.openid = store.getters.openid
  23. }
  24. return uni.request({
  25. url: urlConfig + url,
  26. method,
  27. data: data,
  28. dataType: 'json',
  29. header: headers,
  30. }).then(res => {
  31. if (res && res[1] && res[1].statusCode && res[1].statusCode === 200) {
  32. // 带有返回全部参数标记的返回全部,默认只返回接口参数
  33. if(data && data.getAllRes) {
  34. return res[1]
  35. }
  36. return res[1].data
  37. } else {
  38. throw res
  39. }
  40. }).catch(parmas => {
  41. console.log('接口拦截错误:')
  42. console.log(parmas)
  43. if (parmas[0] === null && parmas[1] && parmas[1].data) {
  44. const { code, message } = parmas[1].data
  45. const codes = [10000, 10001, 10013, 10051, 10100, 401]
  46. if (code && codes.includes(code)) {
  47. uni.showToast({
  48.          title: '当前登录已过期',
  49.          icon: 'none'
  50.        })
  51. this.$store.commit('logout')
  52. } else {
  53. uni.showModal({
  54.          content: message,
  55. showCancel: false
  56.        })
  57. return Promise.reject()
  58. }
  59. } else if (parmas[0] && parmas[0].errMsg){
  60. const { errMsg } = parmas[0]
  61. uni.showModal({
  62.          content: errMsg,
  63. showCancel: false
  64.        })
  65. return Promise.reject()
  66. } else {
  67. uni.showModal({
  68.          content: JSON.stringift(parmas),
  69. showCancel: false
  70.        })
  71. return Promise.reject()
  72. }
  73.   })
  74. }
  75. export default request

附:res中共返回全部数据类似这样
image.png

里面有很多参数,具体用到的话,按照自己实际需求进行封装。
在上面的封装中,我们看到对于返回数据和错误返回,我们使用的是 .then.catch ;这样做的好处,是在组件中使用接口时候就和axios类似了,还是使用习惯的问题。当然,这种封装方式也存在一些问题,接口“红色”也会走.then方法,在该方法中抛出错误,然后才会抛出到catch
我们也可以通过这样的方式进行封装:

  1. uni.request({
  2. url: 'https://www.example.com/request',
  3. data: {
  4. text: 'uni.request'
  5. },
  6. header: {
  7. 'custom-header': 'hello'
  8. },
  9. success: (res) => {
  10. console.log(res.data)
  11. },
  12. fail: (err) => {
  13. console.log(err)
  14. }
  15. });

③根目录下建立api/index.js

  1. import request from '@/common/request.js'
  2. // import { formatGetUri } from '@/common/util.js'
  3. const api = {}
  4. const PORT1 = 'baseinfo'
  5. // POST请求方式 //必须大写,为了兼容其他应用
  6. api.submitForm = params => request.globalRequest(`${PORT1}/mobile/signUp`, 'POST', params, 1)
  7. // 请求列表接口
  8. api.queryList = params => request.globalRequest(`${PORT1}/service/list`, 'GET', params)
  9. export default api

④comon/util.js文件中添加函数formatGetUri

  1. // 这个函数是formdata序列化,因为我开发时候,后端需要的都是json格式,所以我实际上没有用到
  2. export function formatGetUri(obj) {
  3. const params = []
  4. Object.keys(obj).forEach((key) => {
  5. let value = obj[key]
  6. if (typeof value !== 'undefined' || value !== null) {
  7. params.push([key, encodeURIComponent(value)].join('='))
  8. }
  9. })
  10. return '?' + params.join('&')
  11. }
  12. module.exports = {
  13. formatGetUri: formatGetUri
  14. }

⑤main.js中引入

  1. import request from './common/request.js'
  2. import api from './api/index.js'
  3. import url from './common/config.js'
  4. Vue.prototype.$request = request
  5. Vue.prototype.$api = api
  6. Vue.prototype.$url = url

⑥组件中使用

  1. const params = {
  2. pageId: 1,
  3. pageCount: 10
  4. }
  5. this.$api.queryList(params).then(res => {
  6. // ...
  7. }).catch(res => {
  8.   // ...
  9. })

参考文档:https://www.yuque.com/docs/share/79ba2a9c-fb1f-41d5-a1dc-18a6e2d9eda4

4、esaycom模式引入uni-ui

uni-app框架自带了很多组件,但是开发中某些你想用的组件可能并没有,比如说折叠面板、卡片等。这个时候你需要引入扩展组件——uni-ui

官方推荐的引入方式是,esaycom方式引入。

三、项目开发问题记录

1、scss插件安装

因为项目开发使用的是包含uni-ui(style使用的scss)的项目(就是template中自带插件),但是插件引入后编译报错,查看错误,是没有scss编译的插件。需要手动安装一下。

新建项目时候选择了自带uni-ui的。
image.png
image.png
在HBuilder中选择工具-插件安装-前往插件市场安装,然后就打开了如下页面:
image.png
然后点击该插件,选择右侧的使用Hubilder导入插件。
image.png

之后插件就安装完成了,然后重新启动一下项目就运行OK了!

2、获取微信用户信息失败

好久没错小程序开发了,不知道微信小程序做了变化,用scope.userInfo获取用户信息不会弹窗询问了(目前正式版使用的不受影响),默认会直接失败,并且陆续不再支持该功能。
详见这里:https://developers.weixin.qq.com/community/develop/doc/0000a26e1aca6012e896a517556c01
image.png

看了官方的解释,才知道,从两年前就已经不支持了。

因此,uni-app官方的uni.getUserInfo方法获取userInfo并不好用了。会直接报错errMsg: “getUserInfo:fail scope unauthorized”
image.png
按照官方的说法,用button组件设置open-type=”getUserInfo”。但是问题又来了,这样的方法,导致我很难和uni-app一些方法统一起来。
看到这里,感觉微信之所以这么做是不是为了屏蔽第三方框架,诸如uni-app之类的。

废话少说,最后是通过这样一段代码来实现的获取用户信息。

  1. <button
  2. open-type="getUserInfo"
  3. @getuserinfo="wxGetUserInfo"
  4. type="primary">我是经销商</button>
  5. // 在methods中回调函数获取信息
  6. wxGetUserInfo() {
  7. uni.getUserInfo({
  8. provider: 'weixin',
  9. success: function (infoRes) {
  10. console.log(infoRes)
  11. },
  12. fail: function(error) {
  13. console.log(error)
  14. }
  15. });
  16. },

3、微信开发者工具有个大黑块

开发过程中发现微信开发者工具中有个大黑块。有时候刷下一些就没了,有时候刷新还在,十分难受!
image.png

去问了度娘,然后在微信开发社区看到官方回答:
https://developers.weixin.qq.com/community/develop/doc/000c8cfa49c14893763a9b4d856800
image.png

下载后找到文件(文件名:localstorage_b72da75d79277d2f5f9c30c9177be57e.json)里面就一行代码

  1. {
  2. "general": {
  3. "enableGPU": false
  4. }
  5. }

于是没有去替换,看这个语句应该是关闭CPU渲染的语句。
直接找到对应文件去修改:C:\Users\用户名\AppData\Local\微信开发者工具\User Data\1a695ca2de1a85735f93a43fb366c83f\WeappLocalData

在该目录下找到对应文件:
直接搜索enableGPU,
image.png
直接修改了这个为false,没有修改其他的。然后重启HBuilder的uni-app项目,问题解决了!

4、设置tabBar后不显示的问题

在pages.json中设置tabBar后不显示,查阅资料有人说pages中第一项必须和tabBar中第一项相同,否则无法显示,但是经过验证,这种说法并不正确。
只要切换到tabBar中包含的页面,tabBar就会显示出来。所以说这个逻辑是没有问题的。
我看到有人这样提问——如果想要所有页面都有tabBar该怎么设置呢?答案是自己写tabBar!
也就是说tabBar只会在对应包含的页面存在的时候才会显示!

5、uni-app封装省市区三级联动组件

之前在vue项目中自己做过封装,省市数据是从github上获取的,都带有code。现在要在nui-app上做封装,首先明确,需要使用picker而是不select,小程序中也没有select。
因为uni-app的语法和vue很相似,其实封装起来也很简单,直接说上代码吧。

  1. <template>
  2. <view>
  3. <view class="uni-form-item uni-column">
  4. <view class="title">省份:</view>
  5. <view class="uni-list-cell-db">
  6. <picker
  7. @change="bindPickerChangeProvince"
  8. :value="index"
  9. :range="provinces"
  10. range-key="name">
  11. <view class="uni-input">{{map.province}}</view>
  12. </picker>
  13. </view>
  14. </view>
  15. <view class="uni-form-item uni-column">
  16. <view class="title">市:</view>
  17. <view class="uni-list-cell-db">
  18. <picker
  19. @change="bindPickerChangeCity"
  20. :value="index"
  21. :range="cities"
  22. range-key="name">
  23. <view class="uni-input">{{map.city}}</view>
  24. </picker>
  25. </view>
  26. </view>
  27. <view class="uni-form-item uni-column" v-if="showCounty">
  28. <view class="title">县(区):</view>
  29. <view class="uni-list-cell-db">
  30. <picker
  31. @change="bindPickerChangeCounty"
  32. :value="index"
  33. :range="counties"
  34. range-key="name">
  35. <view class="uni-input">{{map.county}}</view>
  36. </picker>
  37. </view>
  38. </view>
  39. </view>
  40. </template>
  41. <script>
  42. // 省市县(区)三级联动地图
  43. import provinceMap from './province'
  44. export default {
  45. name: 'ProvinceSelect',
  46. props: {
  47. showCounty: {
  48. type: Boolean,
  49. default: true
  50. }
  51. },
  52. data() {
  53. return {
  54. map: {
  55. province: '',
  56. city: '',
  57. county: '',
  58. },
  59. index: 0,
  60. provinces: provinceMap,
  61. cities: [],
  62. counties: [],
  63. }
  64. },
  65. methods: {
  66. updateMap(val) {
  67. this.map = val
  68. },
  69. bindPickerChangeProvince: function(e) {
  70. this.index = e.detail.value
  71. this.map.province = this.provinces[this.index].name
  72. this.cities = this.provinces[this.index].cityList
  73. this.$emit('mapData', this.map)
  74. },
  75. bindPickerChangeCity: function(e) {
  76. this.index = e.detail.value
  77. this.map.city = this.cities[this.index].name
  78. this.counties = this.cities[this.index].areaList
  79. this.$emit('mapData', this.map)
  80. },
  81. bindPickerChangeCounty: function(e) {
  82. this.index = e.detail.value
  83. this.map.county = this.counties[this.index].name
  84. this.counties = this.counties[this.index].areaList
  85. this.$emit('mapData', this.map)
  86. }
  87. },
  88. }
  89. </script>

6、微信小程序的图片上传

在uni-app中上传图片api是uni.chooseImage(OBJECT),在回调函数success中会返回上传图片的临时文件地址。
image.png
res返回的一个本地资源的临时文件路径后是这样的: http://tmp/qs1x1G8K84nF61672963284aa67648d51bb3f16196c7.png

我们看到用一个tmp替代了域名。
得到这个路径后,我们需要调用uni.uploadFile(OBJECT)去上传图片。
image.png

7、uni-app小程序事件冒泡

因为uni-app支持vue语法,因此点击事件我都写的@click,然后遇到在父元素、子元素都有点击事件,而子元素的点击事件因为冒泡,触发了父元素的点击事件。
搜索“冒泡”,找到了官方给的解决方案。
image.png
因此,将子元素改为这种方式的点击触发就Ok了!
但是@tap这种写法是只支持小程序的,这明显不符合一套代码多处兼容的要求。再找一下:
搜索事件后,我们找到事件映射表:
发现stop是可以兼容多端的,因此,@click.stop是可以的哦。

8、小程序登录获取openid

以前开发过微信公众号,已经记不太清了,记得那时候openid是可以直接获取的。
现在开发微信小程序,调用uni.login获取code后,将code返给后端接口,后端再算出openid返给前端。盗用官方的一张流程图如下:
api-login.2fcc9f35.jpg

9、小程序获取手机号

现在腾讯对小程序的安全性管控越来越严格了,想要获取手机号无法用api触发,只能用户通过button手动触发。
官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
image.png
由于该接口针对已注册的小程序,所以需要先去注册认证,然后调用该接口才能使用。

11、微信开发者工具接口请求正常,微信扫码模拟时候不行

开发中按照上面的方法,封装了统一的接口请求组件,在模拟器上做联调,一切正常,然后想着用微信扫码模拟一下吧,结果接口没反应,打开调试工具,发现权限headers[‘Authorization’] = store.getters.accessToken 根本没有带上去。这是什么情况呢。

首先,做手机扫码测试时候,要去掉对合法域名校验。
image.png

之后点击“预览”,会生成二维码,扫码后进行测试。

之后,在小程序中打开调试面板。
微信图片_20210309140035.jpg微信图片_20210309140201.jpg微信图片_20210309140257.jpg

打开调试面板后,可以在面板中查看你console的数据。

经过查看发现,接口没有将权限token带上去,直接报401。查看了一下资料,发现,虽然接口请求默认为’content-type’: ‘application/json’。但是还需要配置一下。

  1. const headers = {
  2. 'content-type': 'application/json'
  3. }

加上这样一句代码后,重新预览,神奇的事情发生了,我的天啊,这么神奇吗?
数据就这样出来了。

12、uni-app的v-for循环中无法携带item参数

开发中发现的,打印item永远是undefined。查了一下资料,可以获取index,因此只能折中传递index了

13、获取用户头像(授权)

以前的微信获取头像有很多种方法:
1、手动授权通过button时候有open-type=”getUserInfo”
2、已授权的可以直接通过uni.getUserInfo
3、还有直接使用
4、uni.authorize唤起授权然后调用uni.getUserInfo等方法..
但是随着微信安全性的增加,以上方法逐渐废弃。
详见:
https://developers.weixin.qq.com/community/develop/doc/000e881c7046a8fa1f4d464105b001
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html#scope-%E5%88%97%E8%A1%A8

image.png
image.png

目前微信官方推荐的方法只有一种:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
通过button手动授权获取临时头像地址,可以在小程序中展示。

  1. <button type="primary"
  2. open-type="chooseAvatar"
  3. @chooseavatar="onChooseAvatar">一键登录</button>

四、uni-app小程序发布

官方教程:https://uniapp.dcloud.net.cn/quickstart?id=发布为小程序

image.png

总体来说很简单,发布的时候输入小程序appid和名称,在微信开发者工具上选择上传,然后去微信开发者平台去申请认证就OK了。

发布的时候需要输入版本号和备注,在备注里面一般需要填写好你的测试账号密码等,方便小程序的审核人员去审核你的小程序~~,当然具体需求你可以先发布,到时候有什么要求再去修改。

在微信开发者后台可以选择“提交审核”和“设置为体验版”。按照自己的具体需求来。

五、微信的消息订阅

微信有消息订阅功能,现在的消息订阅都需要手动触发,而且只能触发一次了,这个也是我采坑之后才知道的~

官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html

开发之初,有印象这个功能可以选择只发送一次还是一直授权,然后写了授权代码:

  1. // 最多可以放3个模板id
  2. uni.requestSubscribeMessage({
  3. tmplIds: ['abcdeg', '8xrhA-abcdeg'],
  4. success (res) {
  5. console.log('消息订阅授权成功')
  6. console.log(res)
  7. },
  8. fail(err) {
  9. console.log('消息订阅授权失败')
  10. console.log(err)
  11. }
  12. })

弹出页面如下:
微信图片_20210407133723.jpg

傻傻的以为选择了“总是保持以上选择,不再询问”就是可以一只发送消息通知了(印象中是这样,很久没做这个功能了)。
但是在后端同事调用接口后发现,调用一次后,再次调用就会报错“43101”。查询后得知是授权失效了。
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html

于是赶紧查询了微信开发社区的问题。看到了很多类似的问题。
https://developers.weixin.qq.com/community/develop/doc/000200a631c0c0a8680a0c24056800
https://developers.weixin.qq.com/community/develop/article/doc/0006ac060e4e80183bc9654b856013

查询资料才恍然大明白,原来现在的消息订阅早就不是以前了。申请永久模板是不太现实的(京东的发票通知是永久模板,人家是关系户,咱没办法)
只好绕道,改为用微信公众号内的通知:
image.png

这样一来工作就都交给后端了,公众号的通知是不需要授权的,只要你关注了就能发送。我这里提交表单,请求接口,后端自己去请求公众号发送消息的方法就好了~
另,附公众号的消息推送文档地址:
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
模板消息的发送方式和小程序很类似的。