一文读懂 Vue3 在 uni-app 开发中的使用技巧:从入门到精通全面指南

在这里插入图片描述


在这里插入图片描述

第一章:uni-app 与 Vue3 融合的架构解析

1.1 uni-app 框架概述与技术栈演进

uni-app 是一个基于 Vue.js 的跨平台开发框架,开发者编写一套代码,可发布到 iOS、Android、Web(响应式)、以及各种小程序平台。随着 Vue3 的正式发布,uni-app 也在 2021 年正式支持 Vue3 版本,这标志着 uni-app 开发进入了一个新的时代。

uni-app 对 Vue3 的支持时间线:

  • 2021年6月:uni-app 发布 Vue3 版本公开测试
  • 2021年9月:Vue3 版本正式发布
  • 2022年:全面优化 Vue3 支持,增加 Composition API 完整支持
  • 2023年至今:持续完善生态,提升开发体验

1.2 Vue3 在 uni-app 中的架构差异

在 uni-app 中使用 Vue3 与传统的 Web 开发有几个关键架构差异:

编译层差异:

// uni-app 的编译过程
源代码(Vue3 + uni-app API) → uni-app 编译器 → 各平台原生代码

// 与传统 Vue3 编译对比
传统 Vue3: .vue文件 → Vite/Webpack → 浏览器可执行代码
uni-app Vue3: .vue文件 → uni-app编译器 → 多平台适配代码

运行时差异:

  • uni-app 需要处理多平台运行时环境
  • DOM API 的模拟与限制
  • 原生组件与 Web 组件的桥接
  • 平台特定 API 的统一封装

1.3 项目初始化与配置要点

创建 Vue3 版本的 uni-app 项目:

# 使用 CLI 创建项目(确保 HBuilderX CLI 版本支持 Vue3)
npx @dcloudio/uvm@latest create my-vue3-uniapp

# 或使用 HBuilderX 可视化创建
# 1. 文件 → 新建 → 项目
# 2. 选择 uni-app 项目
# 3. 模板选择 Vue3 版本

关键配置文件解析:

// manifest.json - 应用配置
{
  "name": "MyVue3App",
  "appid": "__UNI__XXXXXX",
  "description": "基于Vue3的uni-app应用",
  "versionName": "1.0.0",
  "versionCode": "100",
  "vueVersion": "3", // 必须指定为3
  "app-plus": {
    "nvueCompiler": "uni-app",
    "compilerVersion": 3
  },
  "mp-weixin": {
    "appid": "wx1234567890",
    "setting": {
      "urlCheck": false,
      "es6": true,
      "enhance": true,
      "postcss": true
    },
    "usingComponents": true
  }
}

// package.json - 依赖管理
{
  "name": "my-vue3-uniapp",
  "dependencies": {
    "@dcloudio/uni-app": "^3.0.0",
    "@dcloudio/uni-app-plus": "^3.0.0",
    "vue": "^3.2.0"
  },
  "devDependencies": {
    "@dcloudio/types": "^3.0.0",
    "@dcloudio/uni-automator": "^3.0.0",
    "@dcloudio/uni-cli-shared": "^3.0.0",
    "@dcloudio/vite-plugin-uni": "^3.0.0",
    "@vue/compiler-sfc": "^3.2.0"
  }
}

Vite 配置优化(vue.config.js 替代方案):

// vite.config.js (uni-app Vue3 使用 Vite 作为构建工具)
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig({
  plugins: [uni()],
  
  // 优化配置
  resolve: {
    alias: {
      '@': resolve('src'),
      'components': resolve('src/components'),
      'pages': resolve('src/pages'),
      'static': resolve('src/static'),
      'store': resolve('src/store'),
      'utils': resolve('src/utils')
    }
  },
  
  // 构建优化
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: process.env.NODE_ENV === 'production',
        drop_debugger: true
      }
    },
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 分包策略
          if (id.includes('node_modules')) {
            if (id.includes('vue')) {
              return 'vue-vendor'
            }
            if (id.includes('lodash') || id.includes('dayjs')) {
              return 'utils-vendor'
            }
            return 'vendor'
          }
        }
      }
    }
  },
  
  // 开发服务器配置
  server: {
    host: '0.0.0.0',
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

第二章:Vue3 Composition API 在 uni-app 中的深度应用

2.1 响应式系统在 uni-app 中的特殊处理

Vue3 的响应式系统基于 Proxy,但在 uni-app 的小程序环境中需要特别注意:

基础响应式使用:

<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'

// ref 用于基本类型和DOM引用
const count = ref(0)
const pageRef = ref(null) // 页面或组件引用

// reactive 用于复杂对象
const userInfo = reactive({
  name: '',
  age: 0,
  address: {
    city: '',
    province: ''
  }
})

// computed 计算属性
const userDisplay = computed(() => {
  return `${userInfo.name} (${userInfo.age}岁)`
})

// uni-app 中特殊的响应式处理
// 小程序端 Proxy 限制的注意事项
const safeReactive = (obj) => {
  // 对于需要在小程序端深度监听的对象
  return reactive({
    ...obj,
    // 添加标识,确保响应式
    __isReactive: true
  })
}

// 响应式数据在 uni-app 中的特殊场景
const systemInfo = ref({})

// 获取系统信息(异步API)
onLoad(() => {
  uni.getSystemInfo({
    success: (res) => {
      // 直接赋值会失去响应性
      // systemInfo = res ❌
      
      // 正确方式:更新ref的值
      systemInfo.value = res
      
      // 或使用Object.assign
      Object.assign(systemInfo.value, res)
    }
  })
})
</script>

2.2 组合式函数(Composables)的最佳实践

在 uni-app 中创建可复用的组合式函数:

// composables/useUniStorage.ts
import { ref, watch } from 'vue'

/**
 * uni-app 存储的 Composition API 封装
 */
export function useUniStorage<T>(key: string, defaultValue: T) {
  const data = ref<T>(defaultValue)
  
  // 初始化从存储读取
  try {
    const stored = uni.getStorageSync(key)
    if (stored !== null && stored !== undefined) {
      data.value = stored
    }
  } catch (error) {
    console.error(`读取存储 ${key} 失败:`, error)
  }
  
  // 监听变化自动存储
  watch(data, (newValue) => {
    try {
      uni.setStorageSync(key, newValue)
    } catch (error) {
      console.error(`存储 ${key} 失败:`, error)
    }
  }, { deep: true })
  
  // 清除存储的方法
  const clear = () => {
    try {
      uni.removeStorageSync(key)
      data.value = defaultValue
    } catch (error) {
      console.error(`清除存储 ${key} 失败:`, error)
    }
  }
  
  return {
    data,
    clear
  }
}

// composables/useRequest.ts
import { ref, onUnmounted } from 'vue'

interface RequestOptions {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
  timeout?: number
}

/**
 * uni.request 的 Composition API 封装
 */
export function useRequest<T = any>(options: RequestOptions) {
  const loading = ref(false)
  const data = ref<T | null>(null)
  const error = ref<any>(null)
  const task = ref<UniApp.RequestTask | null>(null)
  
  const execute = async (overrideOptions?: Partial<RequestOptions>) => {
    loading.value = true
    error.value = null
    
    try {
      const requestOptions: UniApp.RequestOptions = {
        url: options.url,
        method: options.method || 'GET',
        data: options.data,
        header: {
          'Content-Type': 'application/json',
          ...options.header
        },
        timeout: options.timeout || 10000,
        ...overrideOptions
      }
      
      const result = await new Promise<UniApp.RequestSuccessCallbackResult>((resolve, reject) => {
        const requestTask = uni.request({
          ...requestOptions,
          success: resolve,
          fail: reject
        })
        task.value = requestTask
      })
      
      data.value = result.data as T
      return result.data as T
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  // 组件卸载时中止请求
  onUnmounted(() => {
    if (task.value) {
      task.value.abort()
    }
  })
  
  const abort = () => {
    if (task.value) {
      task.value.abort()
      loading.value = false
    }
  }
  
  return {
    loading,
    data,
    error,
    execute,
    abort,
    // 快捷方法
    get: (data?: any, overrideOptions?: Partial<RequestOptions>) => 
      execute({ method: 'GET', data, ...overrideOptions }),
    post: (data?: any, overrideOptions?: Partial<RequestOptions>) => 
      execute({ method: 'POST', data, ...overrideOptions })
  }
}

2.3 生命周期钩子的变化与适配

Vue3 生命周期与 uni-app 生命周期的融合:

<script setup>
import { 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated,
  onBeforeUnmount, 
  onUnmounted,
  onActivated,
  onDeactivated
} from 'vue'

// Vue3 生命周期
onBeforeMount(() => {
  console.log('Vue3: beforeMount')
})

onMounted(() => {
  console.log('Vue3: mounted')
  // uni-app 页面初始化逻辑
  initPage()
})

onBeforeUnmount(() => {
  console.log('Vue3: beforeUnmount')
  // 清理工作
  cleanup()
})

// uni-app 页面生命周期
// 需要在 pages.json 中配置的页面才能使用
// 注意:Vue3 setup 中需要特殊处理

// 方式1:使用 uni-app 的 hooks(如果框架提供了)
import { onLoad, onShow, onHide, onReady, onUnload } from '@dcloudio/uni-app'

// 方式2:使用 uni-app 生命周期钩子(兼容写法)
const onLoad = (options) => {
  console.log('uni-app: onLoad', options)
  // 获取页面参数
  const query = options
  
  // 场景值处理(小程序)
  if (query.scene) {
    handleScene(query.scene)
  }
}

const onShow = () => {
  console.log('uni-app: onShow')
  // 页面显示时的逻辑
  updatePageData()
}

const onHide = () => {
  console.log('uni-app: onHide')
  // 页面隐藏时的逻辑
  savePageState()
}

const onReady = () => {
  console.log('uni-app: onReady')
  // 页面初次渲染完成
  initDom()
}

const onUnload = () => {
  console.log('uni-app: onUnload')
  // 页面卸载
  destroyResources()
}

// 方式3:对于组件,使用 Vue3 生命周期
// 页面和组件的生命周期使用区别
</script>

<!-- 页面模板中可以直接使用 uni-app 生命周期 -->
<script>
// 传统 Options API 写法中可以直接定义
export default {
  // uni-app 页面生命周期
  onLoad(options) {
    console.log('页面加载:', options)
  },
  
  onShow() {
    console.log('页面显示')
  },
  
  // Vue3 Composition API 与 Options API 混合使用
  setup() {
    // Composition API 逻辑
    return {}
  }
}
</script>

第三章:uni-app 组件系统与 Vue3 的集成

3.1 uni-app 内置组件的 Vue3 写法

<template>
  <!-- 视图容器 -->
  <view class="container">
    <!-- 滚动视图 -->
    <scroll-view 
      scroll-y 
      :scroll-top="scrollTop"
      @scroll="handleScroll"
      class="scroll-container"
    >
      <!-- 条件渲染 -->
      <view v-if="showHeader" class="header">
        <text class="title">{{ pageTitle }}</text>
      </view>
      
      <!-- 列表渲染 -->
      <view 
        v-for="(item, index) in listData" 
        :key="item.id"
        class="list-item"
        @click="handleItemClick(item)"
      >
        <text>{{ index + 1 }}. {{ item.name }}</text>
        <text class="sub-text">{{ item.description }}</text>
      </view>
      
      <!-- 加载更多 -->
      <view v-if="loading" class="loading">
        <text>加载中...</text>
      </view>
      
      <view v-else-if="hasMore" class="load-more" @click="loadMore">
        <text>点击加载更多</text>
      </view>
      
      <view v-else class="no-more">
        <text>没有更多数据了</text>
      </view>
    </scroll-view>
    
    <!-- 表单组件 -->
    <form @submit="handleSubmit" @reset="handleReset">
      <view class="form-group">
        <text class="label">用户名:</text>
        <input 
          v-model="formData.username"
          type="text" 
          placeholder="请输入用户名"
          :focus="autoFocus"
          @input="handleInput"
          @focus="handleFocus"
          @blur="handleBlur"
        />
      </view>
      
      <view class="form-group">
        <text class="label">密码:</text>
        <input 
          v-model="formData.password"
          :type="showPassword ? 'text' : 'password'"
          placeholder="请输入密码"
          @confirm="handleConfirm"
        />
        <text @click="togglePassword" class="toggle-password">
          {{ showPassword ? '隐藏' : '显示' }}
        </text>
      </view>
      
      <view class="form-group">
        <text class="label">性别:</text>
        <radio-group @change="handleGenderChange">
          <label v-for="gender in genderOptions" :key="gender.value">
            <radio :value="gender.value" :checked="formData.gender === gender.value" />
            <text>{{ gender.label }}</text>
          </label>
        </radio-group>
      </view>
      
      <!-- 按钮 -->
      <button 
        type="primary" 
        :loading="submitting" 
        @click="handleSubmit"
        :disabled="!formValid"
      >
        提交
      </button>
      
      <button type="default" @click="handleReset">
        重置
      </button>
    </form>
    
    <!-- 弹窗组件 -->
    <uni-popup ref="popup" type="center">
      <view class="popup-content">
        <text class="popup-title">提示</text>
        <text class="popup-message">{{ popupMessage }}</text>
        <view class="popup-buttons">
          <button @click="closePopup">取消</button>
          <button type="primary" @click="confirmAction">确定</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'

// 响应式数据
const scrollTop = ref(0)
const showHeader = ref(true)
const loading = ref(false)
const hasMore = ref(true)
const autoFocus = ref(false)
const showPassword = ref(false)
const submitting = ref(false)

// 复杂数据使用 reactive
const formData = reactive({
  username: '',
  password: '',
  gender: 'male'
})

const listData = ref([
  { id: 1, name: '项目1', description: '描述1' },
  { id: 2, name: '项目2', description: '描述2' }
])

const genderOptions = [
  { value: 'male', label: '男' },
  { value: 'female', label: '女' },
  { value: 'other', label: '其他' }
]

const popupMessage = ref('')

// 计算属性
const formValid = computed(() => {
  return formData.username.trim() && formData.password.trim()
})

const pageTitle = computed(() => {
  return `当前用户: ${formData.username || '未登录'}`
})

// 方法
const handleScroll = (e) => {
  scrollTop.value = e.detail.scrollTop
}

const handleItemClick = (item) => {
  uni.navigateTo({
    url: `/pages/detail/detail?id=${item.id}`
  })
}

const loadMore = async () => {
  if (loading.value || !hasMore.value) return
  
  loading.value = true
  try {
    // 模拟加载数据
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    const newData = Array.from({ length: 5 }, (_, i) => ({
      id: listData.value.length + i + 1,
      name: `项目${listData.value.length + i + 1}`,
      description: `描述${listData.value.length + i + 1}`
    }))
    
    listData.value.push(...newData)
    
    // 模拟数据加载完毕
    if (listData.value.length >= 20) {
      hasMore.value = false
    }
  } finally {
    loading.value = false
  }
}

const togglePassword = () => {
  showPassword.value = !showPassword.value
}

const handleGenderChange = (e) => {
  formData.gender = e.detail.value
}

const handleSubmit = async () => {
  if (!formValid.value) {
    showPopup('请填写完整信息')
    return
  }
  
  submitting.value = true
  try {
    // 提交逻辑
    await submitForm(formData)
    showPopup('提交成功')
  } catch (error) {
    showPopup('提交失败: ' + error.message)
  } finally {
    submitting.value = false
  }
}

const handleReset = () => {
  Object.assign(formData, {
    username: '',
    password: '',
    gender: 'male'
  })
}

// uni-app 生命周期
onLoad((options) => {
  console.log('页面参数:', options)
  if (options.autoFocus) {
    autoFocus.value = true
  }
})

onReachBottom(() => {
  loadMore()
})

onPullDownRefresh(async () => {
  // 下拉刷新逻辑
  await refreshData()
  uni.stopPullDownRefresh()
})

// 弹窗相关
const popup = ref()

const showPopup = (message) => {
  popupMessage.value = message
  popup.value.open()
}

const closePopup = () => {
  popup.value.close()
}

const confirmAction = () => {
  // 确认操作
  popup.value.close()
}
</script>

<style lang="scss">
.container {
  padding: 20rpx;
}

.scroll-container {
  height: 500rpx;
}

.list-item {
  padding: 20rpx;
  border-bottom: 1rpx solid #eee;
  
  .sub-text {
    color: #666;
    font-size: 24rpx;
    display: block;
    margin-top: 10rpx;
  }
}

.form-group {
  margin-bottom: 30rpx;
  
  .label {
    display: block;
    margin-bottom: 10rpx;
    font-weight: bold;
  }
  
  input {
    border: 1rpx solid #ddd;
    padding: 15rpx;
    border-radius: 8rpx;
  }
}

.toggle-password {
  margin-left: 20rpx;
  color: #007aff;
}

.popup-content {
  background: white;
  padding: 40rpx;
  border-radius: 16rpx;
  
  .popup-title {
    font-size: 32rpx;
    font-weight: bold;
    display: block;
    margin-bottom: 20rpx;
  }
  
  .popup-buttons {
    display: flex;
    justify-content: flex-end;
    margin-top: 30rpx;
    
    button {
      margin-left: 20rpx;
    }
  }
}
</style>

3.2 自定义组件的 Vue3 写法

<!-- components/UserCard.vue -->
<template>
  <view 
    class="user-card"
    :class="{ 'user-card--active': active }"
    @click="handleClick"
  >
    <!-- 插槽使用 -->
    <view v-if="$slots.header" class="user-card__header">
      <slot name="header"></slot>
    </view>
    
    <!-- 默认插槽 -->
    <view class="user-card__content">
      <!-- 作用域插槽 -->
      <slot :user="userInfo" :index="index">
        <!-- 默认内容 -->
        <image 
          v-if="userInfo.avatar" 
          :src="userInfo.avatar" 
          class="user-card__avatar"
          mode="aspectFill"
        />
        <view v-else class="user-card__avatar-default">
          <text>{{ userInfo.name?.charAt(0) || '?' }}</text>
        </view>
        
        <view class="user-card__info">
          <text class="user-card__name">{{ userInfo.name || '未知用户' }}</text>
          <text v-if="userInfo.description" class="user-card__desc">
            {{ userInfo.description }}
          </text>
        </view>
      </slot>
    </view>
    
    <!-- 具名插槽 -->
    <view v-if="$slots.footer" class="user-card__footer">
      <slot name="footer"></slot>
    </view>
    
    <!-- 组件内按钮 -->
    <view v-if="showActions" class="user-card__actions">
      <button 
        v-for="action in actions" 
        :key="action.label"
        :type="action.type || 'default'"
        size="mini"
        @click.stop="handleAction(action)"
      >
        {{ action.label }}
      </button>
    </view>
  </view>
</template>

<script setup>
import { computed } from 'vue'

// 定义 Props
const props = defineProps({
  // 基础类型
  user: {
    type: Object,
    required: true,
    default: () => ({})
  },
  
  index: {
    type: Number,
    default: 0
  },
  
  active: {
    type: Boolean,
    default: false
  },
  
  showActions: {
    type: Boolean,
    default: true
  },
  
  // 验证函数
  actions: {
    type: Array,
    default: () => [],
    validator: (value) => {
      return value.every(item => 
        item && typeof item === 'object' && item.label
      )
    }
  }
})

// 定义 Emits
const emit = defineEmits({
  // 无验证函数
  click: null,
  
  // 有验证函数
  action: (payload) => {
    return payload && typeof payload === 'object'
  }
})

// 计算属性
const userInfo = computed(() => {
  // 合并默认值
  return {
    name: '匿名用户',
    avatar: '',
    description: '',
    ...props.user
  }
})

// 方法
const handleClick = () => {
  emit('click', {
    user: userInfo.value,
    index: props.index
  })
}

const handleAction = (action) => {
  emit('action', {
    action,
    user: userInfo.value,
    index: props.index
  })
  
  // 如果 action 有处理函数,则执行
  if (typeof action.handler === 'function') {
    action.handler(userInfo.value, props.index)
  }
}
</script>

<style lang="scss" scoped>
.user-card {
  background: #fff;
  border-radius: 16rpx;
  padding: 24rpx;
  margin: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
  
  &--active {
    border: 2rpx solid #007aff;
    background: #f0f8ff;
  }
  
  &__content {
    display: flex;
    align-items: center;
  }
  
  &__avatar {
    width: 100rpx;
    height: 100rpx;
    border-radius: 50%;
    margin-right: 20rpx;
    
    &-default {
      width: 100rpx;
      height: 100rpx;
      border-radius: 50%;
      background: #007aff;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 20rpx;
      
      text {
        color: white;
        font-size: 40rpx;
        font-weight: bold;
      }
    }
  }
  
  &__info {
    flex: 1;
  }
  
  &__name {
    display: block;
    font-size: 32rpx;
    font-weight: bold;
    margin-bottom: 8rpx;
  }
  
  &__desc {
    display: block;
    font-size: 26rpx;
    color: #666;
  }
  
  &__actions {
    margin-top: 20rpx;
    display: flex;
    justify-content: flex-end;
    gap: 10rpx;
  }
}
</script>

3.3 组件间通信的高级模式

<!-- 父组件使用示例 -->
<template>
  <view class="parent-container">
    <UserCard
      v-for="(user, index) in users"
      :key="user.id"
      :user="user"
      :index="index"
      :show-actions="true"
      :actions="cardActions"
      @click="handleCardClick"
      @action="handleCardAction"
    >
      <!-- 具名插槽内容 -->
      <template #header>
        <view class="card-header">
          <text class="badge">VIP {{ index + 1 }}</text>
        </view>
      </template>
      
      <!-- 作用域插槽内容 -->
      <template #default="{ user, index }">
        <view class="custom-content">
          <image :src="user.avatar" class="custom-avatar" />
          <view class="custom-info">
            <text class="custom-name">{{ user.name }}</text>
            <text class="custom-email">{{ user.email }}</text>
          </view>
        </view>
      </template>
      
      <!-- 底部插槽 -->
      <template #footer>
        <view class="card-footer">
          <text class="footer-text">最后活跃: {{ user.lastActive }}</text>
        </view>
      </template>
    </UserCard>
    
    <!-- 动态组件 -->
    <component 
      :is="currentComponent"
      :key="componentKey"
      :config="componentConfig"
      @update="handleComponentUpdate"
    />
    
    <!-- Teleport 使用(H5端支持) -->
    <Teleport to="body" v-if="showModal">
      <view class="modal-overlay">
        <view class="modal-content">
          <slot name="modal"></slot>
        </view>
      </view>
    </Teleport>
  </view>
</template>

<script setup>
import { ref, reactive, provide, markRaw } from 'vue'
import UserCard from '@/components/UserCard.vue'
import UserDetail from '@/components/UserDetail.vue'
import UserEdit from '@/components/UserEdit.vue'

// 响应式数据
const users = ref([
  { id: 1, name: '张三', avatar: '/static/avatar1.jpg', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', avatar: '/static/avatar2.jpg', email: 'lisi@example.com' }
])

const currentComponent = ref(null)
const componentKey = ref(0)
const componentConfig = reactive({})
const showModal = ref(false)

// 卡片操作配置
const cardActions = ref([
  { label: '编辑', type: 'primary', handler: editUser },
  { label: '删除', type: 'warn', handler: deleteUser }
])

// 使用 provide/inject 进行深层组件通信
provide('userContext', {
  users,
  refreshUsers: async () => {
    // 刷新用户数据
    users.value = await fetchUsers()
  },
  updateUser: (id, data) => {
    const index = users.value.findIndex(u => u.id === id)
    if (index !== -1) {
      users.value[index] = { ...users.value[index], ...data }
    }
  }
})

// 方法
const handleCardClick = (payload) => {
  console.log('卡片点击:', payload)
  // 跳转到详情页
  uni.navigateTo({
    url: `/pages/user/detail?id=${payload.user.id}`
  })
}

const handleCardAction = (payload) => {
  console.log('卡片操作:', payload)
  
  switch (payload.action.label) {
    case '编辑':
      openEditComponent(payload.user)
      break
    case '删除':
      confirmDelete(payload.user)
      break
  }
}

const openEditComponent = (user) => {
  currentComponent.value = markRaw(UserEdit)
  componentConfig.user = user
  componentKey.value++ // 强制重新渲染
}

const editUser = (user) => {
  console.log('编辑用户:', user)
  // 编辑逻辑
}

const deleteUser = (user) => {
  uni.showModal({
    title: '确认删除',
    content: `确定删除用户 ${user.name} 吗?`,
    success: (res) => {
      if (res.confirm) {
        users.value = users.value.filter(u => u.id !== user.id)
      }
    }
  })
}

const handleComponentUpdate = (data) => {
  console.log('组件更新:', data)
  // 处理组件更新逻辑
}

// 动态加载组件(代码分割)
const loadDynamicComponent = async () => {
  if (process.env.UNI_PLATFORM === 'h5') {
    // H5 端可以使用动态导入
    const module = await import('@/components/DynamicComponent.vue')
    currentComponent.value = markRaw(module.default)
  } else {
    // 小程序端需要提前导入
    import('@/components/DynamicComponent.vue').then(module => {
      currentComponent.value = markRaw(module.default)
    })
  }
}
</script>

<style lang="scss">
.parent-container {
  padding: 20rpx;
}

.card-header {
  .badge {
    background: #ff9500;
    color: white;
    padding: 4rpx 12rpx;
    border-radius: 20rpx;
    font-size: 24rpx;
  }
}

.custom-content {
  display: flex;
  align-items: center;
  
  .custom-avatar {
    width: 80rpx;
    height: 80rpx;
    border-radius: 50%;
    margin-right: 20rpx;
  }
  
  .custom-info {
    flex: 1;
    
    .custom-name {
      display: block;
      font-size: 28rpx;
      font-weight: bold;
    }
    
    .custom-email {
      display: block;
      font-size: 24rpx;
      color: #666;
    }
  }
}

.card-footer {
  border-top: 1rpx solid #eee;
  padding-top: 16rpx;
  margin-top: 16rpx;
  
  .footer-text {
    font-size: 22rpx;
    color: #999;
  }
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  
  .modal-content {
    background: white;
    border-radius: 20rpx;
    padding: 40rpx;
    max-width: 600rpx;
    max-height: 80vh;
    overflow: auto;
  }
}
</style>

第四章:状态管理与数据持久化

4.1 Pinia 在 uni-app 中的完整实践

// store/index.ts - Pinia 主文件
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
import type { App } from 'vue'

const pinia = createPinia()

// 持久化存储配置
pinia.use(createPersistedState({
  storage: {
    getItem(key: string): string | null {
      try {
        return uni.getStorageSync(key)
      } catch (error) {
        console.error('读取存储失败:', error)
        return null
      }
    },
    setItem(key: string, value: string) {
      try {
        uni.setStorageSync(key, value)
      } catch (error) {
        console.error('设置存储失败:', error)
      }
    },
    removeItem(key: string) {
      try {
        uni.removeStorageSync(key)
      } catch (error) {
        console.error('删除存储失败:', error)
      }
    }
  },
  
  // 序列化配置
  serializer: {
    serialize: JSON.stringify,
    deserialize: JSON.parse
  },
  
  // 全局配置
  key: (id) => `__pinia__${id}`,
  auto: false // 不自动持久化,需要显式声明
}))

export function setupStore(app: App) {
  app.use(pinia)
}

export { pinia }

// store/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserInfo, LoginParams } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  // State
  const token = ref<string>('')
  const userInfo = ref<UserInfo | null>(null)
  const permissions = ref<string[]>([])
  const loginLoading = ref(false)
  
  // Getters
  const isLogin = computed(() => !!token.value)
  const hasPermission = computed(() => (permission: string) => {
    return permissions.value.includes(permission)
  })
  const userRole = computed(() => userInfo.value?.role || 'guest')
  
  // Actions
  const login = async (params: LoginParams) => {
    loginLoading.value = true
    try {
      // 调用登录接口
      const response = await uni.request({
        url: '/api/auth/login',
        method: 'POST',
        data: params
      })
      
      const { token: authToken, user, permissions: userPermissions } = response.data
      
      // 更新状态
      token.value = authToken
      userInfo.value = user
      permissions.value = userPermissions
      
      // 存储到本地
      uni.setStorageSync('token', authToken)
      
      return { success: true, data: user }
    } catch (error) {
      console.error('登录失败:', error)
      throw error
    } finally {
      loginLoading.value = false
    }
  }
  
  const logout = () => {
    token.value = ''
    userInfo.value = null
    permissions.value = []
    
    // 清除本地存储
    uni.removeStorageSync('token')
    uni.removeStorageSync('userInfo')
    
    // 跳转到登录页
    uni.reLaunch({
      url: '/pages/login/login'
    })
  }
  
  const updateUserInfo = async (info: Partial<UserInfo>) => {
    if (!userInfo.value) return
    
    try {
      const response = await uni.request({
        url: '/api/user/update',
        method: 'PUT',
        data: info,
        header: {
          'Authorization': `Bearer ${token.value}`
        }
      })
      
      userInfo.value = { ...userInfo.value, ...info }
      return response.data
    } catch (error) {
      console.error('更新用户信息失败:', error)
      throw error
    }
  }
  
  const checkAuth = async () => {
    const localToken = uni.getStorageSync('token')
    if (!localToken) return false
    
    try {
      const response = await uni.request({
        url: '/api/auth/check',
        method: 'GET',
        header: {
          'Authorization': `Bearer ${localToken}`
        }
      })
      
      if (response.data.valid) {
        token.value = localToken
        userInfo.value = response.data.user
        permissions.value = response.data.permissions
        return true
      }
    } catch (error) {
      console.error('验证token失败:', error)
    }
    
    return false
  }
  
  return {
    // State
    token,
    userInfo,
    permissions,
    loginLoading,
    
    // Getters
    isLogin,
    hasPermission,
    userRole,
    
    // Actions
    login,
    logout,
    updateUserInfo,
    checkAuth
  }
}, {
  // 持久化配置
  persist: {
    key: 'user-store',
    paths: ['token', 'userInfo', 'permissions'],
    storage: {
      getItem(key) {
        return uni.getStorageSync(key)
      },
      setItem(key, value) {
        uni.setStorageSync(key, value)
      }
    }
  }
})

// store/modules/app.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAppStore = defineStore('app', () => {
  // State
  const theme = ref<'light' | 'dark'>('light')
  const language = ref('zh-CN')
  const systemInfo = ref<UniApp.GetSystemInfoResult | null>(null)
  const networkType = ref<UniApp.NetworkType | null>(null)
  const appVersion = ref('1.0.0')
  const showTabBar = ref(true)
  const currentRoute = ref('')
  
  // Getters
  const isDarkMode = computed(() => theme.value === 'dark')
  const isWideScreen = computed(() => {
    if (!systemInfo.value) return false
    return systemInfo.value.screenWidth > 768
  })
  const isOnline = computed(() => networkType.value !== 'none')
  
  // Actions
  const initApp = async () => {
    // 获取系统信息
    systemInfo.value = uni.getSystemInfoSync()
    
    // 获取网络状态
    networkType.value = (await uni.getNetworkType()).networkType
    
    // 监听网络变化
    uni.onNetworkStatusChange((res) => {
      networkType.value = res.networkType
    })
    
    // 监听主题变化(H5)
    if (process.env.UNI_PLATFORM === 'h5') {
      const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
      darkModeMedia.addListener((e) => {
        theme.value = e.matches ? 'dark' : 'light'
      })
    }
    
    // 获取版本信息
    const accountInfo = uni.getAccountInfoSync()
    appVersion.value = accountInfo.miniProgram?.version || '1.0.0'
  }
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
    updateThemeStyle()
  }
  
  const updateThemeStyle = () => {
    if (theme.value === 'dark') {
      uni.setTabBarStyle({
        backgroundColor: '#1a1a1a',
        color: '#ffffff',
        selectedColor: '#007aff'
      })
    } else {
      uni.setTabBarStyle({
        backgroundColor: '#ffffff',
        color: '#000000',
        selectedColor: '#007aff'
      })
    }
  }
  
  const setLanguage = (lang: string) => {
    language.value = lang
    uni.setLocale(lang)
  }
  
  const hideTabBar = () => {
    showTabBar.value = false
    uni.hideTabBar()
  }
  
  const showTabBarWithAnimation = () => {
    showTabBar.value = true
    uni.showTabBar({
      animation: true
    })
  }
  
  const updateRoute = (route: string) => {
    currentRoute.value = route
  }
  
  return {
    // State
    theme,
    language,
    systemInfo,
    networkType,
    appVersion,
    showTabBar,
    currentRoute,
    
    // Getters
    isDarkMode,
    isWideScreen,
    isOnline,
    
    // Actions
    initApp,
    toggleTheme,
    setLanguage,
    hideTabBar,
    showTabBarWithAnimation,
    updateRoute
  }
}, {
  persist: {
    key: 'app-store',
    paths: ['theme', 'language'],
    storage: {
      getItem(key) {
        return uni.getStorageSync(key)
      },
      setItem(key, value) {
        uni.setStorageSync(key, value)
      }
    }
  }
})

4.2 全局状态与本地状态的协同

<!-- 在组件中使用状态管理 -->
<template>
  <view class="user-profile">
    <!-- 用户信息展示 -->
    <view v-if="userStore.isLogin" class="profile-card">
      <image :src="userStore.userInfo?.avatar" class="avatar" />
      <view class="info">
        <text class="name">{{ userStore.userInfo?.name }}</text>
        <text class="role">{{ userStore.userRole }}</text>
      </view>
      <button @click="handleLogout" size="mini">退出登录</button>
    </view>
    
    <!-- 未登录状态 -->
    <view v-else class="login-prompt">
      <text>请先登录</text>
      <button @click="goToLogin" type="primary">立即登录</button>
    </view>
    
    <!-- 应用设置 -->
    <view class="settings">
      <view class="setting-item">
        <text>主题模式</text>
        <switch 
          :checked="appStore.isDarkMode" 
          @change="toggleTheme"
          color="#007aff"
        />
      </view>
      
      <view class="setting-item">
        <text>语言设置</text>
        <picker 
          :value="languageIndex" 
          :range="languages" 
          @change="changeLanguage"
        >
          <view class="picker">
            {{ appStore.language }}
          </view>
        </picker>
      </view>
      
      <view class="setting-item">
        <text>网络状态</text>
        <text :class="['network-status', appStore.isOnline ? 'online' : 'offline']">
          {{ appStore.isOnline ? '在线' : '离线' }}
        </text>
      </view>
      
      <view class="setting-item">
        <text>应用版本</text>
        <text class="version">{{ appStore.appVersion }}</text>
      </view>
    </view>
    
    <!-- 权限测试 -->
    <view class="permission-test">
      <text class="title">权限测试</text>
      <button 
        v-if="userStore.hasPermission('user:edit')"
        @click="editProfile"
      >
        编辑资料
      </button>
      <button 
        v-if="userStore.hasPermission('admin:dashboard')"
        type="warn"
        @click="goToAdmin"
      >
        管理后台
      </button>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'

// 使用 store
const userStore = useUserStore()
const appStore = useAppStore()

// 使用 storeToRefs 保持响应性
const { isLogin, userInfo, userRole } = storeToRefs(userStore)
const { theme, language, isOnline } = storeToRefs(appStore)

// 本地状态
const languages = ref(['zh-CN', 'en-US', 'ja-JP'])
const languageIndex = computed(() => 
  languages.value.findIndex(lang => lang === language.value)
)

// 生命周期
onMounted(() => {
  // 检查登录状态
  userStore.checkAuth()
  
  // 初始化应用
  appStore.initApp()
})

// 方法
const handleLogout = () => {
  uni.showModal({
    title: '确认退出',
    content: '确定要退出登录吗?',
    success: (res) => {
      if (res.confirm) {
        userStore.logout()
      }
    }
  })
}

const goToLogin = () => {
  uni.navigateTo({
    url: '/pages/login/login'
  })
}

const toggleTheme = () => {
  appStore.toggleTheme()
}

const changeLanguage = (e) => {
  const index = e.detail.value
  appStore.setLanguage(languages.value[index])
}

const editProfile = () => {
  uni.navigateTo({
    url: '/pages/user/edit'
  })
}

const goToAdmin = () => {
  uni.navigateTo({
    url: '/pages/admin/dashboard'
  })
}
</script>

<style lang="scss" scoped>
.user-profile {
  padding: 30rpx;
}

.profile-card {
  display: flex;
  align-items: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 40rpx;
  border-radius: 20rpx;
  margin-bottom: 40rpx;
  color: white;
  
  .avatar {
    width: 120rpx;
    height: 120rpx;
    border-radius: 50%;
    border: 4rpx solid white;
    margin-right: 30rpx;
  }
  
  .info {
    flex: 1;
    
    .name {
      display: block;
      font-size: 36rpx;
      font-weight: bold;
      margin-bottom: 10rpx;
    }
    
    .role {
      display: block;
      font-size: 28rpx;
      opacity: 0.9;
    }
  }
  
  button {
    background: rgba(255, 255, 255, 0.2);
    color: white;
    border: 2rpx solid white;
  }
}

.login-prompt {
  text-align: center;
  padding: 60rpx 30rpx;
  background: #f5f5f5;
  border-radius: 20rpx;
  margin-bottom: 40rpx;
  
  text {
    display: block;
    font-size: 32rpx;
    color: #666;
    margin-bottom: 30rpx;
  }
}

.settings {
  background: white;
  border-radius: 20rpx;
  padding: 0 30rpx;
  margin-bottom: 40rpx;
  
  .setting-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx 0;
    border-bottom: 1rpx solid #eee;
    
    &:last-child {
      border-bottom: none;
    }
    
    text {
      font-size: 32rpx;
      color: #333;
    }
    
    .picker {
      color: #007aff;
      font-size: 32rpx;
    }
    
    .network-status {
      font-size: 28rpx;
      padding: 8rpx 16rpx;
      border-radius: 20rpx;
      
      &.online {
        background: #e8f5e9;
        color: #4caf50;
      }
      
      &.offline {
        background: #ffebee;
        color: #f44336;
      }
    }
    
    .version {
      color: #666;
      font-size: 28rpx;
    }
  }
}

.permission-test {
  background: white;
  border-radius: 20rpx;
  padding: 30rpx;
  
  .title {
    display: block;
    font-size: 32rpx;
    font-weight: bold;
    margin-bottom: 30rpx;
    color: #333;
  }
  
  button {
    margin-bottom: 20rpx;
    
    &:last-child {
      margin-bottom: 0;
    }
  }
}
</style>

第五章:性能优化与最佳实践

5.1 编译时与运行时优化

Vite 配置优化:

// vite.config.js - 深度优化配置
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'

export default defineConfig(({ mode }) => {
  const isProduction = mode === 'production'
  
  return {
    plugins: [
      uni(),
      
      // 包分析工具(开发环境使用)
      !isProduction && visualizer({
        open: true,
        gzipSize: true,
        brotliSize: true
      }),
      
      // Gzip 压缩
      isProduction && viteCompression({
        verbose: true,
        disable: false,
        threshold: 10240,
        algorithm: 'gzip',
        ext: '.gz'
      })
    ],
    
    // 构建配置
    build: {
      target: 'es2015',
      minify: isProduction ? 'terser' : false,
      sourcemap: !isProduction,
      
      // 分包优化
      rollupOptions: {
        output: {
          manualChunks: {
            // 基础库分包
            'vue-vendor': ['vue', 'pinia', 'vue-router'],
            'uni-vendor': ['@dcloudio/uni-app'],
            'utils-vendor': ['lodash-es', 'dayjs', 'axios'],
            'ui-vendor': ['@dcloudio/uni-ui'],
            
            // 按路由分包
            'pages-home': ['src/pages/index/index.vue'],
            'pages-user': ['src/pages/user/**/*.vue'],
            'pages-admin': ['src/pages/admin/**/*.vue']
          },
          
          // 文件名哈希策略
          chunkFileNames: 'assets/js/[name]-[hash].js',
          entryFileNames: 'assets/js/[name]-[hash].js',
          assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
        }
      },
      
      // terser 配置
      terserOptions: {
        compress: {
          drop_console: isProduction,
          drop_debugger: isProduction,
          pure_funcs: isProduction ? ['console.log'] : []
        },
        mangle: {
          safari10: true
        }
      }
    },
    
    // 开发服务器优化
    server: {
      hmr: {
        overlay: true
      },
      fs: {
        strict: false,
        allow: ['..']
      }
    },
    
    // 依赖优化
    optimizeDeps: {
      include: [
        'vue',
        'pinia',
        '@dcloudio/uni-app',
        '@dcloudio/uni-ui'
      ],
      exclude: ['@dcloudio/uni-app-plus']
    },
    
    // 解析配置
    resolve: {
      alias: {
        '@': '/src',
        'vue': 'vue/dist/vue.esm-bundler.js'
      }
    }
  }
})

代码分割与懒加载:

<!-- 路由级懒加载配置 -->
<script setup>
import { defineAsyncComponent, shallowRef, onMounted } from 'vue'

// 组件级懒加载
const LazyComponent = defineAsyncComponent(() =>
  import('@/components/LazyComponent.vue')
)

// 带加载状态的异步组件
const HeavyComponent = defineAsyncComponent({
  loader: () => import('@/components/HeavyComponent.vue'),
  loadingComponent: () => import('@/components/LoadingSpinner.vue'),
  errorComponent: () => import('@/components/ErrorComponent.vue'),
  delay: 200,
  timeout: 3000,
  suspensible: false
})

// 条件加载
const showHeavyComponent = ref(false)

// 图片懒加载指令
const vLazyLoad = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = el.querySelector('img')
          if (img) {
            img.src = binding.value
            observer.unobserve(el)
          }
        }
      })
    })
    
    observer.observe(el)
  }
}
</script>

<template>
  <!-- 异步组件使用 -->
  <Suspense>
    <template #default>
      <LazyComponent v-if="showLazy" />
    </template>
    <template #fallback>
      <view class="loading">加载中...</view>
    </template>
  </Suspense>
  
  <!-- 条件渲染优化 -->
  <view v-if="!showHeavyComponent">
    <button @click="showHeavyComponent = true">加载重型组件</button>
  </view>
  <HeavyComponent v-else />
  
  <!-- 图片懒加载 -->
  <view v-lazy-load="imageUrl" class="image-container">
    <image class="placeholder" src="/static/placeholder.jpg" />
  </view>
</template>

5.2 内存管理与性能监控

// utils/performance.js - 性能监控工具
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map()
    this.startTime = Date.now()
    this.init()
  }
  
  init() {
    // 监听页面性能
    if (typeof uni !== 'undefined') {
      this.setupUniPerformance()
    }
    
    // 内存警告监听(小程序)
    if (uni.onMemoryWarning) {
      uni.onMemoryWarning(this.handleMemoryWarning.bind(this))
    }
    
    // 页面隐藏/显示监听
    uni.onAppHide(this.onAppHide.bind(this))
    uni.onAppShow(this.onAppShow.bind(this))
  }
  
  setupUniPerformance() {
    // 关键生命周期耗时
    const lifecycles = ['onLoad', 'onShow', 'onReady', 'onHide']
    
    lifecycles.forEach(lifecycle => {
      const original = Page.prototype[lifecycle]
      if (original) {
        Page.prototype[lifecycle] = function(...args) {
          const start = performance.now()
          const result = original.apply(this, args)
          const end = performance.now()
          
          console.log(`${lifecycle} 耗时: ${(end - start).toFixed(2)}ms`)
          
          // 上报性能数据
          this.reportPerformance(lifecycle, end - start)
          
          return result
        }
      }
    })
  }
  
  handleMemoryWarning(res) {
    console.warn('内存警告:', res)
    
    // 清理缓存
    this.cleanupMemory()
    
    // 上报内存警告
    this.reportMemoryWarning(res)
  }
  
  cleanupMemory() {
    // 清理不需要的缓存
    const caches = [
      'imageCache',
      'dataCache',
      'componentCache'
    ]
    
    caches.forEach(cacheKey => {
      try {
        uni.removeStorageSync(cacheKey)
      } catch (error) {
        console.error(`清理缓存 ${cacheKey} 失败:`, error)
      }
    })
    
    // 触发垃圾回收提示
    if (uni.triggerGC) {
      uni.triggerGC()
    }
  }
  
  onAppHide() {
    this.appHideTime = Date.now()
    
    // 记录页面停留时间
    const stayTime = Date.now() - this.pageEnterTime
    this.reportPageStayTime(stayTime)
  }
  
  onAppShow() {
    this.pageEnterTime = Date.now()
  }
  
  // 性能数据上报
  reportPerformance(event, duration) {
    const data = {
      event,
      duration,
      timestamp: Date.now(),
      platform: process.env.UNI_PLATFORM,
      url: this.getCurrentRoute()
    }
    
    // 发送到监控服务器
    this.sendToAnalytics('performance', data)
  }
  
  // 内存警告上报
  reportMemoryWarning(warning) {
    const data = {
      type: 'memory_warning',
      level: warning.level,
      timestamp: Date.now(),
      platform: process.env.UNI_PLATFORM
    }
    
    this.sendToAnalytics('memory', data)
  }
  
  // 页面停留时间上报
  reportPageStayTime(stayTime) {
    const data = {
      type: 'page_stay',
      duration: stayTime,
      page: this.getCurrentRoute(),
      timestamp: Date.now()
    }
    
    this.sendToAnalytics('page', data)
  }
  
  sendToAnalytics(type, data) {
    // 实际项目中应该发送到监控服务器
    console.log(`[Analytics ${type}]:`, data)
    
    // 使用 request 发送
    uni.request({
      url: 'https://analytics.example.com/track',
      method: 'POST',
      data: {
        type,
        data,
        appId: '__UNI__XXXXXX'
      },
      header: {
        'Content-Type': 'application/json'
      }
    })
  }
  
  getCurrentRoute() {
    const pages = getCurrentPages()
    return pages.length > 0 ? pages[pages.length - 1].route : ''
  }
  
  // 静态方法:开始计时
  static startMark(name) {
    performance.mark(`start-${name}`)
  }
  
  // 静态方法:结束计时
  static endMark(name) {
    performance.mark(`end-${name}`)
    performance.measure(name, `start-${name}`, `end-${name}`)
    
    const measures = performance.getEntriesByName(name)
    if (measures.length > 0) {
      return measures[0].duration
    }
    return 0
  }
}

// 全局性能监控实例
export const performanceMonitor = new PerformanceMonitor()

// 性能优化工具函数
export const performanceUtils = {
  // 防抖函数
  debounce(func, wait, immediate = false) {
    let timeout
    return function executedFunction(...args) {
      const context = this
      const later = function() {
        timeout = null
        if (!immediate) func.apply(context, args)
      }
      const callNow = immediate && !timeout
      clearTimeout(timeout)
      timeout = setTimeout(later, wait)
      if (callNow) func.apply(context, args)
    }
  },
  
  // 节流函数
  throttle(func, limit) {
    let inThrottle
    return function(...args) {
      const context = this
      if (!inThrottle) {
        func.apply(context, args)
        inThrottle = true
        setTimeout(() => inThrottle = false, limit)
      }
    }
  },
  
  // 批量更新优化
  batchUpdate(callback) {
    // 使用 nextTick 批量更新
    return new Promise(resolve => {
      this.$nextTick(() => {
        callback()
        resolve()
      })
    })
  },
  
  // 图片预加载
  preloadImages(urls) {
    return Promise.all(
      urls.map(url => {
        return new Promise((resolve, reject) => {
          const img = new Image()
          img.onload = resolve
          img.onerror = reject
          img.src = url
        })
      })
    )
  },
  
  // 清理无用数据
  cleanupUnusedData(store, maxAge = 7 * 24 * 60 * 60 * 1000) {
    const now = Date.now()
    const keys = uni.getStorageInfoSync().keys
    
    keys.forEach(key => {
      try {
        const item = uni.getStorageSync(key)
        if (item && item._timestamp && now - item._timestamp > maxAge) {
          uni.removeStorageSync(key)
        }
      } catch (error) {
        console.error(`清理数据 ${key} 失败:`, error)
      }
    })
  }
}

5.3 渲染优化策略

<!-- 列表渲染优化示例 -->
<template>
  <!-- 虚拟列表容器 -->
  <scroll-view 
    scroll-y 
    class="virtual-list-container"
    :style="{ height: containerHeight + 'px' }"
    @scroll="handleScroll"
    :scroll-top="scrollTop"
  >
    <!-- 占位元素,撑开滚动区域 -->
    <view :style="{ height: totalHeight + 'px' }"></view>
    
    <!-- 可视区域内容 -->
    <view 
      class="visible-items"
      :style="{ transform: `translateY(${offsetY}px)` }"
    >
      <!-- 使用 v-memo 优化重复渲染 -->
      <view 
        v-for="item in visibleItems"
        :key="item.id"
        v-memo="[item.id, item.status]"
        class="list-item"
        :class="{ 'list-item--active': item.active }"
      >
        <UserCard 
          :user="item"
          @click="selectItem(item)"
        />
      </view>
    </view>
  </scroll-view>
  
  <!-- 分页加载指示器 -->
  <view v-if="loading" class="loading-indicator">
    <uni-load-more status="loading"></uni-load-more>
  </view>
</template>

<script setup>
import { ref, computed, watchEffect, shallowRef } from 'vue'
import { useVirtualList } from '@/composables/useVirtualList'

// 使用虚拟列表 Hook
const {
  containerRef,
  visibleItems,
  totalHeight,
  offsetY,
  handleScroll
} = useVirtualList({
  data: listData,
  itemHeight: 100,
  containerHeight: 500,
  buffer: 5
})

// 大数据量列表
const listData = shallowRef([])
const loading = ref(false)
const page = ref(1)
const hasMore = ref(true)

// 分页加载数据
const loadData = async () => {
  if (loading.value || !hasMore.value) return
  
  loading.value = true
  try {
    const response = await uni.request({
      url: '/api/data/list',
      data: {
        page: page.value,
        pageSize: 20
      }
    })
    
    const newData = response.data.items
    if (newData.length > 0) {
      listData.value = [...listData.value, ...newData]
      page.value++
    } else {
      hasMore.value = false
    }
  } finally {
    loading.value = false
  }
}

// 初始加载
onMounted(() => {
  loadData()
})

// 滚动到底部加载更多
const handleScrollToBottom = () => {
  if (hasMore.value) {
    loadData()
  }
}

// 选中项目
const selectedItems = ref(new Set())

const selectItem = (item) => {
  if (selectedItems.value.has(item.id)) {
    selectedItems.value.delete(item.id)
  } else {
    selectedItems.value.add(item.id)
  }
}

// 使用计算属性优化渲染
const activeItemCount = computed(() => {
  return listData.value.filter(item => item.active).length
})

const selectedItemCount = computed(() => {
  return selectedItems.value.size
})

// 使用 watchEffect 处理副作用
watchEffect(() => {
  // 当选中项目变化时更新 UI
  if (selectedItemCount.value > 0) {
    uni.setNavigationBarTitle({
      title: `已选中 ${selectedItemCount.value} 项`
    })
  }
})

// 性能监控
const renderCount = ref(0)

onUpdated(() => {
  renderCount.value++
  
  if (renderCount.value % 10 === 0) {
    console.log(`组件已渲染 ${renderCount.value} 次`)
  }
})
</script>

<style lang="scss">
.virtual-list-container {
  position: relative;
  overflow: hidden;
}

.visible-items {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  will-change: transform; /* 启用GPU加速 */
}

.list-item {
  transition: all 0.3s ease;
  
  &--active {
    background-color: #e3f2fd;
    border-left: 4rpx solid #2196f3;
  }
}

.loading-indicator {
  text-align: center;
  padding: 20rpx;
  background: rgba(255, 255, 255, 0.9);
  position: sticky;
  bottom: 0;
}
</style>

第六章:跨平台兼容性处理

6.1 平台特性检测与条件编译

// utils/platform.js - 平台检测工具
export const platformUtils = {
  // 获取当前平台
  getPlatform() {
    return process.env.UNI_PLATFORM
  },
  
  // 判断是否为特定平台
  isH5: process.env.UNI_PLATFORM === 'h5',
  isWeapp: process.env.UNI_PLATFORM === 'mp-weixin',
  isAlipay: process.env.UNI_PLATFORM === 'mp-alipay',
  isBaidu: process.env.UNI_PLATFORM === 'mp-baidu',
  isToutiao: process.env.UNI_PLATFORM === 'mp-toutiao',
  isQQ: process.env.UNI_PLATFORM === 'mp-qq',
  isApp: process.env.UNI_PLATFORM === 'app',
  isIos: process.env.UNI_PLATFORM === 'app' && uni.getSystemInfoSync().platform === 'ios',
  isAndroid: process.env.UNI_PLATFORM === 'app' && uni.getSystemInfoSync().platform === 'android',
  
  // 条件编译执行
  platformExecute(platformHandlers) {
    const platform = this.getPlatform()
    const handler = platformHandlers[platform]
    
    if (handler && typeof handler === 'function') {
      return handler()
    }
    
    // 默认处理
    if (platformHandlers.default && typeof platformHandlers.default === 'function') {
      return platformHandlers.default()
    }
    
    return null
  },
  
  // 平台特定的样式类
  getPlatformClass(baseClass) {
    const platform = this.getPlatform()
    return `${baseClass} ${baseClass}--${platform}`
  },
  
  // API 兼容性包装
  async compatibleRequest(options) {
    return this.platformExecute({
      'h5': async () => {
        // H5 端使用 fetch
        const response = await fetch(options.url, {
          method: options.method,
          headers: options.header,
          body: options.data ? JSON.stringify(options.data) : null
        })
        return response.json()
      },
      
      'mp-weixin': async () => {
        // 小程序端使用 uni.request
        return new Promise((resolve, reject) => {
          uni.request({
            ...options,
            success: resolve,
            fail: reject
          })
        })
      },
      
      'app': async () => {
        // App 端使用 uni.request
        return new Promise((resolve, reject) => {
          uni.request({
            ...options,
            success: resolve,
            fail: reject
          })
        })
      },
      
      default: async () => {
        // 默认使用 uni.request
        return new Promise((resolve, reject) => {
          uni.request({
            ...options,
            success: resolve,
            fail: reject
          })
        })
      }
    })
  },
  
  // 导航兼容性处理
  navigateTo(url) {
    this.platformExecute({
      'h5': () => {
        // H5 端使用 window.location 或 vue-router
        if (typeof window !== 'undefined') {
          window.location.href = url
        }
      },
      
      'mp-weixin': () => {
        // 小程序端使用 uni.navigateTo
        uni.navigateTo({ url })
      },
      
      'app': () => {
        // App 端使用 uni.navigateTo
        uni.navigateTo({ url })
      },
      
      default: () => {
        uni.navigateTo({ url })
      }
    })
  },
  
  // 分享功能兼容
  setupShare(options) {
    this.platformExecute({
      'mp-weixin': () => {
        // 微信小程序分享配置
        uni.showShareMenu({
          withShareTicket: true,
          menus: ['shareAppMessage', 'shareTimeline']
        })
        
        // 页面分享配置
        return {
          onShareAppMessage() {
            return {
              title: options.title,
              path: options.path,
              imageUrl: options.imageUrl
            }
          },
          
          onShareTimeline() {
            return {
              title: options.title,
              query: options.query,
              imageUrl: options.imageUrl
            }
          }
        }
      },
      
      'app': () => {
        // App 端分享
        plus.share.getServices((services) => {
          // 配置分享服务
        })
      },
      
      default: () => {
        // 其他平台分享处理
        console.log('分享功能:', options)
      }
    })
  }
}

// 条件编译指令
export const conditionalCompile = {
  // 条件编译模板指令
  install(app) {
    app.directive('platform', {
      beforeMount(el, binding) {
        const platform = process.env.UNI_PLATFORM
        const value = binding.value
        
        if (typeof value === 'object') {
          // 对象形式:{ include: [], exclude: [] }
          if (value.include && !value.include.includes(platform)) {
            el.style.display = 'none'
          }
          if (value.exclude && value.exclude.includes(platform)) {
            el.style.display = 'none'
          }
        } else if (typeof value === 'string') {
          // 字符串形式:平台名称
          if (value !== platform) {
            el.style.display = 'none'
          }
        } else if (Array.isArray(value)) {
          // 数组形式:多个平台
          if (!value.includes(platform)) {
            el.style.display = 'none'
          }
        }
      }
    })
    
    app.directive('if-platform', {
      beforeMount(el, binding) {
        const platform = process.env.UNI_PLATFORM
        const value = binding.value
        
        if (Array.isArray(value)) {
          if (!value.includes(platform)) {
            el.parentNode?.removeChild(el)
          }
        } else if (value !== platform) {
          el.parentNode?.removeChild(el)
        }
      }
    })
  }
}

6.2 平台特定组件的统一封装

<!-- components/PlatformImage.vue -->
<template>
  <!-- H5 端使用 img -->
  <img 
    v-if="isH5"
    :src="src"
    :alt="alt"
    :class="className"
    :style="style"
    @load="handleLoad"
    @error="handleError"
  />
  
  <!-- 小程序端使用 image -->
  <image 
    v-else
    :src="src"
    :mode="mode"
    :lazy-load="lazyLoad"
    :class="className"
    :style="style"
    @load="handleLoad"
    @error="handleError"
    v-bind="$attrs"
  />
</template>

<script setup>
import { computed } from 'vue'
import { platformUtils } from '@/utils/platform'

const props = defineProps({
  src: {
    type: String,
    required: true
  },
  alt: {
    type: String,
    default: ''
  },
  mode: {
    type: String,
    default: 'scaleToFill'
  },
  lazyLoad: {
    type: Boolean,
    default: false
  },
  className: {
    type: String,
    default: ''
  },
  style: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['load', 'error'])

const isH5 = platformUtils.isH5

// 处理图片加载
const handleLoad = (event) => {
  emit('load', event)
}

// 处理图片错误
const handleError = (event) => {
  emit('error', event)
  
  // 可以在这里添加默认图片逻辑
  if (props.fallbackSrc) {
    // 回退到默认图片
  }
}

// 平台特定的样式
const platformStyle = computed(() => {
  const baseStyle = props.style
  
  if (platformUtils.isWeapp) {
    // 微信小程序特定样式
    return {
      ...baseStyle,
      'wx-mode': props.mode
    }
  }
  
  if (platformUtils.isApp) {
    // App 端特定样式
    return {
      ...baseStyle,
      'app-image': true
    }
  }
  
  return baseStyle
})
</script>

<style lang="scss" scoped>
// 平台特定的样式
:deep(img) {
  max-width: 100%;
  height: auto;
}

:deep(image) {
  &[mode="aspectFit"] {
    object-fit: contain;
  }
  
  &[mode="aspectFill"] {
    object-fit: cover;
  }
  
  &[mode="widthFix"] {
    width: 100%;
    height: auto;
  }
  
  &[mode="heightFix"] {
    height: 100%;
    width: auto;
  }
}
</style>
<!-- components/PlatformScrollView.vue -->
<template>
  <!-- H5 端使用 div + CSS -->
  <div 
    v-if="isH5"
    ref="scrollContainer"
    :class="['scroll-container', containerClass]"
    :style="containerStyle"
    @scroll="handleScroll"
  >
    <slot></slot>
    
    <!-- 下拉刷新(H5端实现) -->
    <div 
      v-if="enablePullDownRefresh"
      ref="pullDownRef"
      :class="['pull-down-refresh', pullDownClass]"
      :style="pullDownStyle"
    >
      <slot name="pull-down" :status="pullDownStatus">
        <div class="default-pull-down">
          {{ pullDownText }}
        </div>
      </slot>
    </div>
    
    <!-- 上拉加载更多 -->
    <div 
      v-if="enableReachBottom"
      ref="reachBottomRef"
      :class="['reach-bottom', reachBottomClass]"
      :style="reachBottomStyle"
    >
      <slot name="reach-bottom" :status="reachBottomStatus">
        <div class="default-reach-bottom">
          {{ reachBottomText }}
        </div>
      </slot>
    </div>
  </div>
  
  <!-- 小程序和 App 端使用 scroll-view -->
  <scroll-view
    v-else
    :scroll-y="scrollY"
    :scroll-x="scrollX"
    :scroll-top="scrollTop"
    :scroll-left="scrollLeft"
    :scroll-into-view="scrollIntoView"
    :scroll-with-animation="scrollWithAnimation"
    :scroll-animation-duration="scrollAnimationDuration"
    :enable-back-to-top="enableBackToTop"
    :enable-flex="enableFlex"
    :show-scrollbar="showScrollbar"
    :refresher-enabled="enablePullDownRefresh"
    :refresher-triggered="pullDownTriggered"
    :lower-threshold="lowerThreshold"
    :upper-threshold="upperThreshold"
    :class="containerClass"
    :style="containerStyle"
    @scroll="handleScroll"
    @scrolltoupper="handleScrollToUpper"
    @scrolltolower="handleScrollToLower"
    @refresherpulling="handleRefresherPulling"
    @refresherrefresh="handleRefresherRefresh"
    @refresherrestore="handleRefresherRestore"
    @refresherabort="handleRefresherAbort"
    v-bind="$attrs"
  >
    <slot></slot>
    
    <!-- 上拉加载更多插槽 -->
    <view 
      v-if="enableReachBottom"
      slot="lower"
      :class="['reach-bottom', reachBottomClass]"
      :style="reachBottomStyle"
    >
      <slot name="reach-bottom" :status="reachBottomStatus">
        <view class="default-reach-bottom">
          {{ reachBottomText }}
        </view>
      </slot>
    </view>
  </scroll-view>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { platformUtils } from '@/utils/platform'

const props = defineProps({
  // 通用属性
  scrollY: {
    type: Boolean,
    default: true
  },
  scrollX: {
    type: Boolean,
    default: false
  },
  scrollTop: {
    type: [Number, String],
    default: 0
  },
  scrollLeft: {
    type: [Number, String],
    default: 0
  },
  scrollIntoView: {
    type: String,
    default: ''
  },
  scrollWithAnimation: {
    type: Boolean,
    default: false
  },
  scrollAnimationDuration: {
    type: Number,
    default: 300
  },
  enableBackToTop: {
    type: Boolean,
    default: false
  },
  enableFlex: {
    type: Boolean,
    default: false
  },
  showScrollbar: {
    type: Boolean,
    default: false
  },
  
  // 下拉刷新
  enablePullDownRefresh: {
    type: Boolean,
    default: false
  },
  pullDownThreshold: {
    type: Number,
    default: 50
  },
  
  // 上拉加载更多
  enableReachBottom: {
    type: Boolean,
    default: false
  },
  lowerThreshold: {
    type: Number,
    default: 50
  },
  upperThreshold: {
    type: Number,
    default: 50
  },
  
  // 样式
  containerClass: {
    type: String,
    default: ''
  },
  containerStyle: {
    type: Object,
    default: () => ({})
  },
  pullDownClass: {
    type: String,
    default: ''
  },
  reachBottomClass: {
    type: String,
    default: ''
  }
})

const emit = defineEmits([
  'scroll',
  'scroll-to-upper',
  'scroll-to-lower',
  'pull-down-refresh',
  'reach-bottom'
])

const isH5 = platformUtils.isH5

// H5 端特定逻辑
const scrollContainer = ref(null)
const pullDownRef = ref(null)
const reachBottomRef = ref(null)

const pullDownStatus = ref('waiting') // waiting, pulling, refreshing, completed
const reachBottomStatus = ref('waiting') // waiting, loading, no-more

const pullDownTriggered = ref(false)

// 下拉刷新文本
const pullDownText = computed(() => {
  switch (pullDownStatus.value) {
    case 'pulling': return '下拉刷新'
    case 'refreshing': return '刷新中...'
    case 'completed': return '刷新完成'
    default: return '下拉刷新'
  }
})

// 上拉加载文本
const reachBottomText = computed(() => {
  switch (reachBottomStatus.value) {
    case 'loading': return '加载中...'
    case 'no-more': return '没有更多了'
    default: return '上拉加载更多'
  }
})

// H5 端下拉刷新实现
let startY = 0
let currentY = 0
let isPulling = false

const handleTouchStart = (e) => {
  if (!props.enablePullDownRefresh) return
  
  const scrollTop = scrollContainer.value.scrollTop
  if (scrollTop > 0) return
  
  startY = e.touches[0].clientY
  isPulling = true
}

const handleTouchMove = (e) => {
  if (!isPulling || !props.enablePullDownRefresh) return
  
  currentY = e.touches[0].clientY
  const deltaY = currentY - startY
  
  if (deltaY > 0) {
    e.preventDefault()
    
    if (deltaY <= props.pullDownThreshold) {
      pullDownStatus.value = 'pulling'
      updatePullDownStyle(deltaY)
    } else {
      pullDownStatus.value = 'refreshing'
      updatePullDownStyle(props.pullDownThreshold)
      
      // 触发刷新
      emit('pull-down-refresh')
    }
  }
}

const handleTouchEnd = () => {
  if (!isPulling) return
  
  if (pullDownStatus.value === 'refreshing') {
    // 保持刷新状态
  } else {
    // 恢复原状
    pullDownStatus.value = 'waiting'
    updatePullDownStyle(0)
  }
  
  isPulling = false
}

const updatePullDownStyle = (height) => {
  if (pullDownRef.value) {
    pullDownRef.value.style.height = `${height}px`
    pullDownRef.value.style.transform = `translateY(${height}px)`
  }
}

// H5 端上拉加载更多
const handleScroll = (e) => {
  if (isH5) {
    const container = scrollContainer.value
    const scrollHeight = container.scrollHeight
    const scrollTop = container.scrollTop
    const clientHeight = container.clientHeight
    
    // 检查是否滚动到底部
    if (props.enableReachBottom && 
        scrollHeight - scrollTop - clientHeight < props.lowerThreshold &&
        reachBottomStatus.value === 'waiting') {
      reachBottomStatus.value = 'loading'
      emit('reach-bottom')
    }
  }
  
  emit('scroll', e)
}

// 完成下拉刷新
const completePullDown = () => {
  if (isH5) {
    pullDownStatus.value = 'completed'
    setTimeout(() => {
      pullDownStatus.value = 'waiting'
      updatePullDownStyle(0)
    }, 1000)
  } else {
    pullDownTriggered.value = false
  }
}

// 完成上拉加载
const completeReachBottom = (hasMore = true) => {
  reachBottomStatus.value = hasMore ? 'waiting' : 'no-more'
}

// 小程序端事件处理
const handleScrollToUpper = (e) => {
  emit('scroll-to-upper', e)
}

const handleScrollToLower = (e) => {
  if (props.enableReachBottom && reachBottomStatus.value === 'waiting') {
    reachBottomStatus.value = 'loading'
    emit('reach-bottom', e)
  }
}

const handleRefresherPulling = (e) => {
  pullDownTriggered.value = true
  emit('pull-down-refresh', e)
}

const handleRefresherRefresh = (e) => {
  // 下拉刷新触发
}

const handleRefresherRestore = (e) => {
  pullDownTriggered.value = false
}

const handleRefresherAbort = (e) => {
  pullDownTriggered.value = false
}

// 生命周期
onMounted(() => {
  if (isH5 && props.enablePullDownRefresh) {
    const container = scrollContainer.value
    container.addEventListener('touchstart', handleTouchStart)
    container.addEventListener('touchmove', handleTouchMove)
    container.addEventListener('touchend', handleTouchEnd)
  }
})

onUnmounted(() => {
  if (isH5 && props.enablePullDownRefresh) {
    const container = scrollContainer.value
    container.removeEventListener('touchstart', handleTouchStart)
    container.removeEventListener('touchmove', handleTouchMove)
    container.removeEventListener('touchend', handleTouchEnd)
  }
})

// 暴露方法给父组件
defineExpose({
  completePullDown,
  completeReachBottom,
  scrollTo: (options) => {
    if (isH5) {
      scrollContainer.value.scrollTo(options)
    }
  },
  scrollIntoView: (selector) => {
    if (isH5) {
      const element = scrollContainer.value.querySelector(selector)
      if (element) {
        element.scrollIntoView({ behavior: 'smooth' })
      }
    }
  }
})
</script>

<style lang="scss" scoped>
.scroll-container {
  position: relative;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  
  &.scroll-y {
    overflow-y: auto;
    overflow-x: hidden;
  }
  
  &.scroll-x {
    overflow-x: auto;
    overflow-y: hidden;
  }
}

.pull-down-refresh {
  position: absolute;
  top: -50px;
  left: 0;
  right: 0;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.3s ease;
}

.reach-bottom {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20rpx;
  color: #666;
  font-size: 28rpx;
}

// 小程序端样式
scroll-view {
  &.scroll-y {
    overflow-y: scroll;
  }
  
  &.scroll-x {
    overflow-x: scroll;
  }
}
</style>

第七章:TypeScript 深度集成

7.1 类型定义与配置

// types/uni-app.d.ts - uni-app 类型扩展
import '@dcloudio/types'

// 扩展 uni 命名空间
declare namespace UniApp {
  interface RequestOptions {
    timeout?: number
    dataType?: string
    responseType?: string
    sslVerify?: boolean
    withCredentials?: boolean
    firstIpv4?: boolean
  }
  
  interface RequestSuccessCallbackResult {
    cookies?: string[]
    profile?: any
  }
  
  interface GetSystemInfoResult {
    safeArea?: {
      top: number
      bottom: number
      left: number
      right: number
      width: number
      height: number
    }
    safeAreaInsets?: {
      top: number
      bottom: number
      left: number
      right: number
    }
  }
}

// Vue 组件类型扩展
declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    // uni-app 内置组件
    UniButton: typeof import('@dcloudio/uni-ui')['UniButton']
    UniIcons: typeof import('@dcloudio/uni-ui')['UniIcons']
    UniBadge: typeof import('@dcloudio/uni-ui')['UniBadge']
    UniCard: typeof import('@dcloudio/uni-ui')['UniCard']
    UniCollapse: typeof import('@dcloudio/uni-ui')['UniCollapse']
    UniCollapseItem: typeof import('@dcloudio/uni-ui')['UniCollapseItem']
    UniCombox: typeof import('@dcloudio/uni-ui')['UniCombox']
    UniCountdown: typeof import('@dcloudio/uni-ui')['UniCountdown']
    UniDataChecklist: typeof import('@dcloudio/uni-ui')['UniDataChecklist']
    UniDataPicker: typeof import('@dcloudio/uni-ui')['UniDataPicker']
    UniDateformat: typeof import('@dcloudio/uni-ui')['UniDateformat']
    UniDrawer: typeof import('@dcloudio/uni-ui')['UniDrawer']
    UniFab: typeof import('@dcloudio/uni-ui')['UniFab']
    UniFav: typeof import('@dcloudio/uni-ui')['UniFav']
    UniFilePicker: typeof import('@dcloudio/uni-ui')['UniFilePicker']
    UniForms: typeof import('@dcloudio/uni-ui')['UniForms']
    UniFormsItem: typeof import('@dcloudio/uni-ui')['UniFormsItem']
    UniGoodsNav: typeof import('@dcloudio/uni-ui')['UniGoodsNav']
    UniGrid: typeof import('@dcloudio/uni-ui')['UniGrid']
    UniGridItem: typeof import('@dcloudio/uni-ui')['UniGridItem']
    UniGroup: typeof import('@dcloudio/uni-ui')['UniGroup']
    UniIcons: typeof import('@dcloudio/uni-ui')['UniIcons']
    UniIndexedList: typeof import('@dcloudio/uni-ui')['UniIndexedList']
    UniLink: typeof import('@dcloudio/uni-ui')['UniLink']
    UniList: typeof import('@dcloudio/uni-ui')['UniList']
    UniListItem: typeof import('@dcloudio/uni-ui')['UniListItem']
    UniLoadMore: typeof import('@dcloudio/uni-ui')['UniLoadMore']
    UniNavBar: typeof import('@dcloudio/uni-ui')['UniNavBar']
    UniNoticeBar: typeof import('@dcloudio/uni-ui')['UniNoticeBar']
    UniNumberBox: typeof import('@dcloudio/uni-ui')['UniNumberBox']
    UniPagination: typeof import('@dcloudio/uni-ui')['UniPagination']
    UniPopup: typeof import('@dcloudio/uni-ui')['UniPopup']
    UniRate: typeof import('@dcloudio/uni-ui')['UniRate']
    UniSearchBar: typeof import('@dcloudio/uni-ui')['UniSearchBar']
    UniSegmentedControl: typeof import('@dcloudio/uni-ui')['UniSegmentedControl']
    UniSteps: typeof import('@dcloudio/uni-ui')['UniSteps']
    UniSwiperDot: typeof import('@dcloudio/uni-ui')['UniSwiperDot']
    UniSwipeAction: typeof import('@dcloudio/uni-ui')['UniSwipeAction']
    UniTable: typeof import('@dcloudio/uni-ui')['UniTable']
    UniTag: typeof import('@dcloudio/uni-ui')['UniTag']
    UniTitle: typeof import('@dcloudio/uni-ui')['UniTitle']
    UniTooltip: typeof import('@dcloudio/uni-ui')['UniTooltip']
    UniTransition: typeof import('@dcloudio/uni-ui')['UniTransition']
  }
  
  export interface ComponentCustomProperties {
    $uni: typeof uni
    $platform: string
  }
}

// 扩展 Window 对象
interface Window {
  __wxjs_environment?: 'miniprogram' | 'browser'
  uni?: any
}

export {}

// types/api.d.ts - API 类型定义
export interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
  success: boolean
}

export interface PaginationParams {
  page?: number
  pageSize?: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}

export interface PaginationResult<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
}

// types/user.d.ts - 用户相关类型
export interface UserInfo {
  id: string | number
  username: string
  nickname?: string
  avatar?: string
  email?: string
  phone?: string
  gender?: 'male' | 'female' | 'unknown'
  birthday?: string
  role: UserRole
  permissions: string[]
  status: 'active' | 'inactive' | 'banned'
  createdAt: string
  updatedAt: string
}

export type UserRole = 'admin' | 'user' | 'guest' | 'vip'

export interface LoginParams {
  username: string
  password: string
  rememberMe?: boolean
}

export interface RegisterParams {
  username: string
  password: string
  email?: string
  phone?: string
  nickname?: string
}

// types/components.d.ts - 组件 Props 类型
import type { PropType } from 'vue'

export type ButtonType = 'default' | 'primary' | 'success' | 'warning' | 'error'
export type ButtonSize = 'default' | 'mini' | 'small' | 'medium' | 'large'
export type ButtonShape = 'square' | 'circle' | 'round'

export interface ButtonProps {
  type?: ButtonType
  size?: ButtonSize
  shape?: ButtonShape
  plain?: boolean
  disabled?: boolean
  loading?: boolean
  icon?: string
  iconPosition?: 'left' | 'right'
}

export type InputType = 'text' | 'number' | 'idcard' | 'digit' | 'password'
export type InputMode = 'normal' | 'search'

export interface InputProps {
  type?: InputType
  modelValue?: string | number
  placeholder?: string
  disabled?: boolean
  maxlength?: number
  confirmType?: 'done' | 'go' | 'next' | 'search' | 'send'
  autoFocus?: boolean
  focus?: boolean
  password?: boolean
}

// types/store.d.ts - 状态管理类型
import type { StoreDefinition } from 'pinia'

export interface UserState {
  token: string
  userInfo: UserInfo | null
  permissions: string[]
  loginLoading: boolean
}

export interface AppState {
  theme: 'light' | 'dark'
  language: string
  systemInfo: UniApp.GetSystemInfoResult | null
  networkType: UniApp.NetworkType | null
  appVersion: string
  showTabBar: boolean
  currentRoute: string
}

export interface CartItem {
  id: string
  productId: string
  name: string
  price: number
  quantity: number
  image: string
  sku?: string
}

export interface CartState {
  items: CartItem[]
  total: number
  selectedItems: string[]
}

7.2 类型安全的组件开发

<!-- components/TypedButton.vue -->
<template>
  <button
    :class="buttonClasses"
    :style="buttonStyles"
    :disabled="disabled || loading"
    :loading="loading"
    @click="handleClick"
    @touchstart="handleTouchStart"
    @touchend="handleTouchEnd"
  >
    <!-- 加载状态 -->
    <view v-if="loading" class="button-loading">
      <uni-icons type="spinner-cycle" size="16" color="currentColor" animated />
      <text v-if="loadingText" class="loading-text">{{ loadingText }}</text>
    </view>
    
    <!-- 图标 -->
    <view v-else-if="icon" class="button-icon" :class="iconPositionClass">
      <uni-icons :type="icon" size="16" color="currentColor" />
    </view>
    
    <!-- 文本内容 -->
    <text 
      v-if="$slots.default || text" 
      class="button-text"
      :class="{ 'button-text--with-icon': icon }"
    >
      <slot>{{ text }}</slot>
    </text>
  </button>
</template>

<script setup lang="ts">
import { computed, withDefaults } from 'vue'
import type { ButtonType, ButtonSize, ButtonShape, ButtonProps } from '@/types/components'

interface Emits {
  (e: 'click', event: MouseEvent | TouchEvent): void
  (e: 'touchstart', event: TouchEvent): void
  (e: 'touchend', event: TouchEvent): void
}

const props = withDefaults(defineProps<{
  // 基础属性
  type?: ButtonType
  size?: ButtonSize
  shape?: ButtonShape
  text?: string
  disabled?: boolean
  loading?: boolean
  loadingText?: string
  
  // 图标
  icon?: string
  iconPosition?: 'left' | 'right'
  
  // 样式
  plain?: boolean
  block?: boolean
  round?: boolean
  customClass?: string
  customStyle?: Record<string, string | number>
  
  // 事件
  throttleDelay?: number
}>(), {
  type: 'default',
  size: 'medium',
  shape: 'square',
  iconPosition: 'left',
  plain: false,
  block: false,
  round: false,
  throttleDelay: 300
})

const emit = defineEmits<Emits>()

// 计算属性
const buttonClasses = computed(() => {
  const classes = [
    'typed-button',
    `typed-button--${props.type}`,
    `typed-button--${props.size}`,
    `typed-button--${props.shape}`,
    props.customClass
  ]
  
  if (props.plain) classes.push('typed-button--plain')
  if (props.block) classes.push('typed-button--block')
  if (props.round) classes.push('typed-button--round')
  if (props.disabled) classes.push('typed-button--disabled')
  if (props.loading) classes.push('typed-button--loading')
  
  return classes.filter(Boolean).join(' ')
})

const buttonStyles = computed(() => {
  return props.customStyle || {}
})

const iconPositionClass = computed(() => {
  return `button-icon--${props.iconPosition}`
})

// 节流处理
let lastClickTime = 0

const handleClick = (event: MouseEvent | TouchEvent) => {
  const now = Date.now()
  
  // 节流处理
  if (now - lastClickTime < props.throttleDelay) {
    return
  }
  
  lastClickTime = now
  
  // 防止重复点击
  if (props.disabled || props.loading) {
    return
  }
  
  emit('click', event)
}

const handleTouchStart = (event: TouchEvent) => {
  emit('touchstart', event)
}

const handleTouchEnd = (event: TouchEvent) => {
  emit('touchend', event)
}

// 暴露方法给父组件
defineExpose({
  focus: () => {
    // 聚焦处理
  },
  blur: () => {
    // 失焦处理
  }
})
</script>

<style lang="scss" scoped>
.typed-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  outline: none;
  cursor: pointer;
  transition: all 0.3s ease;
  font-family: inherit;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  
  // 类型样式
  &--default {
    background-color: #f5f5f5;
    color: #333;
    
    &:not(.typed-button--disabled):active {
      background-color: #e8e8e8;
    }
  }
  
  &--primary {
    background-color: #007aff;
    color: white;
    
    &:not(.typed-button--disabled):active {
      background-color: #0056cc;
    }
  }
  
  &--success {
    background-color: #4cd964;
    color: white;
    
    &:not(.typed-button--disabled):active {
      background-color: #2ac845;
    }
  }
  
  &--warning {
    background-color: #f0ad4e;
    color: white;
    
    &:not(.typed-button--disabled):active {
      background-color: #ec971f;
    }
  }
  
  &--error {
    background-color: #dd524d;
    color: white;
    
    &:not(.typed-button--disabled):active {
      background-color: #d3322c;
    }
  }
  
  // 尺寸样式
  &--mini {
    padding: 4px 8px;
    font-size: 12px;
    height: 24px;
    
    .button-icon {
      margin: 0 4px;
    }
  }
  
  &--small {
    padding: 6px 12px;
    font-size: 14px;
    height: 32px;
    
    .button-icon {
      margin: 0 6px;
    }
  }
  
  &--medium {
    padding: 8px 16px;
    font-size: 16px;
    height: 40px;
    
    .button-icon {
      margin: 0 8px;
    }
  }
  
  &--large {
    padding: 12px 24px;
    font-size: 18px;
    height: 48px;
    
    .button-icon {
      margin: 0 12px;
    }
  }
  
  // 形状样式
  &--square {
    border-radius: 4px;
  }
  
  &--round {
    border-radius: 20px;
  }
  
  &--circle {
    border-radius: 50%;
    width: 40px;
    height: 40px;
    padding: 0;
    
    &.typed-button--mini {
      width: 24px;
      height: 24px;
    }
    
    &.typed-button--small {
      width: 32px;
      height: 32px;
    }
    
    &.typed-button--large {
      width: 48px;
      height: 48px;
    }
  }
  
  // 变体样式
  &--plain {
    background-color: transparent;
    border: 1px solid currentColor;
    
    &.typed-button--default {
      color: #666;
      border-color: #ddd;
    }
    
    &.typed-button--primary {
      color: #007aff;
      border-color: #007aff;
    }
    
    &.typed-button--success {
      color: #4cd964;
      border-color: #4cd964;
    }
    
    &.typed-button--warning {
      color: #f0ad4e;
      border-color: #f0ad4e;
    }
    
    &.typed-button--error {
      color: #dd524d;
      border-color: #dd524d;
    }
  }
  
  &--block {
    display: flex;
    width: 100%;
  }
  
  // 状态样式
  &--disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  
  &--loading {
    cursor: wait;
  }
}

.button-loading {
  display: flex;
  align-items: center;
  
  .loading-text {
    margin-left: 8px;
  }
}

.button-icon {
  display: flex;
  align-items: center;
  
  &--left {
    order: 1;
  }
  
  &--right {
    order: 3;
  }
}

.button-text {
  order: 2;
  
  &--with-icon {
    &.button-icon--left {
      margin-left: 8px;
    }
    
    &.button-icon--right {
      margin-right: 8px;
    }
  }
}
</style>

7.3 类型安全的 API 调用

// utils/request.ts - 类型安全的请求封装
import type { ApiResponse, PaginationParams, PaginationResult } from '@/types/api'

interface RequestConfig extends UniApp.RequestOptions {
  params?: Record<string, any>
  transformRequest?: (data: any) => any
  transformResponse?: (data: any) => any
  validateStatus?: (status: number) => boolean
}

class RequestError extends Error {
  constructor(
    public code: number,
    public message: string,
    public data?: any
  ) {
    super(message)
    this.name = 'RequestError'
  }
}

export class HttpClient {
  private baseURL: string
  private defaultConfig: RequestConfig
  
  constructor(baseURL = '', config: Partial<RequestConfig> = {}) {
    this.baseURL = baseURL
    this.defaultConfig = {
      method: 'GET',
      header: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      },
      timeout: 10000,
      dataType: 'json',
      responseType: 'text',
      validateStatus: (status) => status >= 200 && status < 300,
      ...config
    }
  }
  
  // 请求拦截器
  private requestInterceptors: ((config: RequestConfig) => RequestConfig | Promise<RequestConfig>)[] = []
  private responseInterceptors: ((response: any) => any | Promise<any>)[] = []
  private errorInterceptors: ((error: any) => any | Promise<any>)[] = []
  
  useRequestInterceptor(
    interceptor: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>
  ) {
    this.requestInterceptors.push(interceptor)
  }
  
  useResponseInterceptor(
    interceptor: (response: any) => any | Promise<any>
  ) {
    this.responseInterceptors.push(interceptor)
  }
  
  useErrorInterceptor(
    interceptor: (error: any) => any | Promise<any>
  ) {
    this.errorInterceptors.push(interceptor)
  }
  
  private async executeInterceptors(
    interceptors: ((value: any) => any | Promise<any>)[],
    initialValue: any
  ): Promise<any> {
    let value = initialValue
    
    for (const interceptor of interceptors) {
      value = await interceptor(value)
    }
    
    return value
  }
  
  // 核心请求方法
  async request<T = any>(url: string, config: Partial<RequestConfig> = {}): Promise<T> {
    try {
      // 合并配置
      const mergedConfig = {
        ...this.defaultConfig,
        ...config,
        header: {
          ...this.defaultConfig.header,
          ...config.header
        }
      }
      
      // 处理请求参数
      if (mergedConfig.params) {
        const params = new URLSearchParams()
        Object.entries(mergedConfig.params).forEach(([key, value]) => {
          if (value !== null && value !== undefined) {
            params.append(key, String(value))
          }
        })
        const queryString = params.toString()
        if (queryString) {
          url += (url.includes('?') ? '&' : '?') + queryString
        }
        delete mergedConfig.params
      }
      
      // 请求拦截器
      const finalConfig = await this.executeInterceptors(
        this.requestInterceptors,
        { ...mergedConfig, url: this.baseURL + url }
      )
      
      // 发起请求
      const response = await new Promise<UniApp.RequestSuccessCallbackResult>((resolve, reject) => {
        uni.request({
          ...finalConfig,
          success: resolve,
          fail: reject
        })
      })
      
      // 验证状态码
      if (finalConfig.validateStatus && !finalConfig.validateStatus(response.statusCode)) {
        throw new RequestError(
          response.statusCode,
          `Request failed with status code ${response.statusCode}`,
          response.data
        )
      }
      
      // 响应拦截器
      let processedResponse = response.data
      processedResponse = await this.executeInterceptors(
        this.responseInterceptors,
        processedResponse
      )
      
      // 响应转换
      if (finalConfig.transformResponse) {
        processedResponse = finalConfig.transformResponse(processedResponse)
      }
      
      return processedResponse as T
    } catch (error) {
      // 错误拦截器
      let processedError = error
      processedError = await this.executeInterceptors(
        this.errorInterceptors,
        processedError
      )
      
      throw processedError
    }
  }
  
  // 快捷方法
  get<T = any>(url: string, config?: Partial<RequestConfig>) {
    return this.request<T>(url, { ...config, method: 'GET' })
  }
  
  post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>) {
    return this.request<T>(url, { ...config, method: 'POST', data })
  }
  
  put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>) {
    return this.request<T>(url, { ...config, method: 'PUT', data })
  }
  
  delete<T = any>(url: string, config?: Partial<RequestConfig>) {
    return this.request<T>(url, { ...config, method: 'DELETE' })
  }
  
  patch<T = any>(url: string, data?: any, config?: Partial<RequestConfig>) {
    return this.request<T>(url, { ...config, method: 'PATCH', data })
  }
}

// API 服务类示例
export class UserService {
  constructor(private http: HttpClient) {}
  
  // 用户登录
  async login(params: { username: string; password: string }) {
    return this.http.post<ApiResponse<{
      token: string
      user: UserInfo
      permissions: string[]
    }>>('/api/auth/login', params)
  }
  
  // 获取用户信息
  async getUserInfo(userId: string | number) {
    return this.http.get<ApiResponse<UserInfo>>(`/api/users/${userId}`)
  }
  
  // 更新用户信息
  async updateUserInfo(userId: string | number, data: Partial<UserInfo>) {
    return this.http.put<ApiResponse<UserInfo>>(`/api/users/${userId}`, data)
  }
  
  // 获取用户列表(分页)
  async getUserList(params: PaginationParams & {
    keyword?: string
    role?: UserRole
    status?: UserInfo['status']
  }) {
    return this.http.get<ApiResponse<PaginationResult<UserInfo>>>('/api/users', { params })
  }
  
  // 上传用户头像
  async uploadAvatar(userId: string | number, filePath: string) {
    return new Promise<ApiResponse<{ avatar: string }>>((resolve, reject) => {
      uni.uploadFile({
        url: `${this.http['baseURL']}/api/users/${userId}/avatar`,
        filePath,
        name: 'avatar',
        success: (res) => {
          try {
            const data = JSON.parse(res.data)
            resolve(data)
          } catch (error) {
            reject(new Error('Failed to parse response'))
          }
        },
        fail: reject
      })
    })
  }
}

// 全局请求实例配置
const httpClient = new HttpClient(import.meta.env.VITE_API_BASE_URL)

// 请求拦截器:添加 token
httpClient.useRequestInterceptor(async (config) => {
  const token = uni.getStorageSync('token')
  if (token) {
    config.header = {
      ...config.header,
      Authorization: `Bearer ${token}`
    }
  }
  return config
})

// 响应拦截器:处理通用响应格式
httpClient.useResponseInterceptor((response) => {
  if (response && typeof response === 'object' && 'code' in response) {
    const apiResponse = response as ApiResponse
    
    if (apiResponse.code === 200) {
      return apiResponse.data
    } else {
      throw new RequestError(apiResponse.code, apiResponse.message, apiResponse.data)
    }
  }
  return response
})

// 错误拦截器:统一错误处理
httpClient.useErrorInterceptor((error) => {
  console.error('Request error:', error)
  
  // 处理网络错误
  if (error.errMsg && error.errMsg.includes('network')) {
    uni.showToast({
      title: '网络连接失败',
      icon: 'none'
    })
  }
  
  // 处理认证错误
  if (error.code === 401) {
    uni.removeStorageSync('token')
    uni.showToast({
      title: '登录已过期,请重新登录',
      icon: 'none'
    })
    
    setTimeout(() => {
      uni.reLaunch({
        url: '/pages/login/login'
      })
    }, 1500)
  }
  
  // 处理业务错误
  if (error.code && error.code !== 200) {
    uni.showToast({
      title: error.message || '请求失败',
      icon: 'none'
    })
  }
  
  throw error
})

// 导出服务实例
export const userService = new UserService(httpClient)
export { httpClient }

第八章:构建与部署优化

8.1 多环境配置

// 环境配置管理
// config/index.js
const configs = {
  development: {
    baseURL: 'http://localhost:3000/api',
    appId: 'dev_app_id',
    debug: true,
    console: true
  },
  
  test: {
    baseURL: 'https://test.example.com/api',
    appId: 'test_app_id',
    debug: true,
    console: false
  },
  
  staging: {
    baseURL: 'https://staging.example.com/api',
    appId: 'staging_app_id',
    debug: false,
    console: false
  },
  
  production: {
    baseURL: 'https://api.example.com/api',
    appId: 'prod_app_id',
    debug: false,
    console: false
  }
}

// 获取当前环境
const getEnv = () => {
  // 优先使用命令行参数
  if (process.env.UNI_APP_ENV) {
    return process.env.UNI_APP_ENV
  }
  
  // 次优先使用编译模式
  if (process.env.NODE_ENV === 'production') {
    return 'production'
  }
  
  // 默认开发环境
  return 'development'
}

// 获取配置
export const getConfig = () => {
  const env = getEnv()
  return configs[env] || configs.development
}

// 环境变量注入
export const env = {
  // 基础信息
  MODE: getEnv(),
  BASE_URL: getConfig().baseURL,
  APP_ID: getConfig().appId,
  
  // 功能开关
  DEBUG: getConfig().debug,
  CONSOLE_LOG: getConfig().console,
  
  // 平台信息
  PLATFORM: process.env.UNI_PLATFORM,
  SUB_PLATFORM: process.env.UNI_SUB_PLATFORM,
  
  // 版本信息
  VERSION: process.env.UNI_APP_VERSION,
  VERSION_CODE: process.env.UNI_APP_VERSION_CODE,
  
  // 编译信息
  COMPILE_TIME: new Date().toISOString(),
  GIT_COMMIT: process.env.GIT_COMMIT || 'unknown'
}

// vite.config.js - 环境变量注入
import { defineConfig, loadEnv } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig(({ mode }) => {
  // 加载环境变量
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    plugins: [uni()],
    
    define: {
      // 注入环境变量
      'import.meta.env': {
        ...env,
        MODE: mode,
        DEV: mode === 'development',
        PROD: mode === 'production'
      }
    },
    
    // 环境特定的构建配置
    build: {
      sourcemap: mode === 'development',
      minify: mode === 'production' ? 'terser' : false,
      
      rollupOptions: {
        output: {
          // 开发环境不添加 hash,方便调试
          entryFileNames: mode === 'development' 
            ? 'assets/js/[name].js' 
            : 'assets/js/[name]-[hash].js',
          
          chunkFileNames: mode === 'development'
            ? 'assets/js/[name].js'
            : 'assets/js/[name]-[hash].js',
          
          assetFileNames: mode === 'development'
            ? 'assets/[ext]/[name].[ext]'
            : 'assets/[ext]/[name]-[hash].[ext]'
        }
      }
    }
  }
})

8.2 自动化构建脚本

// package.json - 构建脚本配置
{
  "name": "my-vue3-uniapp",
  "version": "1.0.0",
  "scripts": {
    // 开发命令
    "dev:h5": "cross-env UNI_PLATFORM=h5 UNI_APP_ENV=development uni -p h5",
    "dev:weapp": "cross-env UNI_PLATFORM=mp-weixin UNI_APP_ENV=development uni -p mp-weixin",
    "dev:app": "cross-env UNI_PLATFORM=app UNI_APP_ENV=development uni -p app",
    
    // 构建命令
    "build:h5": "cross-env UNI_PLATFORM=h5 UNI_APP_ENV=production uni build -p h5",
    "build:weapp": "cross-env UNI_PLATFORM=mp-weixin UNI_APP_ENV=production uni build -p mp-weixin",
    "build:app": "cross-env UNI_PLATFORM=app UNI_APP_ENV=production uni build -p app",
    
    // 多平台构建
    "build:all": "npm run build:h5 && npm run build:weapp && npm run build:app",
    
    // 测试环境构建
    "build:test:h5": "cross-env UNI_PLATFORM=h5 UNI_APP_ENV=test uni build -p h5",
    "build:test:weapp": "cross-env UNI_PLATFORM=mp-weixin UNI_APP_ENV=test uni build -p mp-weixin",
    
    // 预览命令
    "preview:h5": "serve dist/build/h5",
    "preview:weapp": "open dist/build/mp-weixin",
    
    // 代码检查
    "lint": "eslint src --ext .vue,.js,.ts",
    "lint:fix": "eslint src --ext .vue,.js,.ts --fix",
    "type-check": "vue-tsc --noEmit",
    
    // 单元测试
    "test:unit": "vitest",
    "test:unit:coverage": "vitest --coverage",
    
    // E2E 测试
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    
    // 自动化部署
    "deploy:h5": "npm run build:h5 && node scripts/deploy-h5.js",
    "deploy:weapp": "npm run build:weapp && node scripts/deploy-weapp.js",
    
    // 代码分析
    "analyze": "cross-env ANALYZE=true npm run build:h5",
    "size": "size-limit",
    
    // 版本管理
    "version:patch": "npm version patch && git push && git push --tags",
    "version:minor": "npm version minor && git push && git push --tags",
    "version:major": "npm version major && git push && git push --tags",
    
    // 文档生成
    "docs": "vue-docgen-web-types --outDir docs",
    "docs:build": "vitepress build docs"
  },
  "devDependencies": {
    "@dcloudio/uni-automator": "^3.0.0",
    "@dcloudio/uni-cli-shared": "^3.0.0",
    "@dcloudio/vite-plugin-uni": "^3.0.0",
    "@size-limit/preset-app": "^8.0.0",
    "@types/node": "^18.0.0",
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/eslint-config-typescript": "^11.0.0",
    "@vue/tsconfig": "^0.4.0",
    "cross-env": "^7.0.0",
    "eslint": "^8.0.0",
    "playwright": "^1.35.0",
    "rollup-plugin-visualizer": "^5.9.0",
    "serve": "^14.0.0",
    "size-limit": "^8.0.0",
    "typescript": "^5.0.0",
    "vite": "^4.0.0",
    "vitepress": "^1.0.0",
    "vitest": "^0.32.0",
    "vue-tsc": "^1.6.0"
  }
}
// scripts/deploy-h5.js - H5 部署脚本
#!/usr/bin/env node

const fs = require('fs-extra')
const path = require('path')
const { execSync } = require('child_process')
const chalk = require('chalk')
const inquirer = require('inquirer')

class DeployH5 {
  constructor() {
    this.distDir = path.join(process.cwd(), 'dist/build/h5')
    this.config = this.loadConfig()
  }
  
  loadConfig() {
    const configPath = path.join(process.cwd(), 'deploy.config.js')
    
    if (fs.existsSync(configPath)) {
      return require(configPath)
    }
    
    return {
      outputDir: this.distDir,
      upload: {
        type: 'oss', // oss, cos, qiniu, ftp, ssh
        config: {}
      },
      cdn: {
        enable: false,
        domain: ''
      },
      backup: {
        enable: true,
        maxCount: 5
      }
    }
  }
  
  async deploy() {
    console.log(chalk.blue('🚀 开始部署 H5 应用...'))
    
    try {
      // 1. 检查构建目录
      await this.checkDistDir()
      
      // 2. 备份旧版本
      if (this.config.backup.enable) {
        await this.backupOldVersion()
      }
      
      // 3. 上传到服务器
      await this.uploadFiles()
      
      // 4. 刷新 CDN
      if (this.config.cdn.enable) {
        await this.refreshCDN()
      }
      
      // 5. 发送通知
      await this.sendNotification()
      
      console.log(chalk.green('✅ 部署成功!'))
    } catch (error) {
      console.error(chalk.red('❌ 部署失败:'), error.message)
      process.exit(1)
    }
  }
  
  async checkDistDir() {
    console.log(chalk.cyan('📁 检查构建目录...'))
    
    if (!fs.existsSync(this.distDir)) {
      throw new Error(`构建目录不存在:${this.distDir}`)
    }
    
    const files = fs.readdirSync(this.distDir)
    if (files.length === 0) {
      throw new Error('构建目录为空')
    }
    
    // 检查必要文件
    const requiredFiles = ['index.html', 'assets']
    requiredFiles.forEach(file => {
      const filePath = path.join(this.distDir, file)
      if (!fs.existsSync(filePath)) {
        throw new Error(`必要文件缺失:${file}`)
      }
    })
    
    console.log(chalk.green('✅ 构建目录检查通过'))
  }
  
  async backupOldVersion() {
    console.log(chalk.cyan('💾 备份旧版本...'))
    
    const backupDir = path.join(process.cwd(), 'backups')
    if (!fs.existsSync(backupDir)) {
      fs.mkdirpSync(backupDir)
    }
    
    // 获取当前版本
    const packageJson = require(path.join(process.cwd(), 'package.json'))
    const version = packageJson.version
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
    
    const backupName = `v${version}-${timestamp}`
    const backupPath = path.join(backupDir, backupName)
    
    // 复制文件
    fs.copySync(this.distDir, backupPath)
    
    // 清理旧备份
    this.cleanOldBackups(backupDir)
    
    console.log(chalk.green(`✅ 备份完成:${backupName}`))
  }
  
  cleanOldBackups(backupDir) {
    const backups = fs.readdirSync(backupDir)
      .filter(name => name.startsWith('v'))
      .sort()
      .reverse()
    
    if (backups.length > this.config.backup.maxCount) {
      const toDelete = backups.slice(this.config.backup.maxCount)
      toDelete.forEach(name => {
        const backupPath = path.join(backupDir, name)
        fs.removeSync(backupPath)
        console.log(chalk.yellow(`🗑️  删除旧备份:${name}`))
      })
    }
  }
  
  async uploadFiles() {
    console.log(chalk.cyan('☁️  上传文件到服务器...'))
    
    const { upload } = this.config
    
    switch (upload.type) {
      case 'oss':
        await this.uploadToOSS(upload.config)
        break
      case 'cos':
        await this.uploadToCOS(upload.config)
        break
      case 'qiniu':
        await this.uploadToQiniu(upload.config)
        break
      case 'ftp':
        await this.uploadToFTP(upload.config)
        break
      case 'ssh':
        await this.uploadToSSH(upload.config)
        break
      default:
        throw new Error(`不支持的上传类型:${upload.type}`)
    }
    
    console.log(chalk.green('✅ 文件上传完成'))
  }
  
  async uploadToOSS(config) {
    const OSS = require('ali-oss')
    const client = new OSS(config)
    
    const files = this.getAllFiles(this.distDir)
    
    for (const file of files) {
      const relativePath = path.relative(this.distDir, file)
      const objectName = relativePath.replace(/\\/g, '/')
      
      console.log(chalk.gray(`  上传:${objectName}`))
      
      await client.put(objectName, file)
    }
  }
  
  getAllFiles(dir) {
    let results = []
    const list = fs.readdirSync(dir)
    
    list.forEach(file => {
      const filePath = path.join(dir, file)
      const stat = fs.statSync(filePath)
      
      if (stat && stat.isDirectory()) {
        results = results.concat(this.getAllFiles(filePath))
      } else {
        results.push(filePath)
      }
    })
    
    return results
  }
  
  async refreshCDN() {
    console.log(chalk.cyan('🔄 刷新 CDN 缓存...'))
    
    const { cdn } = this.config
    
    if (cdn.type === 'aliyun') {
      await this.refreshAliyunCDN(cdn.config)
    }
    
    console.log(chalk.green('✅ CDN 刷新完成'))
  }
  
  async sendNotification() {
    console.log(chalk.cyan('📢 发送部署通知...'))
    
    // 发送到钉钉/企业微信/飞书等
    const packageJson = require(path.join(process.cwd(), 'package.json'))
    const message = {
      title: '部署通知',
      content: `应用 ${packageJson.name} v${packageJson.version} 部署成功`,
      time: new Date().toLocaleString(),
      platform: 'H5'
    }
    
    // 实际项目中集成通知服务
    console.log(chalk.green('✅ 通知发送完成'))
  }
  
  async run() {
    const answers = await inquirer.prompt([
      {
        type: 'confirm',
        name: 'confirm',
        message: '确认部署到生产环境?',
        default: false
      },
      {
        type: 'list',
        name: 'environment',
        message: '选择部署环境:',
        choices: ['production', 'staging', 'test'],
        when: (answers) => answers.confirm
      }
    ])
    
    if (!answers.confirm) {
      console.log(chalk.yellow('⚠️  部署已取消'))
      return
    }
    
    // 设置环境变量
    process.env.UNI_APP_ENV = answers.environment
    
    await this.deploy()
  }
}

// 执行部署
const deployer = new DeployH5()
deployer.run()

8.3 监控与错误追踪

// utils/monitor.js - 应用监控系统
class ApplicationMonitor {
  constructor() {
    this.config = {
      appId: '__UNI__XXXXXX',
      version: '1.0.0',
      endpoint: 'https://monitor.example.com/api',
      sampleRate: 1.0, // 采样率
      maxBatchSize: 20,
      flushInterval: 10000 // 10秒
    }
    
    this.queue = []
    this.timer = null
    this.initialized = false
  }
  
  init(config = {}) {
    Object.assign(this.config, config)
    
    // 监听错误
    this.setupErrorHandlers()
    
    // 监听性能
    this.setupPerformanceObserver()
    
    // 监听用户行为
    this.setupUserBehaviorTracking()
    
    // 启动定时上报
    this.startFlushTimer()
    
    this.initialized = true
    console.log('ApplicationMonitor initialized')
  }
  
  setupErrorHandlers() {
    // Vue 错误处理器
    const vueApp = getApp()
    if (vueApp && vueApp.config) {
      vueApp.config.errorHandler = (err, vm, info) => {
        this.captureError(err, {
          type: 'vue',
          component: vm?.$options?.name,
          lifecycle: info
        })
      }
    }
    
    // Promise 错误
    window.addEventListener('unhandledrejection', (event) => {
      this.captureError(event.reason, {
        type: 'promise',
        unhandled: true
      })
    })
    
    // 资源加载错误
    window.addEventListener('error', (event) => {
      if (event.target && (event.target.src || event.target.href)) {
        this.captureError(new Error('Resource load failed'), {
          type: 'resource',
          resource: event.target.src || event.target.href,
          tag: event.target.tagName
        })
      }
    }, true)
    
    // uni-app API 错误
    const originalRequest = uni.request
    uni.request = (options) => {
      const startTime = Date.now()
      
      return originalRequest({
        ...options,
        success: (res) => {
          const duration = Date.now() - startTime
          
          // 记录慢请求
          if (duration > 3000) {
            this.capturePerformance({
              type: 'slow_request',
              url: options.url,
              method: options.method,
              duration,
              status: res.statusCode
            })
          }
          
          options.success && options.success(res)
        },
        fail: (err) => {
          this.captureError(err, {
            type: 'request',
            url: options.url,
            method: options.method
          })
          
          options.fail && options.fail(err)
        }
      })
    }
  }
  
  setupPerformanceObserver() {
    if (typeof PerformanceObserver !== 'undefined') {
      // 监听长任务
      const longTaskObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.duration > 50) {
            this.capturePerformance({
              type: 'long_task',
              duration: entry.duration,
              startTime: entry.startTime,
              name: entry.name
            })
          }
        })
      })
      
      longTaskObserver.observe({ entryTypes: ['longtask'] })
      
      // 监听资源加载
      const resourceObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.duration > 2000) {
            this.capturePerformance({
              type: 'slow_resource',
              name: entry.name,
              duration: entry.duration,
              initiatorType: entry.initiatorType
            })
          }
        })
      })
      
      resourceObserver.observe({ entryTypes: ['resource'] })
    }
  }
  
  setupUserBehaviorTracking() {
    // 页面访问
    const originalNavigateTo = uni.navigateTo
    uni.navigateTo = (options) => {
      this.trackPageView(options.url)
      return originalNavigateTo(options)
    }
    
    // 按钮点击
    document.addEventListener('click', (event) => {
      const element = event.target
      const trackData = this.extractTrackData(element)
      
      if (trackData) {
        this.trackEvent('click', trackData)
      }
    }, true)
    
    // 表单提交
    document.addEventListener('submit', (event) => {
      this.trackEvent('form_submit', {
        formId: event.target.id,
        formAction: event.target.action
      })
    }, true)
  }
  
  extractTrackData(element) {
    if (!element) return null
    
    // 从 data-track 属性提取数据
    const trackAttr = element.getAttribute('data-track')
    if (trackAttr) {
      try {
        return JSON.parse(trackAttr)
      } catch (error) {
        return { action: trackAttr }
      }
    }
    
    // 从类名或 ID 推断
    const id = element.id
    const className = element.className
    
    if (id || className) {
      return {
        element: id || className.split(' ')[0],
        tag: element.tagName.toLowerCase()
      }
    }
    
    return null
  }
  
  captureError(error, metadata = {}) {
    const errorEvent = {
      type: 'error',
      timestamp: Date.now(),
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      metadata,
      context: this.getContext()
    }
    
    this.addToQueue(errorEvent)
    
    // 开发环境直接打印
    if (process.env.NODE_ENV === 'development') {
      console.error('Captured error:', errorEvent)
    }
  }
  
  capturePerformance(data) {
    const performanceEvent = {
      type: 'performance',
      timestamp: Date.now(),
      data,
      context: this.getContext()
    }
    
    this.addToQueue(performanceEvent)
  }
  
  trackPageView(url) {
    const pageEvent = {
      type: 'pageview',
      timestamp: Date.now(),
      url,
      context: this.getContext()
    }
    
    this.addToQueue(pageEvent)
  }
  
  trackEvent(name, data = {}) {
    const event = {
      type: 'event',
      timestamp: Date.now(),
      name,
      data,
      context: this.getContext()
    }
    
    this.addToQueue(event)
  }
  
  getContext() {
    const pages = getCurrentPages()
    const currentPage = pages[pages.length - 1]
    
    return {
      appId: this.config.appId,
      version: this.config.version,
      platform: process.env.UNI_PLATFORM,
      route: currentPage?.route || '',
      query: currentPage?.options || {},
      network: uni.getNetworkType(),
      system: uni.getSystemInfoSync(),
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    }
  }
  
  addToQueue(event) {
    // 采样控制
    if (Math.random() > this.config.sampleRate) {
      return
    }
    
    this.queue.push(event)
    
    // 达到批量大小立即上报
    if (this.queue.length >= this.config.maxBatchSize) {
      this.flush()
    }
  }
  
  startFlushTimer() {
    if (this.timer) {
      clearInterval(this.timer)
    }
    
    this.timer = setInterval(() => {
      if (this.queue.length > 0) {
        this.flush()
      }
    }, this.config.flushInterval)
  }
  
  async flush() {
    if (this.queue.length === 0) return
    
    const batch = [...this.queue]
    this.queue = []
    
    try {
      await this.sendToServer(batch)
    } catch (error) {
      // 发送失败,重新加入队列(前5条)
      this.queue.unshift(...batch.slice(0, 5))
      
      // 指数退避重试
      setTimeout(() => this.flush(), 5000)
    }
  }
  
  async sendToServer(events) {
    const response = await uni.request({
      url: `${this.config.endpoint}/collect`,
      method: 'POST',
      data: {
        appId: this.config.appId,
        events
      },
      header: {
        'Content-Type': 'application/json'
      }
    })
    
    if (response.statusCode !== 200) {
      throw new Error(`Server responded with ${response.statusCode}`)
    }
    
    return response.data
  }
  
  // 手动上报方法
  logError(error, metadata) {
    this.captureError(error, metadata)
  }
  
  logPerformance(metric, value) {
    this.capturePerformance({
      type: 'custom_performance',
      metric,
      value
    })
  }
  
  setUser(user) {
    this.user = user
  }
  
  setTag(key, value) {
    this.tags = this.tags || {}
    this.tags[key] = value
  }
  
  // 销毁清理
  destroy() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
    
    this.queue = []
    this.initialized = false
  }
}

// 全局监控实例
export const monitor = new ApplicationMonitor()

// 使用示例
monitor.init({
  appId: '__UNI__XXXXXX',
  version: '1.0.0',
  endpoint: 'https://monitor.example.com/api',
  sampleRate: 0.1 // 10%采样率
})

// 在 Vue 应用中使用
export const installMonitor = (app) => {
  app.config.globalProperties.$monitor = monitor
  
  // 提供注入
  app.provide('monitor', monitor)
}

// 错误边界组件
const ErrorBoundary = defineComponent({
  name: 'ErrorBoundary',
  
  props: {
    fallback: {
      type: [String, Object, Function],
      default: null
    },
    onError: {
      type: Function,
      default: null
    }
  },
  
  data() {
    return {
      hasError: false,
      error: null,
      errorInfo: null
    }
  },
  
  errorCaptured(err, vm, info) {
    this.hasError = true
    this.error = err
    this.errorInfo = info
    
    // 上报错误
    monitor.captureError(err, {
      type: 'error_boundary',
      component: vm?.$options?.name,
      lifecycle: info
    })
    
    // 调用自定义错误处理
    if (this.onError) {
      this.onError(err, vm, info)
    }
    
    // 阻止错误继续向上传播
    return false
  },
  
  render() {
    if (this.hasError) {
      if (this.fallback) {
        return typeof this.fallback === 'function' 
          ? this.fallback({ error: this.error, errorInfo: this.errorInfo })
          : this.fallback
      }
      
      return h('div', { class: 'error-boundary' }, [
        h('h3', '出错了'),
        h('p', this.error?.message || '未知错误'),
        h('button', {
          onClick: () => {
            this.hasError = false
            this.error = null
            this.errorInfo = null
          }
        }, '重试')
      ])
    }
    
    return this.$slots.default?.()
  }
})

export { ErrorBoundary }

第九章:实战案例与最佳实践总结

9.1 电商应用完整示例

<!-- pages/product/detail.vue - 商品详情页 -->
<template>
  <view class="product-detail">
    <!-- 自定义导航栏 -->
    <uni-nav-bar 
      title="商品详情"
      left-icon="back"
      :right-text="favorited ? '已收藏' : '收藏'"
      @clickLeft="goBack"
      @clickRight="toggleFavorite"
    />
    
    <!-- 轮播图 -->
    <swiper 
      class="product-swiper" 
      :indicator-dots="true" 
      :autoplay="false"
      circular
    >
      <swiper-item v-for="(image, index) in product.images" :key="index">
        <image 
          :src="image" 
          mode="aspectFit"
          class="product-image"
          @click="previewImage(index)"
        />
      </swiper-item>
    </swiper>
    
    <!-- 商品信息 -->
    <view class="product-info">
      <view class="product-header">
        <view class="product-title">
          <text class="title">{{ product.title }}</text>
          <uni-tag 
            v-if="product.tag" 
            :text="product.tag" 
            type="error" 
            size="small"
          />
        </view>
        
        <view class="product-price">
          <text class="current-price">¥{{ product.price }}</text>
          <text v-if="product.originalPrice" class="original-price">
            ¥{{ product.originalPrice }}
          </text>
          <text class="discount" v-if="product.discount">
            {{ product.discount }}折
          </text>
        </view>
      </view>
      
      <!-- 商品属性 -->
      <view class="product-attributes">
        <view 
          v-for="attr in product.attributes" 
          :key="attr.name"
          class="attribute-item"
        >
          <text class="attribute-name">{{ attr.name }}:</text>
          <text class="attribute-value">{{ attr.value }}</text>
        </view>
      </view>
      
      <!-- 商品描述 -->
      <view class="product-description">
        <rich-text :nodes="product.description"></rich-text>
      </view>
    </view>
    
    <!-- SKU 选择器 -->
    <ProductSkuSelector
      v-if="showSkuSelector"
      :product="product"
      :skus="product.skus"
      v-model="selectedSku"
      @change="handleSkuChange"
    />
    
    <!-- 底部操作栏 -->
    <view class="product-actions">
      <view class="action-buttons">
        <button 
          class="cart-button" 
          @click="addToCart"
          :disabled="!selectedSku"
        >
          <uni-icons type="cart" size="24" color="#fff" />
          <text>加入购物车</text>
        </button>
        
        <button 
          class="buy-button" 
          type="primary" 
          @click="buyNow"
          :disabled="!selectedSku"
        >
          立即购买
        </button>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, onLoad } from 'vue'
import { storeToRefs } from 'pinia'
import { useProductStore } from '@/store/modules/product'
import { useCartStore } from '@/store/modules/cart'
import { useUserStore } from '@/store/modules/user'
import ProductSkuSelector from '@/components/ProductSkuSelector.vue'

// Store
const productStore = useProductStore()
const cartStore = useCartStore()
const userStore = useUserStore()

const { currentProduct: product } = storeToRefs(productStore)
const { isLogin } = storeToRefs(userStore)

// 组件状态
const showSkuSelector = ref(false)
const selectedSku = ref(null)
const favorited = ref(false)
const loading = ref(false)

// 计算属性
const hasStock = computed(() => {
  return selectedSku.value ? selectedSku.value.stock > 0 : false
})

const canAddToCart = computed(() => {
  return isLogin.value && selectedSku.value && hasStock.value
})

// 生命周期
onLoad(async (options) => {
  const productId = options.id
  
  if (productId) {
    loading.value = true
    try {
      await productStore.fetchProductDetail(productId)
      await productStore.checkFavorite(productId)
      favorited.value = productStore.isFavorite
    } catch (error) {
      uni.showToast({
        title: '加载失败',
        icon: 'error'
      })
    } finally {
      loading.value = false
    }
  }
})

// 方法
const goBack = () => {
  uni.navigateBack()
}

const previewImage = (index: number) => {
  uni.previewImage({
    current: index,
    urls: product.value.images
  })
}

const toggleFavorite = async () => {
  if (!isLogin.value) {
    uni.navigateTo({
      url: '/pages/login/login'
    })
    return
  }
  
  try {
    if (favorited.value) {
      await productStore.removeFavorite(product.value.id)
      favorited.value = false
      uni.showToast({
        title: '已取消收藏',
        icon: 'success'
      })
    } else {
      await productStore.addFavorite(product.value.id)
      favorited.value = true
      uni.showToast({
        title: '收藏成功',
        icon: 'success'
      })
    }
  } catch (error) {
    uni.showToast({
      title: '操作失败',
      icon: 'error'
    })
  }
}

const handleSkuChange = (sku) => {
  selectedSku.value = sku
  showSkuSelector.value = false
}

const addToCart = async () => {
  if (!canAddToCart.value) {
    if (!isLogin.value) {
      uni.navigateTo({
        url: '/pages/login/login'
      })
    } else if (!selectedSku.value) {
      showSkuSelector.value = true
    } else if (!hasStock.value) {
      uni.showToast({
        title: '库存不足',
        icon: 'error'
      })
    }
    return
  }
  
  try {
    await cartStore.addToCart({
      productId: product.value.id,
      skuId: selectedSku.value.id,
      quantity: 1
    })
    
    uni.showToast({
      title: '已加入购物车',
      icon: 'success'
    })
    
    // 显示购物车动画
    showCartAnimation()
  } catch (error) {
    uni.showToast({
      title: '添加失败',
      icon: 'error'
    })
  }
}

const buyNow = async () => {
  if (!canAddToCart.value) {
    if (!isLogin.value) {
      uni.navigateTo({
        url: '/pages/login/login'
      })
    } else if (!selectedSku.value) {
      showSkuSelector.value = true
    } else if (!hasStock.value) {
      uni.showToast({
        title: '库存不足',
        icon: 'error'
      })
    }
    return
  }
  
  // 创建订单
  try {
    const order = await cartStore.createOrder({
      items: [{
        productId: product.value.id,
        skuId: selectedSku.value.id,
        quantity: 1,
        price: product.value.price
      }],
      type: 'direct'
    })
    
    // 跳转到确认订单页
    uni.navigateTo({
      url: `/pages/order/confirm?orderId=${order.id}`
    })
  } catch (error) {
    uni.showToast({
      title: '创建订单失败',
      icon: 'error'
    })
  }
}

const showCartAnimation = () => {
  // 实现加入购物车动画
  const query = uni.createSelectorQuery()
  query.select('.cart-button').boundingClientRect()
  query.select('.product-image').boundingClientRect()
  
  query.exec((res) => {
    if (res[0] && res[1]) {
      const buttonRect = res[0]
      const imageRect = res[1]
      
      // 创建动画节点
      const animationNode = document.createElement('div')
      animationNode.style.cssText = `
        position: fixed;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background: #007aff;
        z-index: 9999;
        pointer-events: none;
        left: ${imageRect.left + imageRect.width / 2}px;
        top: ${imageRect.top + imageRect.height / 2}px;
      `
      document.body.appendChild(animationNode)
      
      // 执行动画
      const animation = animationNode.animate([
        {
          transform: 'translate(0, 0) scale(1)',
          opacity: 1
        },
        {
          transform: `translate(${buttonRect.left - imageRect.left}px, ${buttonRect.top - imageRect.top}px) scale(0.5)`,
          opacity: 0.5
        }
      ], {
        duration: 600,
        easing: 'cubic-bezier(0.42, 0, 0.58, 1)'
      })
      
      animation.onfinish = () => {
        document.body.removeChild(animationNode)
      }
    }
  })
}
</script>

<style lang="scss" scoped>
.product-detail {
  padding-bottom: 100rpx;
}

.product-swiper {
  width: 100%;
  height: 750rpx;
  
  .product-image {
    width: 100%;
    height: 100%;
  }
}

.product-info {
  padding: 30rpx;
  background: white;
  
  .product-header {
    margin-bottom: 30rpx;
    
    .product-title {
      display: flex;
      align-items: center;
      margin-bottom: 20rpx;
      
      .title {
        font-size: 36rpx;
        font-weight: bold;
        margin-right: 20rpx;
        flex: 1;
      }
    }
    
    .product-price {
      display: flex;
      align-items: center;
      
      .current-price {
        font-size: 48rpx;
        color: #ff5000;
        font-weight: bold;
        margin-right: 20rpx;
      }
      
      .original-price {
        font-size: 28rpx;
        color: #999;
        text-decoration: line-through;
        margin-right: 20rpx;
      }
      
      .discount {
        font-size: 24rpx;
        color: white;
        background: #ff5000;
        padding: 4rpx 12rpx;
        border-radius: 20rpx;
      }
    }
  }
  
  .product-attributes {
    margin-bottom: 30rpx;
    
    .attribute-item {
      display: flex;
      margin-bottom: 10rpx;
      font-size: 28rpx;
      
      .attribute-name {
        color: #666;
        min-width: 120rpx;
      }
      
      .attribute-value {
        color: #333;
        flex: 1;
      }
    }
  }
  
  .product-description {
    line-height: 1.6;
    color: #333;
  }
}

.product-actions {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  border-top: 1rpx solid #eee;
  padding: 20rpx;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
  
  .action-buttons {
    display: flex;
    gap: 20rpx;
    
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 40rpx;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 32rpx;
      
      &.cart-button {
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        
        uni-icons {
          margin-right: 10rpx;
        }
      }
      
      &.buy-button {
        background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
        color: white;
      }
      
      &:disabled {
        opacity: 0.5;
      }
    }
  }
}
</style>

9.2 最佳实践总结

1. 架构设计原则

分层架构:

  • 表现层(Views):负责UI展示和用户交互
  • 业务层(Services):封装业务逻辑和API调用
  • 数据层(Stores):管理应用状态和数据
  • 工具层(Utils):提供通用工具函数

组件设计原则:

  • 单一职责:每个组件只负责一个功能
  • 可复用性:通过Props和Slots提供灵活性
  • 可测试性:组件逻辑清晰,易于测试
  • 可维护性:代码结构清晰,注释完善
2. 性能优化策略

编译时优化:

  • 使用Vite进行快速构建
  • 配置合理的代码分割
  • 开启Tree Shaking
  • 使用按需引入

运行时优化:

  • 虚拟列表处理长列表
  • 图片懒加载和预加载
  • 合理使用缓存策略
  • 避免不必要的重新渲染

内存管理:

  • 及时清理无用资源
  • 避免内存泄漏
  • 使用弱引用存储大对象
  • 监控内存使用情况
3. 代码质量保证

代码规范:

  • 使用ESLint + Prettier统一代码风格
  • 配置提交前检查
  • 使用TypeScript增强类型安全
  • 编写清晰的注释和文档

测试策略:

  • 单元测试:测试工具函数和业务逻辑
  • 组件测试:测试UI组件功能
  • E2E测试:测试完整用户流程
  • 性能测试:监控关键性能指标
4. 跨平台开发要点

平台差异处理:

  • 使用条件编译处理平台特定代码
  • 统一API封装,屏蔽平台差异
  • 设计响应式布局,适配不同屏幕
  • 提供平台特定的用户体验

性能调优:

  • 小程序端注意包体积限制
  • App端优化启动速度
  • H5端优化首屏加载
  • 各平台使用各自的性能优化技巧
5. 开发工作流优化

自动化流程:

  • 自动化构建和部署
  • 自动化测试
  • 自动化代码检查
  • 自动化文档生成

团队协作:

  • 使用Git规范提交信息
  • 配置代码审查流程
  • 建立组件库和工具库
  • 分享技术经验和最佳实践
6. 安全注意事项

数据安全:

  • 敏感数据加密存储
  • API请求使用HTTPS
  • 输入数据验证和过滤
  • 防止XSS和CSRF攻击

用户隐私:

  • 明确告知用户数据收集
  • 提供隐私设置选项
  • 合规处理用户数据
  • 定期安全审计
7. 持续改进机制

监控体系:

  • 错误监控和上报
  • 性能监控和分析
  • 用户行为分析
  • 业务指标监控

反馈循环:

  • 收集用户反馈
  • 分析使用数据
  • 定期技术复盘
  • 持续优化迭代

9.3 未来发展趋势

Vue3 生态发展:

  • Composition API 更加成熟
  • Volar 更好的TypeScript支持
  • 新的响应式优化
  • 更好的开发工具链

uni-app 发展方向:

  • 更完善的多端支持
  • 更好的性能表现
  • 更丰富的生态组件
  • 更好的开发体验

跨平台技术演进:

  • Web标准的不断完善
  • 小程序生态的持续发展
  • 混合开发技术的成熟
  • 新的跨平台解决方案

在实际开发中,应该根据项目需求灵活运用这些技术,并持续学习和探索新的解决方案。

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百锦再@新空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值