1.uni-app的组件库解决方案
使用uni-ui组件库,这是官方出品的组件库,有官方的技术支持和持续维护且uni-ui组件库比较精简,组件自动按需导入,利于减少项目体积。
两个核心步骤: 1.安装dcloudio/uni-ui(组件库)和scss 2.在page.json文件中配置easycom规则,实现对uni-ui组件的自动导入和注册。
//组件自动引入规则
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
//以Xtx开头,easycom服务修改需要重启
"^Xtx(.*)": "@/components/XTX$1.vue"
}
},
为什么使用uni-ui而不选择uview-ui:
1.uni-ui是官方出品的组件库,有官方的技术支持和持续维护(最重要)
2.uni-ui相对精简,有利于减少项目体积
3.uview暂不支持vue3的开发,稍微落后
uview-ui组件库是uni-app插件市场下载量最高的第三方vue2组件库,也被做过vue3版。但作者太忙,处于没更新状态,不稳定,所以综合考虑选择官方维护的uni-ui
2.uni-app的全局状态管理以及vuex Pinia的了解
Vuex采用单一状态树的概念,将全局状态集中管理,方便追踪状态变化:
1.State用于存储全局状态
2.Getter:用于从State中派生出一些状态,例如计算属性
3.Mutation:用于同步修改State,严格遵循单向数据流
4.Action:用于异步操作,可以包含异步API请求,异步提交Mutation等等
Pina是轻量级且兼容Vue3和Vue2的状态管理库。Pinia和VueX的主要区别就是废弃了极其冗余的mutation。Pina主要包括:
1.Store,用于存储全局状态和处理状态变化的方法,类似于Vuex的State,Getter和Action的集合
2.可以创建多个Store实例,每个Store又都有独立的状态和方法
pina-plugin-persistedstate插件实现了持久化,但是这个插件默认使用localStorage实现持久化,小程序不兼容,所以要替换为uni.setStorageSync()和uni.getStorageSync()(总)持久化存储配置完成后,就会自动将用户数据保存在客户端,即使用户关闭了小程序,数据依然可以保留。
// 定义 Store
export const useMemberStore = defineStore(
'member',
() => {
...
},
// TODO: 持久化
{
//网页端配置
// persist: true,
persist: {
storage: {
getItem(key) {
return uni.getStorageSync(key)
},
setItem(key, value) {
uni.setStorageSync(key, value)
},
},
},
},
)
3.uni-app组件与vue.js组件的区别
uni-app是基于vue.js开发,所以很多方面是相似的,但uni-app支持多平台,所以会有差异:
1.基础组件uni-app提供了一套与vue.js不同的基础组件,是为了适应不同平台的ui要求设计的。在微信小程序,app,h5有统一的表现,在使用这些组件时,需要注意它们在不同平台之间的差异,封装这些组件时更推荐 等基础组件,而非div,span。
2.小程序和vue.js组件都有组件生命周期钩子,但是小程序有真机的生命周期钩子:onLaunch、onLoad,onShow 和 onHide
3.样式差异:某些css选择器不受支持比如*,uni-app支持rpx,可以自动适应不同尺寸屏幕(屏幕宽度为750rpx)
4.条件编译:开发者通过条件编译使用平台特有api和组件,从而实现平台相关功能、
4.uni.request与axios
优点:
1.uni.reuest内置于uni-app框架,不需要格外安装
2.兼容多端,方便在不同平台使用
缺点:
1.功能较为简单,缺少高级功能如请求拦截器
2.错误处理不如 axios 完善。axios 可以轻松区分网络错误和业务错误,而在 uni.request 中需要手动判断状态码。
import { useMemberStore } from '@/stores'
const baseURL = 'https://pcapi-xiaotuxian-front-devtest.itheima.net'
const httpInterceptor = {
invoke(options: UniApp.RequestOptions) {
// 1.非http开头需要拼接地址
if (!options.url.startsWith('http')) {
options.url = baseURL + options.url
}
//2.请求超时
options.timeout = 10000
options.header = {
...options.header,
'source-client': 'miniapp',
}
const memberStore = useMemberStore()
const token = memberStore.profile?.token
if (token) {
options.header.Authorization = token
}
console.log(options)
},
}
//添加拦截器`
uni.addInterceptor('request', httpInterceptor)
uni.addInterceptor('uploadFile', httpInterceptor)
interface Data<T> {
code: string
msg: string
result: T
}
export const http = <T>(options: UniApp.RequestOptions) => {
return new Promise<Data<T>>((reslove, reject) => {
uni.request({
...options,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
reslove(res.data as Data<T>)
} else if (res.statusCode == 401) {
//群里用户信息,跳转登录页
const memberStore = useMemberStore()
memberStore.clearProfile()
uni.navigateTo({
url: '/pages/login/login',
})
reject(res)
} else {
uni.showToast({
icon: 'none',
title: (res.data as Data<T>).msg || '请求错误',
})
reject()
}
},
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络',
})
reject(err)
},
})
})
}
5.uni-app实现自定义导航栏
1.隐藏默认导航栏:在page.json中设置navigationStyle:"custom"
2.按设计稿要求编写自定义导航栏结构,样式
3.封装左上角返回按钮,通过getCurrentPages获取路由站,如果路由栈数组长度是1,通过switchTab返回首页否则使用navigateBack返回上一页
4.根据需要抽离成通用的组件,预留标题插槽,方便复用
5.通过uni.getSystemInfoSync获取顶部到安全区域的距离,在模板中绑定行内样式,避免刘海屏或前置摄像头遮挡导航栏标题或 logo 等重要内容。
// src/pages.json
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom" // 隐藏默认导航
}
}
<!-- CustomNavbar.vue -->
<script setup lang="ts">
// 获取页面栈
const pages = getCurrentPages()
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>
<template>
<!-- 顶部安全区占位 -->
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<view class="wrap">
<navigator
v-if="pages.length > 1"
open-type="navigateBack"
class="back icon-left"
></navigator>
<navigator v-else url="/pages/index/index" open-type="switchTab" class="back icon-home">
</navigator>
<view class="title">
<!-- 插槽 -->
<slot>导航栏标题</slot>
</view>
</view>
</view>
</template>
其实我们项目的订单详情页(或者某个页),给自定义导航栏加了滚动驱动动画,增强用户视觉效果:
-
scroll-view滚动容器设置一个id,用于绑定动画效果和滚动容器偏移量。 -
获取当前页面实例,因为这个功能目前只有微信小程序端支持,H5 端不支持,还需要写条件编译。
-
onReady 绑定生命周期钩子中,通过 animate 设置动画配置,并通过
id绑定滚动容器,设置触发偏移量等信息。
<script setup lang="ts">
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
// 获取页面栈
const pages = getCurrentPages()
// #ifdef MP-WEIXIN
// 获取当前页面实例,数组最后一项
const pageInstance = pages.at(-1) as any
// 页面渲染完毕,绑定动画效果
onReady(() => {
// 动画效果,导航栏背景色
pageInstance.animate(
'.navbar',
[{ backgroundColor: 'transparent' }, { backgroundColor: '#f8f8f8' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
})
// #endif
</script>
<template>
<!-- 自定义导航栏: 默认透明不可见, scroll-view 滚动到 50 时展示 -->
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<view class="wrap">
<navigator
v-if="pages.length > 1"
open-type="navigateBack"
class="back icon-left"
></navigator>
<navigator v-else url="/pages/index/index" open-type="switchTab" class="back icon-home">
</navigator>
<view class="title">订单详情</view>
</view>
</view>
<scroll-view class="viewport" scroll-y enable-back-to-top id="scroller">
...滚动容器
</scroll-view>
</template>
6.uni-app性能优化
1.按需加载:使用图片懒加载,商品图片上添加(lazy-load属性即可)
2.分包加载。项目分为主包和分包,进入主包里某个页面在加载分包(分包预加载)
{
//分包加载配置
"subPackages": [
{
"root": "pagesMember",
"pages": [
{
"path": "settings/settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "profile/profile",
"style": {
"navigationBarTitleText": "个人信息页",
"navigationStyle": "custom"
}
},
{
"path": "address/address",
"style": {
"navigationBarTitleText": "地址管理"
}
},
{
"path": "address-form/address-form",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pagesOrder",
"pages": [
{
"path": "create/create",
"style": {
"navigationBarTitleText": "填写订单"
}
},
{
"path": "detail/detail",
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom"
}
},
{
"path": "payment/payment",
"style": {
"navigationBarTitleText": "支付结果页"
}
},
{
"path": "list/list",
"style": {
"navigationBarTitleText": "订单列表页"
}
}
]
}
],
"preloadRule": {
"pages/my/my": {
"packages": [
"pagesMember"
],
"network": "all"
}
}
}
3.条件编译:在处理多端的时候,按条件编译平台所需代码,减少冗余。
4.数据缓存,配置持久化数据缓存
5.组件化开发与复用
6.减少本地图片:除了 tabBar 图片,logo 等重要的图片,其他图片进可能使用 CND 图片。将静态资源文件放在 CDN 上,加速资源文件的加载速度。(公司已购买,由运维管理,项目商品图都已开启 CDN 加速)
7.骨架屏提升用户体验:使用户感知到数据正在加载,降低用户等待时的焦虑感,避免页面白屏闪烁(注意uniapp打包成h5端和app端会有样式隔离,骨架屏样式可能会出问题)
8.防抖节流,以及多使用组件自带动画,css动画而不是通过js定时器操作界面的动画swiper 滑动动画是自带的,订单列表页 Tabs 的滑块使用了 css 动画,滚动驱动的动画是小程序自带的并非手动监听 scroll 事件)
细讲防抖和节流
防抖(debounce)和节流(throttle)函数在处理高频触发事件时都非常实用。
debounce(防抖)函数:该函数会从上一次被调用后,延迟 wait 毫秒后调用func 函数。
throttle (节流)函数:在 wait 秒内最多执行 func 一次的函数。
(分 1)防抖(debounce)函数在以下应用场景中非常实用:
- 输入框实时搜索:当用户在输入框中输入时,可以使用防抖函数来延迟触发搜索请求。这样可以减少服务器请求次数,提高性能。
- 按钮点击:在一些场景下,如表单提交、购物车结算等,为避免用户频繁点击按钮导致重复提交数据,可以使用防抖函数控制按钮点击事件的触发。
- 滚动事件:在处理滚动事件时,可以使用防抖函数来限制事件处理函数的执行频率。例如,当用户滚动页面时,可以使用防抖函数来延迟加载图片或触发其他与滚动相关的操作。
- 用户操作监听:在实时监控用户操作(如鼠标移动、点击等)的场景中,可以使用防抖函数来减少事件处理函数的执行次数,降低系统资源消耗。
(分 2)节流(throttle)函数在以下应用场景中非常实用:
- 滚动加载:在无限滚动列表或页面滚动加载数据的场景中,可以使用节流函数控制滚动事件处理函数的执行频率,以减轻服务器压力和提高性能。(组件库一般有实现)
- 页面滚动时的动画效果:当用户滚动页面时,可以使用节流函数来控制动画效果的触发频率,以保持流畅的动画表现,避免性能抖动。(微信小程序的滚动驱动动画内部实现)
- 实时监控鼠标移动:在需要实时监控鼠标移动的场景中,可以使用节流函数限制事件处理函数的执行频率,降低系统资源消耗。
- 浏览器窗口大小调整:当用户调整浏览器窗口大小时,可以使用节流函数来限制与窗口大小相关的布局调整或重绘的执行频率,提高页面性能。
- 实时数据采集:在需要实时采集用户行为数据的场景中,可以使用节流函数来控制数据发送频率,减轻服务器压力。
7.下拉刷新和上拉加载
1.下拉刷新: refresher-enabled属性启动下拉刷新,@refresherrefresh绑定事件,refresher-triggered属性确定刷新状态。在刷新事件内部开启关闭下拉刷新动画以及发请求,注意同时发请求时可以使用Promise.all()或者promise.allsettled()
<script setup lang="ts">
// 当前下拉刷新状态
const isTriggered = ref(false)
// 自定义下拉刷新被触发
const onRefresherrefresh = async () => {
// 开始动画
isTriggered.value = true
// 加载数据
await Promise.all([getHomeBannerData(), getHomeCategoryData(), getHomeHotData()])
// 关闭动画
isTriggered.value = false
}
</script>
<template>
<!-- 滚动容器 -->
<scroll-view
refresher-enabled
@refresherrefresh="onRefresherrefresh"
:refresher-triggered="isTriggered"
scroll-y
>
....
</scroll-view>
</template>
2.分页加载
-
定义
pageParams对象,存储分页参数,包括页码page和每页数据量pageSize。 -
定义
finish响应式引用,表示是否已加载完所有数据。 -
滚动触底事件:给
scroll-view组件绑定@scrolltolower事件。 -
在事件内部需要判断是否已加载完所有数据,没有结束就继续发送请求,同时页码要累加,获取的数据要追加到原数组后,如果分页已结束,就更新
finish标记,并提醒用户。
<script setup lang="ts">
// 分页参数
const pageParams: Required<PageParams> = {
page: 1,
pageSize: 10,
}
// 猜你喜欢的列表
const guessList = ref<GuessItem[]>([])
// 已结束标记
const finish = ref(false)
// 获取猜你喜欢数据
const getHomeGoodsGuessLikeData = async () => {
// 退出分页判断
if (finish.value === true) {
return uni.showToast({ icon: 'none', title: '没有更多数据~' })
}
const res = await getHomeGoodsGuessLikeAPI(pageParams)
// 数组追加
guessList.value.push(...res.result.items)
// 分页条件
if (pageParams.page < res.result.pages) {
// 页码累加
pageParams.page++
} else {
finish.value = true
}
}
// 重置数据
const resetData = () => {
pageParams.page = 1
guessList.value = []
finish.value = false
}
// 组件挂载完毕
onMounted(() => {
getHomeGoodsGuessLikeData()
})
</script>
<template>
<!-- 滚动容器 -->
<scroll-view @scrolltolower="onScrolltolower" scroll-y>
<!-- 猜你喜欢列表 -->
<view class="guess">
<navigator
class="guess-item"
v-for="item in guessList"
:key="item.id"
:url="`/pages/goods/goods?id=${item.id}`"
>
<image class="image" mode="aspectFill" :src="item.picture"></image>
<view class="name"> {{ item.name }} </view>
<view class="price">
<text class="small">¥</text>
<text>{{ item.price }}</text>
</view>
</navigator>
</view>
<view class="loading-text">
{{ finish ? '没有更多数据~' : '正在加载...' }}
</view>
</scroll-view>
</template>
8.表单元素处理
1.input支持v-model双向绑定。<radio> 、<checkbox> 和 <picker> 等不支持 v-model 指令,可以使用 :value 和 @change 代替 v-model 来实现类似的效果。
2.项目中我们还通过 uni-forms 实现了表单的校验。
以下是使用 uni-forms 实现表单校验的具体步骤:
- 创建表单数据和验证规则:使用
ref函数创建表单数据对象form和验证规则对象rules。 - 设置 ·uni-forms 属性:在
<uni-forms>组件上设置:model和:rules属性,分别绑定到form和rules。 - 使用 uni-form-item 组件和 input 组件构建表单:为每个表单项创建一个
<uni-form-item>组件,并设置name属性。在表单项内部使用<input>组件,并使用v-model指令进行双向数据绑定。 - 创建表单引用:使用
ref函数创建一个名为formRef的引用,将其设置为<uni-forms>组件的ref属性。 - 创建表单提交处理函数:定义一个名为
onSubmit的函数,在此函数内部使用formRef.value.validate()方法进行表单验证。根据验证结果执行相应的逻辑(例如,提交表单或显示错误提示)。 - 添加提交按钮:在模板中添加一个提交按钮,为其设置
@tap事件监听器,绑定到onSubmit函数。
<script setup lang="ts">
import { ref } from 'vue'
const form = ref({
username: '',
password: '',
})
const rules: UniHelper.UniFormsRules = {
username: {
rules: [{ required: true, errorMessage: '用户名不能为空' }],
},
password: {
rules: [{ required: true, errorMessage: '密码不能为空' }],
},
}
const formRef = ref<UniHelper.UniFormsInstance>()
const onSubmit = async () => {
try {
const result = await formRef.value?.validate!()
console.log('校验通过,提交数据:', result)
} catch (error) {
console.log('校验未通过:', error)
}
}
</script>
<template>
<view>
<uni-forms :model="form" :rules="rules" ref="formRef">
<uni-forms-item label="用户名:" name="username">
<input v-model="form.username" placeholder="请输入用户名" />
</uni-forms-item>
<uni-forms-item label="密码:" name="password">
<input password v-model="form.password" placeholder="请输入密码" />
</uni-forms-item>
<view>
<button @tap="onSubmit">提交</button>
</view>
</uni-forms>
</view>
</template>
小程序端的表单组件具有一些特有的属性,外观和功能都有些差异,如 input 组件的 type 属性支持的值与网页端有所不同。例如,小程序端的 input 组件有一个 idcard 类型,而网页端没有。所以不要完全凭借网页端的经验处理小程序的表单,尽管部分表单组件的名称和网页端同名,也要应该要查阅 uni-app 组件部分的文档了解差异。
9.uni-app开发过程中遇到的问题
1.跨平台兼容性问题:不同平台兼容性问题需要参考官方问题解决,比如 H5 端不支持微信登录,滚动驱动的动画等功能。
2.性能问题:使用分包加载,按需加载页面和资源
3.兼容性问题:查找针对 uni-app 的插件版本;(如 SKU 组件要筛选支持 Vue3 版的,axios 不支持小程序端则自行封装),修改插件配置,以适应 uni-app 的环境;(如 Pinia 持久化存储方案需要改默认配置)。
4.更新和维护问题: 随着项目的发展,可能需要更新和维护代码。为了降低维护成本,持代码的模块化和组件化;(一直都有保持),遵循良好的编码规范;(一直都有保持)
10.uni-app中条件编译和平台差异化处理
- 条件编译(常见): ,条件编译是在编译阶段根据预设条件对代码进行不同分支的编译。在 uni-app 中,可以通过条件编译实现针对不同平台编译不同的代码。 通过在代码中添加特定的注释来实现条件编译。例如:
// #ifdef H5
console.log('这段代码只编译到H5端')
// #endif
// #ifdef MP-WEIXIN
console.log('这段代码只编译到微信小程序端')
// #endif
- 平台差异化处理:平台差异化处理是在运行时根据当前平台执行不同的代码逻辑。在 uni-app 中,可以通过
const { osName } = uni.getSystemInfoSync()来判断当前平台,然后编写针对不同平台的代码逻辑。例如:
// 获取系统名称
const { osName } = uni.getSystemInfoSync()
if (osName === 'ios') {
console.log('ios平台执行的逻辑')
} else if (osName === 'android') {
console.log('android平台执行的逻辑')
}
总结: 条件编译和平台差异化处理是 uni-app 为解决多平台兼容性问题提供的两种方法。条件编译更加常见,在编译阶段根据预设条件对代码进行不同分支的编译,而平台差异化处理是在运行时根据当前平台执行不同的代码逻辑。根据项目需求和场景,可以灵活选择使用这两种方法。
条件编译在 uni-app 中有很多应用场景,主要用于处理不同平台间的差异和兼容性问题。以下是一些常见的应用场景:
- 登录功能差异:不同平台可能有不同的登录方式和 API。例如,微信小程序可以使用微信登录,而 H5 平台可能需要使用其他登录方式,如手机号+验证码。这种情况下,可以使用条件编译为不同平台提供适当的登录实现。
// #ifdef MP-WEIXIN
// 微信小程序登录
const { code } = wx.login()
// #endif
// #ifdef H5
// H5平台登录,如手机号+验证码
// ...
// #endif
- API 差异:不同平台可能存在 API 差异,有的 API 在某些平台上可能不可用。在这种情况下,可以使用条件编译来处理平台差异。
// #ifdef H5
navigator.geolocation.getCurrentPosition((position) => {
// H5获取地理位置
})
// #endif
// #ifdef MP-WEIXIN
wx.getLocation({
type: 'wgs84',
success(res) {
// 微信小程序获取地理位置
},
})
// #endif
- UI 组件差异:不同平台的 UI 组件可能存在差异,例如 导航栏、底部标签栏,微信小程序的
picker组件和 H5 平台的select元素。可以使用条件编译针对不同平台提供不同的 UI 组件。
<template>
<!-- #ifdef H5 -->
<select>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<picker>
<view>Option 1</view>
<view>Option 2</view>
</picker>
<!-- #endif -->
</template>
- 资源路径差异:有时,不同平台对资源路径的处理方式不同,可以使用条件编译为不同平台提供适当的资源路径。
<template>
<!-- #ifdef H5 -->
<img data-fancybox="gallery" src="/static/img/logo.png" />
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<image src="/static/img/logo-weixin.png" />
<!-- #endif -->
</template>
总之,条件编译在 uni-app 开发过程中具有广泛的应用场景,主要用于解决不同平台间的差异和兼容性问题。根据具体需求,可以灵活运用条件编译来实现适配不同平台的功能和表现。