uniapp学习【整体实践】
项目目录结构详解
my-project/
├── pages/ # 页面文件目录
│ ├── index/
│ │ ├── index.vue # 页面组件
│ │ └── index.json # 页面配置文件
├── static/ # 静态资源目录
├── components/ # 自定义组件目录
├── uni_modules/ # uni模块目录
├── App.vue # 应用入口文件
├── main.js # 主入口文件
├── manifest.json # 应用配置文件
├── pages.json # 页面路由与样式配置
└── uni.scss # 全局样式文件
配置文件详解
manifest.json 应用配置
{
"name": "我的应用",
"appid": "__UNI__XXXXXX",
"description": "应用描述",
"versionName": "1.0.0",
"versionCode": "100",
"mp-weixin": {
"appid": "wx1234567890", // 微信小程序appid
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true
},
"usingComponents": true
},
"vueVersion": "3"
}
pages.json 页面配置
{"pages": [{"path": "pages/index/index","style": {"navigationBarTitleText": "首页","enablePullDownRefresh": true,"navigationStyle": "default"}}],"globalStyle": {"navigationBarTextStyle": "black","navigationBarTitleText": "uni-app","navigationBarBackgroundColor": "#F8F8F8","backgroundColor": "#F8F8F8"},"tabBar": {"color": "#7A7E83","selectedColor": "#3cc51f","borderStyle": "black","backgroundColor": "#ffffff","list": [{"pagePath": "pages/index/index","iconPath": "static/tabbar/home.png","selectedIconPath": "static/tabbar/home-active.png","text": "首页"}]}
}
Vue3基础与uniapp结合
Vue3组合式API基础
<template><view class="container"><text>{{ count }}</text><button @click="increment">增加</button><text>{{ doubleCount }}</text></view>
</template><script setup>
import { ref, computed, onMounted } from 'vue'// 响应式数据
const count = ref(0)// 计算属性
const doubleCount = computed(() => count.value * 2)// 方法
const increment = () => {count.value++
}// 生命周期
onMounted(() => {console.log('组件挂载完成')
})
</script><style scoped>
.container {padding: 20rpx;
}
</style>
Uniapp生命周期
<script setup>
import { onLoad, onShow, onReady, onHide, onUnload } from '@dcloudio/uni-app'// 页面加载时触发
onLoad((options) => {console.log('页面加载', options)
})// 页面显示时触发
onShow(() => {console.log('页面显示')
})// 页面初次渲染完成
onReady(() => {console.log('页面就绪')
})// 页面隐藏时触发
onHide(() => {console.log('页面隐藏')
})// 页面卸载时触发
onUnload(() => {console.log('页面卸载')
})
</script>
条件编译
<template><view><!-- #ifdef MP-WEIXIN --><view>仅在小程序中显示</view><!-- #endif --><!-- #ifdef H5 --><view>仅在H5中显示</view><!-- #endif --><!-- #ifdef APP-PLUS --><view>仅在App中显示</view><!-- #endif --></view>
</template><script setup>
// 条件编译JS代码
// #ifdef MP-WEIXIN
const weixinOnly = '微信小程序特有'
// #endif// #ifdef H5
const h5Only = 'H5特有'
// #endif
</script><style>
/* 条件编译样式 */
/* #ifdef MP-WEIXIN */
.weixin-style {color: red;
}
/* #endif */
</style>
UI组件库详解
常用UI组件库介绍
uView UI(推荐)
# 安装uView
npm install uview-ui# 或通过uni_modules安装
在uni-app插件市场搜索uView,导入到项目中
配置uView
// main.js
import uView from 'uview-ui'
import { createSSRApp } from 'vue'export function createApp() {const app = createSSRApp(App)app.use(uView)return {app}
}
// uni.scss
@import 'uview-ui/theme.scss';
// pages.json
{"easycom": {"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"}
}
基础组件使用
<template><view class="container"><!-- 布局组件 --><u-grid :col="3"><u-grid-item v-for="item in 6" :key="item"><u-icon name="photo" :size="46"></u-icon><text class="grid-text">网格{{item}}</text></u-grid-item></u-grid><!-- 表单组件 --><u-form :model="form" :rules="rules" ref="uForm"><u-form-item label="姓名" prop="name"><u-input v-model="form.name" placeholder="请输入姓名" /></u-form-item><u-form-item label="年龄" prop="age"><u-input v-model="form.age" type="number" placeholder="请输入年龄" /></u-form-item></u-form><!-- 按钮组件 --><u-button type="primary" @click="submit">提交</u-button><!-- 反馈组件 --><u-toast ref="uToast"></u-toast></view>
</template><script setup>
import { ref, reactive } from 'vue'const form = reactive({name: '',age: ''
})const rules = {name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],age: [{ required: true, message: '请输入年龄', trigger: 'blur' },{ type: 'number', message: '年龄必须为数字', trigger: 'blur' }]
}const submit = () => {// 表单验证uni.showToast({title: '提交成功',icon: 'success'})
}
</script>
自定义组件开发
<!-- components/my-button/my-button.vue -->
<template><button class="my-button" :class="[type, size, { disabled: disabled }]":disabled="disabled"@click="handleClick"><slot></slot></button>
</template><script setup>
import { computed } from 'vue'const props = defineProps({type: {type: String,default: 'default',validator: (value) => {return ['default', 'primary', 'success', 'warning', 'danger'].includes(value)}},size: {type: String,default: 'normal',validator: (value) => {return ['small', 'normal', 'large'].includes(value)}},disabled: {type: Boolean,default: false}
})const emit = defineEmits(['click'])const handleClick = (event) => {if (!props.disabled) {emit('click', event)}
}
</script><style scoped>
.my-button {padding: 20rpx 40rpx;border: none;border-radius: 10rpx;font-size: 32rpx;transition: all 0.3s;
}.my-button.primary {background-color: #2979ff;color: white;
}.my-button.small {padding: 15rpx 30rpx;font-size: 28rpx;
}.my-button.large {padding: 25rpx 50rpx;font-size: 36rpx;
}.my-button.disabled {opacity: 0.6;pointer-events: none;
}
</style>
路由与导航
页面跳转
<script setup>
// 保留当前页面,跳转到应用内的某个页面
const navigateTo = () => {uni.navigateTo({url: '/pages/detail/detail?id=1&name=test',success: () => console.log('跳转成功'),fail: (err) => console.log('跳转失败', err)})
}// 关闭当前页面,跳转到应用内的某个页面
const redirectTo = () => {uni.redirectTo({url: '/pages/detail/detail'})
}// 跳转到 tabBar 页面
const switchTab = () => {uni.switchTab({url: '/pages/home/home'})
}// 关闭所有页面,打开到应用内的某个页面
const reLaunch = () => {uni.reLaunch({url: '/pages/index/index'})
}// 返回上一页面
const navigateBack = () => {uni.navigateBack({delta: 1 // 返回层数})
}
</script>
页面传参与接收
<!-- pages/detail/detail.vue -->
<template><view><text>ID: {{ id }}</text><text>名称: {{ name }}</text></view>
</template><script setup>
import { ref, onLoad } from '@dcloudio/uni-app'const id = ref('')
const name = ref('')onLoad((options) => {id.value = options.id || ''name.value = options.name || ''
})
</script>
导航栏自定义
// pages.json
{"pages": [{"path": "pages/index/index","style": {"navigationBarTitleText": "首页","navigationStyle": "custom", // 自定义导航栏"enablePullDownRefresh": true,"onReachBottomDistance": 50}}]
}
<!-- 自定义导航栏组件 -->
<template><view class="custom-navbar" :style="{ height: navbarHeight + 'px' }"><view class="navbar-content" :style="{ height: statusBarHeight + 'px', paddingTop: statusBarHeight + 'px' }"><view class="navbar-title">{{ title }}</view><view class="navbar-actions"><slot name="right"></slot></view></view></view>
</template><script setup>
import { ref, onMounted } from 'vue'defineProps({title: {type: String,default: ''}
})const navbarHeight = ref(0)
const statusBarHeight = ref(0)onMounted(() => {// 获取系统信息const systemInfo = uni.getSystemInfoSync()statusBarHeight.value = systemInfo.statusBarHeight || 0// 小程序导航栏高度// #ifdef MP-WEIXINconst menuButtonInfo = uni.getMenuButtonBoundingClientRect()navbarHeight.value = menuButtonInfo.bottom + menuButtonInfo.top - systemInfo.statusBarHeight// #endif// #ifdef H5 || APP-PLUSnavbarHeight.value = 44 + statusBarHeight.value// #endif
})
</script>
状态管理
Pinia状态管理(推荐)
# 安装Pinia
npm install pinia @pinia/nuxt
// stores/counter.js
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {state: () => ({count: 0,name: '计数器'}),getters: {doubleCount: (state) => state.count * 2,doubleCountPlusOne() {return this.doubleCount + 1}},actions: {increment() {this.count++},decrement() {this.count--},async incrementAsync() {await new Promise(resolve => setTimeout(resolve, 1000))this.increment()}}
})
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'export function createApp() {const app = createSSRApp(App)const pinia = createPinia()app.use(pinia)return {app}
}
在组件中使用状态
<template><view class="container"><text>计数: {{ count }}</text><text>双倍计数: {{ doubleCount }}</text><button @click="increment">增加</button><button @click="incrementAsync">异步增加</button></view>
</template><script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'const counterStore = useCounterStore()// 使用storeToRefs保持响应式
const { count, doubleCount } = storeToRefs(counterStore)// 直接解构action
const { increment, incrementAsync } = counterStore
</script>
网络请求
请求封装
// utils/request.js
class Request {constructor() {this.baseURL = 'https://api.example.com'this.timeout = 10000}request(config) {return new Promise((resolve, reject) => {uni.request({url: this.baseURL + config.url,method: config.method || 'GET',data: config.data || {},header: {'Content-Type': 'application/json','Authorization': uni.getStorageSync('token') || '',...config.header},timeout: this.timeout,success: (res) => {if (res.statusCode === 200) {resolve(res.data)} else {reject(this.handleError(res))}},fail: (err) => {reject(this.handleError(err))}})})}handleError(error) {let message = '网络请求失败'if (error.statusCode) {switch (error.statusCode) {case 401:message = '未授权,请重新登录'// 跳转到登录页uni.navigateTo({ url: '/pages/login/login' })breakcase 404:message = '请求地址不存在'breakcase 500:message = '服务器内部错误'breakdefault:message = error.data?.message || `请求失败:${error.statusCode}`}}uni.showToast({title: message,icon: 'none'})return message}get(url, data = {}) {return this.request({ url, method: 'GET', data })}post(url, data = {}) {return this.request({ url, method: 'POST', data })}put(url, data = {}) {return this.request({ url, method: 'PUT', data })}delete(url, data = {}) {return this.request({ url, method: 'DELETE', data })}
}export default new Request()
API接口管理
// api/user.js
import request from '@/utils/request'export const userApi = {// 用户登录login(data) {return request.post('/user/login', data)},// 获取用户信息getUserInfo() {return request.get('/user/info')},// 更新用户信息updateUserInfo(data) {return request.put('/user/info', data)},// 上传头像uploadAvatar(filePath) {return new Promise((resolve, reject) => {uni.uploadFile({url: request.baseURL + '/user/avatar',filePath: filePath,name: 'file',header: {'Authorization': uni.getStorageSync('token')},success: (res) => {const data = JSON.parse(res.data)resolve(data)},fail: reject})})}
}
在组件中使用
<script setup>
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'const userInfo = ref({})
const loading = ref(false)const getUserInfo = async () => {loading.value = truetry {const res = await userApi.getUserInfo()userInfo.value = res.data} catch (error) {console.error('获取用户信息失败', error)} finally {loading.value = false}
}onMounted(() => {getUserInfo()
})
</script>
数据缓存
缓存工具类
// utils/storage.js
class Storage {// 同步设置缓存setSync(key, value) {try {uni.setStorageSync(key, value)return true} catch (e) {console.error('设置缓存失败', e)return false}}// 同步获取缓存getSync(key, defaultValue = null) {try {const value = uni.getStorageSync(key)return value || defaultValue} catch (e) {console.error('获取缓存失败', e)return defaultValue}}// 同步移除缓存removeSync(key) {try {uni.removeStorageSync(key)return true} catch (e) {console.error('移除缓存失败', e)return false}}// 清空缓存clearSync() {try {uni.clearStorageSync()return true} catch (e) {console.error('清空缓存失败', e)return false}}// 异步设置缓存set(key, value) {return new Promise((resolve, reject) => {uni.setStorage({key,data: value,success: resolve,fail: reject})})}// 异步获取缓存get(key, defaultValue = null) {return new Promise((resolve) => {uni.getStorage({key,success: (res) => resolve(res.data),fail: () => resolve(defaultValue)})})}
}export default new Storage()
使用示例
<script setup>
import storage from '@/utils/storage'
import { ref, onMounted } from 'vue'const userToken = ref('')// 设置token
const setToken = () => {storage.setSync('token', 'your-token-here')
}// 获取token
const getToken = () => {userToken.value = storage.getSync('token', '')
}// 移除token
const removeToken = () => {storage.removeSync('token')
}onMounted(() => {getToken()
})
</script>
设备API与平台兼容
常用设备API
<script setup>
// 获取系统信息
const getSystemInfo = () => {const systemInfo = uni.getSystemInfoSync()console.log('系统信息:', systemInfo)
}// 网络状态
const getNetworkType = () => {uni.getNetworkType({success: (res) => {console.log('网络类型:', res.networkType)}})
}// 地理位置
const getLocation = () => {uni.getLocation({type: 'wgs84',success: (res) => {console.log('位置:', res.latitude, res.longitude)},fail: (err) => {console.error('获取位置失败', err)}})
}// 扫码
const scanCode = () => {uni.scanCode({success: (res) => {console.log('扫码结果:', res.result)}})
}// 图片选择
const chooseImage = () => {uni.chooseImage({count: 1,sizeType: ['compressed'],sourceType: ['album', 'camera'],success: (res) => {console.log('图片路径:', res.tempFilePaths[0])}})
}// 显示操作菜单
const showActionSheet = () => {uni.showActionSheet({itemList: ['选项1', '选项2', '选项3'],success: (res) => {console.log('选中:', res.tapIndex)}})
}
</script>
平台兼容处理
// utils/platform.js
export const isWeapp = () => {// #ifdef MP-WEIXINreturn true// #endifreturn false
}export const isH5 = () => {// #ifdef H5return true// #endifreturn false
}export const isApp = () => {// #ifdef APP-PLUSreturn true// #endifreturn false
}export const getPlatform = () => {// #ifdef MP-WEIXINreturn 'weapp'// #endif// #ifdef H5return 'h5'// #endif// #ifdef APP-PLUSreturn 'app'// #endifreturn 'unknown'
}
性能优化
图片优化
<template><view><!-- 使用webp格式(小程序支持) --><image :src="imageUrl" mode="aspectFill"lazy-load@load="onImageLoad"@error="onImageError"></image><!-- 图片预加载 --><image v-for="img in preloadImages" :key="img" :src="img" style="display: none;" /></view>
</template><script setup>
import { ref } from 'vue'const imageUrl = ref('')
const preloadImages = ref([])// 压缩图片
const compressImage = (src) => {return new Promise((resolve, reject) => {// #ifdef MP-WEIXINwx.compressImage({src,quality: 80,success: (res) => resolve(res.tempFilePath),fail: reject})// #endif// #ifdef H5 || APP-PLUSresolve(src) // H5和App需要自己实现压缩// #endif})
}const onImageLoad = (e) => {console.log('图片加载完成')
}const onImageError = (e) => {console.error('图片加载失败', e)
}
</script>
数据懒加载
<template><view><view v-for="item in visibleData" :key="item.id"class="list-item">{{ item.name }}</view><!-- 加载更多 --><view v-if="hasMore" class="load-more" @click="loadMore">{{ loading ? '加载中...' : '加载更多' }}</view></view>
</template><script setup>
import { ref, onMounted, onReachBottom } from '@dcloudio/uni-app'const allData = ref([])
const visibleData = ref([])
const page = ref(1)
const pageSize = 10
const hasMore = ref(true)
const loading = ref(false)const loadData = async () => {if (loading.value || !hasMore.value) returnloading.value = truetry {// 模拟API调用const newData = await mockApi(page.value, pageSize)if (newData.length < pageSize) {hasMore.value = false}allData.value = [...allData.value, ...newData]visibleData.value = allData.value.slice(0, page.value * pageSize)page.value++} catch (error) {console.error('加载数据失败', error)} finally {loading.value = false}
}// 上拉加载更多
onReachBottom(() => {loadData()
})onMounted(() => {loadData()
})
</script>
打包发布
小程序发布流程
// manifest.json 配置
{"mp-weixin": {"appid": "你的小程序appid","setting": {"urlCheck": false,"es6": true,"enhance": true,"postcss": true},"usingComponents": true,"permission": {"scope.userLocation": {"desc": "你的位置信息将用于小程序位置接口的效果展示"}}}
}
发行步骤
开发环境测试
# 运行到微信开发者工具
npm run dev:mp-weixin
生产环境构建
# 构建小程序
npm run build:mp-weixin
上传代码
在微信开发者工具中点击"上传"
填写版本号和项目备注
提交审核
登录微信公众平台
在管理后台提交审核
发布上线
审核通过后,点击发布