注册
web

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


最近使用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的页面无法自定义导航栏!


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()
}
})
}
}
})
}

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


作者:Jiude
来源:juejin.cn/post/7361688292351967259

0 个评论

要回复文章请先登录注册