uniapp开发微信小程序,我踩了大家都会踩的坑

7,220 阅读10分钟

最近使用uniapp开发了一个微信小程序(本项目技术栈是uniapp + vue3 + ts,用了最近比较火的模板unibest。),踩了一些大家普遍都会踩的坑,下面做一些总结。文章多处引用到权威官方内容和一些比较可靠的文章。如有错误,欢迎指正。

1. 使用微信昵称填写能力遇到的问题

自 2022 年 10 月 25 日 24 时后,wx.getUserProfilewx.getUserInfo的接口被收回,要想获取微信的昵称头像需要使用微信的头像昵称填写能力

我们的设计稿中没有编辑确认按钮,所以应该失焦后调用后端的变更昵称接口:

63ccfb0bc694c1e77c6cc90dbbf2de5.jpg 1d2f6b0b172af8d9c4c64aab3605349.jpg

但是失焦之后,微信会对昵称内容做合规性校验,导致失焦后不能立马获取到输入的内容

<uv-input v-model="form.name" type="nickname" placeholder="请输入内容" @blur="handleSubmit"></uv-input>
async function handleSubmit() {
    console.log('form.value.name', form.value.name) // 测试用户001
    console.log('rawName', rawName) // 测试用户001
    if (form.value.name === rawName)
        return
    // ...
}

因此最开始的想法是等待校验结束:

async function handleSubmit() {
    // 微信会对type="nickname"的输入框失焦时进行昵称违规校验,这个校验是异步的,所以需要等待一下
    await new Promise((resolve) => setTimeout(resolve, 0))
    console.log('form.value.name', form.value.name) // Jude
    console.log('rawName', rawName) // 测试用户001
    if (form.value.name === rawName) {
        return
    }
    // ...
}

但如果真的输入了违规昵称,微信将自动清空输入框内容,而在此之前我的提交请求已经发送

80ced13b-9e04-4478-b250-08e073dc10e0.gif

因此需要用到官方新加的一个回调事件bindnicknamereview文档):

<uv-input v-model="form.name" type="nickname" placeholder="请输入内容" @nicknamereview="handleSubmit"></uv-input>
function onNickNameReview(e) {
    console.log('onNickNameReview', e)
    if (e.detail.pass) {
        // 校验通过
        handleSubmit()
    } else {
        form.value.name = rawName
    }
}

但发现 uv-ui 并没有提供这个事件,还是没有生效,只能改node_modulesuv-input源码,并给uv-ui提个pr

image.png

2. 自定义导航栏

原生导航栏配置方面有很多限制,比如不允许修改字体大小等。所以有的时候需要自定义导航栏。

首先注意,webview会自动铺满整个小程序页面,所以带有webview的页面无法自定义导航栏!

image.png

所以: 导航栏高度 = 状态栏到胶囊的间距(胶囊上坐标位置-状态栏高度) * 2 + 胶囊高度 + 状态栏高度

第一步配置当前页面的json文件

// pages.json
{ navigationStyle: "custom" }

第二步:获取状态栏和导航栏高度,只需要获取一次即可,获取到可以放到pinia

// 自定义导航栏
const statusBarHeight = ref(0)
const navBarHeight = ref(0)
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuButtonInfo.height + (menuButtonInfo.top - statusBarHeight.value) * 2

第三步:自定义导航栏

<view class="nav-bar">
        <!-- 状态栏占位 -->
        <view :style="{ height: statusBarHeight + 'px' }"></view>
        <!-- 真正的导航栏内容 ,请按照自己的需求自行定义-->
        <view class="nav-bar-content" style="font-size: 34rpx;" :style="{ height: navBarHeight + 'px' }">导航栏标题</view>
</view>

问题:微信小程序原生导航栏会根据微信设置(字体大小,是否开启深色模式)等变化,深色模式是页面是可以获取到的,但字体大小等目前没有开放接口,所以无法根据微信设置动态变化。

3. 自定义tabbar

由于原生底部tabbar的局限性,未能满足产品需求,所以需要自定义tabbar。

首先,自定义tabbar的第一步配置pages.json

// pages.json
  tabBar: {
    custom: true,
    // ...
  },

然后,我们只需要在项目根目录(src)创建custom-tab-bar目录,uniapp编译器会直接它拷贝到小程序中:

<!-- src/custom-tab-bar/index.wxml -->
<view class="tab-bar">
  <view class="tab-bar-border"></view>
  <view wx:for="{{list}}" wx:key="index" class="tab-bar-item" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab">
    <image class="tab-bar-item-img" src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image>
    <view class="tab-bar-item-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
  </view>
</view>
// src/custom-tab-bar/index.js
Component({
  data: {
    selected: 0,
    color: "#8d939f",
    selectedColor: "#e3eaf9",
    list: [{
      pagePath: "/pages/index/index",
      iconPath: "../static/tabbar/home01.png",
      selectedIconPath: "../static/tabbar/home02.png",
      text: "首页"
    }, {
      pagePath: "/pages/my/my",
      iconPath: "../static/tabbar/user01.png",
      selectedIconPath: "../static/tabbar/user02.png",
      text: "我的"
    }]
  },
  attached() {
  },
  methods: {
    switchTab(e) {
      const data = e.currentTarget.dataset
      const url = data.path
      wx.switchTab({url})
      this.setData({
        selected: data.index
      })
    }
  }
})
// src/custom-tab-bar/index.json
{
  "component": true
}
// src/custom-tab-bar/index.wxss
.tab-bar {
  position: fixed;
  bottom: calc(16rpx + env(safe-area-inset-bottom));
  left: 0;
  right: 0;
  height: 100rpx;
  background: linear-gradient(180deg, rgba(13, 15, 26, 0.95) 0%, rgba(42, 50, 76, 0.95) 100%);
  box-shadow: 0rpx 4rpx 16rpx 0px rgba(0, 0, 0, 0.12);
  display: flex;
  width: calc(100% - 2 * 36rpx);
  border-radius: 36rpx;
  margin: 0 auto;
}

.tab-bar-item {
  flex: 1;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.tab-bar-item .tab-bar-item-img {
  width: 32rpx;
  height: 32rpx;
}

.tab-bar-item .tab-bar-item-text {
  margin-top: 10rpx;
  font-size: 20rpx;
}

最后,关键坑注意:每个tab页都有自己的tabbar实例:

image.png

因此需要每个tab页渲染时设置一下自定义tabbar组件的 activeIndex(我这里变量名是selected): 如果是原生小程序开发像官网那样写就好,如果是uniapp开发,需要:

onShow(() => {
    const currentPage = getCurrentPages()[0];  // 获取当前页面实例
    const currentTabBar = currentPage?.getTabBar?.();
    // 设置当前tab页的下标index
    currentTabBar?.setData({ selected: 0 });
})

效果:

image.png

4. IOS适配安全距离

当用户使用圆形设备访问页面时,就存在“安全区域”和“安全距离”的概念。安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)的影响

image.png

上图来自designing-websites-for-iphone-x

uniapp适配:

uniapp适配安全距离有三个方法:

a. manifest.json配置安全距离

// manifest.json
{
    "app-plus": {
        "safearea": {    //可选,JSON对象,安全区域配置
            "background": "#RRGGBB",    //可选,字符串类型,#RRGGBB格式,安全区域背景颜色
            "backgroundDark": "#RRGGBB",     //可选,字符串类型,#RRGGBB格式,暗黑模式安全区域背景颜色
            "bottom": {     //可选,JSON对象,底部安全区域配置
                "offset": "auto"     //可选,字符串类型,安全区域偏移值,可取值auto、none
            },
            "left": {      //可选,JSON对象,左侧安全区域配置
                "offset": "none"  //可选,字符串类型,安全区域偏移值,可取值auto、none
            },
            "right": {       //可选,JSON对象,左侧安全区域配置
                "offset": "none"    //可选,字符串类型,安全区域偏移值,可取值auto、none
            }
        },
    }
}

问题: 这种方式显然不够灵活,它设置的是单独的背景色,如果需要下方一个区域是背景图,延伸到底部安全区就满足不了了。

所以,我是将以上的配置设置成none,然后手动适配页面的安全距离:

b. js获取安全距离

let app = uni.getSystemInfoSync()
app.statusBarHeight // 手机状态栏的高度
app.bottom // 底部安全距离

c. 使用苹果官方推出的css函数env()、constant()适配

padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/ 
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/

注意: constantenv不能调换位置

可以配合calc使用:

padding-bottom: calc(constant(safe-area-inset-bottom) + 20rpx); /*兼容 IOS<11.2*/ 
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); /*兼容 IOS>11.2*/

h5适配

网页适配安全距离的前提是需要将<meta name="viewport">标签设置viewport-fit:cover;

<meta name='viewport' content='initial-scale=1, viewport-fit=cover'>

这是MDN上关于viewport-fit的解释

image.png

直观一点就是:

image.png image.png

上图来自移动端安全区域适配方案

然后再使用envconstant

padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/ 
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/

5. 列表滚动相关问题

列表滚动如果使用overflow: auto;首次下拉时(即使触控点在列表内)也会使整个页面下拉

20240424-113003_Edit.gif

解决这个问题只需要将内容使用 scroll-view 包裹即可:

<scroll-view scroll-y class="max-h-[800rpx] overflow-auto"></scroll-view>

c981feec5e35baa2baa300d4e21fc531.gif

下拉刷新将列表滚动到顶部:

小程序默认使用webview渲染,如果需要Skyline渲染引擎需要配置,而srcoll-view标签在webview中有个独有的属性enhanced,启用后可通过 ScrollViewContext 操作 scroll-view

<scroll-view id="scrollview" :enhanced="true" scroll-y class="max-h-[800rpx] overflow-auto"></scroll-view>
/** 将scrollview滚动到顶部 */
function scrollToTop(id: string) {
	wx.createSelectorQuery()
		.select(id)
		.node()
		.exec((res) => {
			const scrollView = res[0].node;
			scrollView.scrollTo({
				top: 0,
				animated: true
			});
		})
}

onPullDownRefresh(async () => {
	console.log('下拉刷新')
	try {
		await fetchList()
	} catch (error) {
		console.log(error)
	} finally {
		uni.stopPullDownRefresh()
		scrollToTop('#scrollview')
	}
})

6. 配置小程序用户隐私保护指引

文档:小程序隐私协议开发指南

1714296295268.png

什么时候要配置:

但凡你的小程序用到上图中任何一种用户信息就得配置,否则使用wx.authorize来获取相应授权时直接会走到fail回调,报 { "errMsg": "authorize:fail api scope is not declared in the privacy agreement", "errno": 112 }

配置的是什么:

配置的是将来你的程序打开让用户确认授权的隐私协议内容

如何配置:

登录微信公众平台 -> 设置 -> 服务内容声明 -> 用户隐私保护指引 -> 修改

隐私弹框触发的流程是什么:

程序调用隐私相关接口 ——> 微信判断该接口是否需要隐私授权 ——> 如果需要隐私授权开发者没有对其响应(注册onNeedPrivacyAuthorization的监听事件)主动弹出官方弹框(此时隐私相关接口调用处于pending状态,如果用户拒绝将会报{ "errMsg":" getLocation:fail privacy permission is not authorized", "errno":104 })。

image.png

代码逻辑:

配置并等待审核通过后,进行以下步骤:

1. 配置 __usePrivacyCheck__: true

尽管官方文档说明2023年10月17日之后无论是否配置改字段,隐私相关功能都会启用,但是实际尝试后发现还是得配置上才生效

// manifest.config.ts
'mp-weixin': {
    __usePrivacyCheck__: true
},

2. 自定义隐私弹框组件

尽管官方提供了官方隐私弹框组件,但是真机上没有生效,于是还是使用了自定义隐私弹框。

我是直接在插件市场找了一个下载量最多的插件,兼容vue2和vue3。

在小程序对应的页面:

<WsWxPrivacy id="privacy-popup" @agree="onAgree" @disagree="onDisAgree"></WsWxPrivacy>
function onAgree() {}

function onDisAgree() {}

tip: 这部分逻辑相对于业务是几乎没有耦合的,甚至如果没有特殊需求agreedisagree事件都不用写。如果将来官方主动弹框没问题了,那这个逻辑可以直接删掉。

3. 业务代码

举个例子,我这里隐私相关接口是uni.getLocation获取用户地理位置。

function handleCheckLocation() {
	return new Promise((resolve, reject) => {
		uni.getLocation({
			type: 'gcj02',
			success: async (res) => {
				console.log('当前位置:', res)
				try {
					let r = await checkLocation({
						lon: res.longitude.toString(),
						lat: res.latitude.toString(),
					})
                                        // ...
                                        resolve('success')
				} catch (error) {
					reject(error)
				}
			},
			fail: (error) => {
				console.log('获取位置失败:', error)
				reject(error)
			}
		})
	})
}

以上代码,在调用uni.getLocation时,微信自动发起位置授权,发起位置授权之前又会自动发起隐私授权。到此,这一流程是ok的。但是,如果用户拒绝了隐私授权,或者拒绝了位置授权,该怎么办?

如果拒绝了隐私授权,下次调用隐私相关接口时还会再次弹出隐私授权弹框。

如果拒绝了位置授权,下次调用就不会弹出位置授权弹框,但可以通过uni.getSetting来判断用户是否拒绝过,再通过wx.openSetting让用户打开设置界面手动开启授权。代码如下:

function getLocationSetting() {
    uni.getSetting({
        success: (res) => {
            console.log('获取设置:', res)
            if (res.authSetting['scope.userLocation']) {
                // 已经授权,可以直接调用 getLocation 获取位置
                handleCheckLocation()
            } else if (res.authSetting['scope.userLocation'] === false) {
                // 用户已拒绝授权,引导用户到设置页面开启
                wx.showModal({
                    title: '您未开启地理位置授权',
                    content: '请在设置中开启授权',
                    success: res => {
                        if (res.confirm) {
                            wx.openSetting({
                                success(settingRes) {
                                    if (settingRes.authSetting['scope.userLocation']) {
                                        // 用户打开了授权,再次获取地理位置
                                        handleCheckLocation()
                                    }
                                }
                            })
                        }
                    }
                })
            } else {
                // 首次使用功能,请求授权
                uni.authorize({
                    scope: 'scope.userLocation',
                    success() {
                        handleCheckLocation()
                    }
                })
            }
        }
    })
}

当然你也可以封装一下:

function getSetting(scopeName: string, cb: () => any) {
    uni.getSetting({
        success: (res) => {
            console.log('获取设置:', res)
            if (res.authSetting[scopeName]) {
                // 已经授权,可以直接调用
                cb()
            } else if (res.authSetting[scopeName] === false) {
                // 用户已拒绝授权,引导用户到设置页面开启
                wx.showModal({
                    title: '您未开启相关授权',
                    content: '请在设置中开启授权',
                    success: res => {
                        if (res.confirm) {
                            wx.openSetting({
                                success(settingRes) {
                                    if (settingRes.authSetting[scopeName]) {
                                        // 用户打开了授权,再次获取地理位置
                                        cb()
                                    }
                                }
                            })
                        }
                    }
                })
            } else {
                // 首次使用功能,请求授权
                uni.authorize({
                    scope: scopeName,
                    success() {
                        cb()
                    }
                })
            }
        }
    })
}

这样,整个隐私协议指引流程就完整了。