项目目录结构详解
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-WEIXIN
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
navbarHeight.value = menuButtonInfo.bottom + menuButtonInfo.top - systemInfo.statusBarHeight
// #endif
// #ifdef H5 || APP-PLUS
navbarHeight.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' })
break
case 404:
message = '请求地址不存在'
break
case 500:
message = '服务器内部错误'
break
default:
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 = true
try {
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-WEIXIN
return true
// #endif
return false
}
export const isH5 = () => {
// #ifdef H5
return true
// #endif
return false
}
export const isApp = () => {
// #ifdef APP-PLUS
return true
// #endif
return false
}
export const getPlatform = () => {
// #ifdef MP-WEIXIN
return 'weapp'
// #endif
// #ifdef H5
return 'h5'
// #endif
// #ifdef APP-PLUS
return 'app'
// #endif
return '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-WEIXIN
wx.compressImage({
src,
quality: 80,
success: (res) => resolve(res.tempFilePath),
fail: reject
})
// #endif
// #ifdef H5 || APP-PLUS
resolve(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) return
loading.value = true
try {
// 模拟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
上传代码
在微信开发者工具中点击"上传"
填写版本号和项目备注
提交审核
登录微信公众平台
在管理后台提交审核
发布上线
- 审核通过后,点击发布