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-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

上传代码

  • 在微信开发者工具中点击"上传"

  • 填写版本号和项目备注

提交审核

  • 登录微信公众平台

  • 在管理后台提交审核

发布上线

  • 审核通过后,点击发布
相关推荐
用力的活着4 小时前
uniapp 微信小程序蓝牙接收中文乱码
微信小程序·小程序·uni-app
一枚前端小能手4 小时前
「周更第7期」实用JS库推荐:Vite
前端·javascript·vite
润 下5 小时前
C语言——深入解析C语言指针:从基础到实践从入门到精通(二)
c语言·开发语言·经验分享·笔记·学习·程序人生
小中12345 小时前
异步请求的性能提升
前端
我是天龙_绍5 小时前
渐变层生成器——直接用鼠标拖拽就可以调整渐变层的各种参数,然后可以导出为svg格式
前端
我是天龙_绍5 小时前
Easing 曲线 easings.net
前端
知识分享小能手5 小时前
微信小程序入门学习教程,从入门到精通,电影之家小程序项目知识点详解 (17)
前端·javascript·学习·微信小程序·小程序·前端框架·vue
訾博ZiBo5 小时前
React组件复用导致的闪烁问题及通用解决方案
前端
Dever5 小时前
记一次 CORS 深水坑:开启 withCredentials 后Response headers 只剩 content-type
前端·javascript