uniapp开发微信小程序,我踩了大家都会踩的坑
最近使用uniapp开发了一个微信小程序(本项目技术栈是uniapp + vue3 + ts,用了最近比较火的模板unibest。),踩了一些大家普遍都会踩的坑,下面做一些总结。文章多处引用到权威官方内容和一些比较可靠的文章。如有错误,欢迎指正。
1. 使用微信昵称填写能力遇到的问题
自 2022 年 10 月 25 日 24 时后,wx.getUserProfile
和 wx.getUserInfo
的接口被收回,要想获取微信的昵称头像需要使用微信的头像昵称填写能力。
我们的设计稿中没有编辑确认按钮,所以应该失焦后就调用后端的变更昵称接口:
但是失焦之后,微信会对昵称内容做合规性校验,导致失焦后不能立马获取到输入的内容:
<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
}
// ...
}
但如果真的输入了违规昵称,微信将自动清空输入框内容,而在此之前我的提交请求已经发送:
因此需要用到官方新加的一个回调事件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_modules
的uv-input
源码,并给uv-ui
提个pr
:
2. 自定义导航栏
原生导航栏配置方面有很多限制,比如不允许修改字体大小等。所以有的时候需要自定义导航栏。
首先注意,webview的页面无法自定义导航栏!
所以:
导航栏高度 = 状态栏到胶囊的间距(胶囊上坐标位置-状态栏高度) * 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实例:
因此需要每个tab页渲染时设置一下自定义tabbar组件的 activeIndex
(我这里变量名是selected
):
如果是原生小程序开发像官网那样写就好,如果是uniapp
开发,需要:
onShow(() => {
const currentPage = getCurrentPages()[0]; // 获取当前页面实例
const currentTabBar = currentPage?.getTabBar?.();
// 设置当前tab页的下标index
currentTabBar?.setData({ selected: 0 });
})
效果:
4. IOS适配安全距离
当用户使用圆形设备访问页面时,就存在“安全区域”和“安全距离”的概念。安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners
)、齐刘海(sensor housing
)、小黑条(Home Indicator
)的影响。
上图来自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*/
注意: constant
和env
不能调换位置
可以配合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'>
直观一点就是:
上图来自移动端安全区域适配方案
然后再使用env
和constant
padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
5. 列表滚动相关问题
列表滚动如果使用overflow: auto;
在首次下拉时(即使触控点在列表内)也会使整个页面下拉:
解决这个问题只需要将内容使用 scroll-view 包裹即可:
<scroll-view scroll-y class="max-h-[800rpx] overflow-auto"></scroll-view>
下拉刷新将列表滚动到顶部:
小程序默认使用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. 配置小程序用户隐私保护指引
文档:小程序隐私协议开发指南
什么时候要配置:
但凡你的小程序用到上图中任何一种用户信息就得配置,否则使用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 }
)。
代码逻辑:
配置并等待审核通过后,进行以下步骤:
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: 这部分逻辑相对于业务是几乎没有耦合的,甚至如果没有特殊需求agree
和disagree
事件都不用写。如果将来官方主动弹框没问题了,那这个逻辑可以直接删掉。
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()
}
})
}
}
})
}
这样,整个隐私协议指引流程就完整了。
来源:juejin.cn/post/7361688292351967259