design.md 25 KB

Design Document

Overview

本设计文档描述了将「农小禹智慧农业系统」从 uni-app Vue2 (Options API) 迁移到 uni-app Vue3 (Composition API) 的技术方案。迁移的核心目标是支持 HarmonyOS 打包,同时确保在 Android、iOS 和 H5 平台上的功能和行为完全一致。

迁移原则

  1. 零业务逻辑改动 - 所有业务逻辑、API 调用、数据处理保持不变
  2. 零 UI 结构改动 - 所有模板结构、样式、布局保持不变
  3. 语法层面迁移 - 仅进行 Vue2 到 Vue3 的语法和 API 转换
  4. 跨平台一致性 - 确保所有平台行为一致
  5. HarmonyOS 兼容 - 移除浏览器特定 API,使用 uni-app 跨平台 API
  6. 可维护性优先 - 遵循 Vue3 最佳实践,添加清晰的注释

项目现状分析

当前技术栈:

  • Vue 2.6.14 + Options API
  • Vuex 3.6.2
  • uni-app 2.x
  • uview-ui 2.0.38
  • 第三方插件: Jessibuca (视频播放)、高德地图 SDK

项目规模:

  • 约 40+ 页面组件
  • 4 个公共组件
  • 15+ API 服务模块
  • 1 个 Vuex store
  • 多个工具函数模块

Architecture

迁移架构设计

┌─────────────────────────────────────────────────────────────┐
│                     迁移执行层                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 组件迁移引擎  │  │ Store迁移引擎 │  │ 配置迁移引擎  │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                     转换规则层                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 语法转换规则  │  │ API映射规则   │  │ 生命周期映射  │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                     验证层                                    │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 语法验证器    │  │ 功能测试器    │  │ 性能测试器    │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘

分层职责

迁移执行层:

  • 组件迁移引擎: 负责 .vue 文件的转换
  • Store 迁移引擎: 负责 Vuex store 的升级
  • 配置迁移引擎: 负责 package.json、manifest.json 等配置文件的更新

转换规则层:

  • 语法转换规则: Options API → Composition API 的转换规则
  • API 映射规则: Vue2 API → Vue3 API 的映射关系
  • 生命周期映射: Vue2 生命周期钩子 → Vue3 生命周期钩子的映射

验证层:

  • 语法验证器: 验证转换后的代码符合 Vue3 规范
  • 功能测试器: 验证业务逻辑和功能一致性
  • 性能测试器: 验证性能指标不低于原版本

Components and Interfaces

核心组件

1. 组件迁移引擎 (ComponentMigrationEngine)

职责: 将 Vue2 单文件组件转换为 Vue3 Composition API 格式

接口:

interface ComponentMigrationEngine {
  // 迁移单个组件文件
  migrateComponent(filePath: string): MigrationResult
  
  // 批量迁移组件
  migrateComponents(filePaths: string[]): MigrationResult[]
  
  // 验证迁移结果
  validateMigration(result: MigrationResult): ValidationResult
}

转换流程:

  1. 解析 Vue2 组件的 script、template、style 部分
  2. 转换 script 部分: Options API → Composition API
  3. 更新 template 部分: 移除 Vue2 特有语法
  4. 保持 style 部分不变
  5. 生成 Vue3 组件文件

2. Store 迁移引擎 (StoreMigrationEngine)

职责: 将 Vuex 3.x store 升级到 Vuex 4.x 或迁移到 Pinia

接口:

interface StoreMigrationEngine {
  // 迁移 store 文件
  migrateStore(storePath: string, target: 'vuex4' | 'pinia'): MigrationResult
  
  // 更新组件中的 store 使用
  updateStoreUsage(componentPath: string): MigrationResult
}

转换策略:

  • 选项 A: 升级到 Vuex 4.x (保持 API 相似性,迁移成本低)
  • 选项 B: 迁移到 Pinia (更现代,更好的 TypeScript 支持)
  • 推荐: Vuex 4.x (项目已有 Vuex 使用经验,迁移风险低)

3. 配置迁移引擎 (ConfigMigrationEngine)

职责: 更新项目配置文件以支持 Vue3

接口:

interface ConfigMigrationEngine {
  // 更新 package.json 依赖
  updateDependencies(): void
  
  // 更新 manifest.json 配置
  updateManifest(): void
  
  // 更新 main.js 入口文件
  updateMainEntry(): void
}

需要更新的配置:

  • package.json: 升级 Vue、Vuex、uni-app 等依赖
  • manifest.json: 更新 vueVersion 为 "3"
  • main.js: 使用 createApp 替代 new Vue

转换规则定义

Options API → Composition API 转换规则

Vue2 Options API Vue3 Composition API 说明
data() ref() / reactive() 基本类型用 ref,对象用 reactive
methods 函数定义 在 setup 中定义普通函数
computed computed() 使用 computed 函数包装
watch watch() / watchEffect() 使用 watch 函数
props defineProps() 使用 defineProps 声明
$emit defineEmits() 使用 defineEmits 声明
this.$refs ref() 使用 ref 创建模板引用
this.$store useStore() 使用 useStore 获取 store
this.$route useRoute() 使用 useRoute 获取路由
this.$router useRouter() 使用 useRouter 获取路由器

生命周期钩子映射

Vue2 生命周期 Vue3 生命周期 说明
beforeCreate setup() 顶层 逻辑移至 setup 顶层
created setup() 顶层 逻辑移至 setup 顶层
beforeMount onBeforeMount() 导入并使用
mounted onMounted() 导入并使用
beforeUpdate onBeforeUpdate() 导入并使用
updated onUpdated() 导入并使用
beforeDestroy onBeforeUnmount() 名称变更
destroyed onUnmounted() 名称变更
activated onActivated() 导入并使用
deactivated onDeactivated() 导入并使用
errorCaptured onErrorCaptured() 导入并使用
uni-app 生命周期 保持不变 onLoad, onShow, onHide 等

模板语法转换规则

Vue2 语法 Vue3 语法 说明
v-model v-model 自定义组件需调整
.sync v-model:propName .sync 修饰符已移除
$listeners 移除 已合并到 $attrs
v-bind="$attrs" v-bind="$attrs" 保持不变
自定义指令钩子 更新钩子名称 bind→beforeMount 等

Data Models

迁移结果数据模型

interface MigrationResult {
  // 文件路径
  filePath: string
  
  // 迁移状态
  status: 'success' | 'partial' | 'failed'
  
  // 转换后的代码
  code: string
  
  // 警告信息
  warnings: Warning[]
  
  // 错误信息
  errors: Error[]
  
  // TODO 项
  todos: TodoItem[]
}

interface Warning {
  line: number
  column: number
  message: string
  rule: string
}

interface TodoItem {
  line: number
  message: string
  reason: string
  recommendation: string
}

interface ValidationResult {
  isValid: boolean
  errors: ValidationError[]
  warnings: ValidationWarning[]
}

组件转换示例

Vue2 组件 (Before):

<script>
export default {
  data() {
    return {
      count: 0,
      user: {
        name: 'John',
        age: 30
      }
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('Component mounted')
  }
}
</script>

Vue3 组件 (After):

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

const count = ref(0)
const user = reactive({
  name: 'John',
  age: 30
})

const doubleCount = computed(() => count.value * 2)

const increment = () => {
  count.value++
}

onMounted(() => {
  console.log('Component mounted')
})
</script>

Store 转换示例

Vuex 3.x (Before):

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

export default store

Vuex 4.x (After):

// store/index.js
import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

export default store

组件中使用 Store (Before):

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}
</script>

组件中使用 Store (After):

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

const store = useStore()

const count = computed(() => store.state.count)

const increment = () => {
  store.commit('increment')
}
</script>

全局配置转换示例

main.js (Before):

import Vue from 'vue'
import App from './App'
import store from './store'
import uView from 'uview-ui'

// 全局过滤器
Vue.filter('formatDate', (value) => {
  // ...
})

// 全局属性
Vue.prototype.$api = api

Vue.use(store)
Vue.use(uView)

const app = new Vue({
  store,
  ...App
})
app.$mount()

main.js (After):

import { createSSRApp } from 'vue'
import App from './App.vue'
import store from './store'
import uView from 'uview-ui' // 需要 Vue3 兼容版本

// 全局方法 (替代过滤器)
import { formatDate } from './utils/filters'

export function createApp() {
  const app = createSSRApp(App)
  
  app.use(store)
  app.use(uView)
  
  // 全局属性
  app.config.globalProperties.$api = api
  app.config.globalProperties.$formatDate = formatDate
  
  return {
    app
  }
}

Correctness Properties

属性 (Property) 是关于系统应该如何行为的形式化陈述,它应该在所有有效执行中保持为真。属性是人类可读规范和机器可验证正确性保证之间的桥梁。通过属性测试,我们可以验证代码在各种输入下的正确性。

Property 1: Options API 完整转换

对于任意 Vue2 组件,如果它包含 data、methods、computed、watch 中的任意选项,转换后的 Vue3 组件应该使用对应的 Composition API (ref/reactive、函数定义、computed()、watch()),并且不包含 Options API 的选项对象。

Validates: Requirements 1.1, 1.3

Property 2: 生命周期钩子正确映射

对于任意 Vue2 组件,如果它包含生命周期钩子 (mounted、created、beforeDestroy、destroyed 等),转换后的 Vue3 组件应该使用对应的 Composition API 钩子 (onMounted、setup 顶层、onBeforeUnmount、onUnmounted 等),并且钩子内的业务逻辑保持不变。

Validates: Requirements 1.2, 2.1, 2.2, 2.3, 2.4

Property 3: uni-app 生命周期保持不变

对于任意 包含 uni-app 特有生命周期 (onLoad、onShow、onHide、onPullDownRefresh 等) 的组件,转换后这些生命周期钩子的名称和用法应该保持完全不变。

Validates: Requirements 2.5

Property 4: this 关键字完全移除

对于任意 转换后的 Vue3 组件,代码中不应该包含 this. 的引用 (除了在注释中),所有数据和方法访问应该使用 Composition API 的直接引用。

Validates: Requirements 1.4

Property 5: script setup 语法使用

对于任意 转换后的 Vue3 组件,应该使用 <script setup> 语法糖,而不是传统的 <script> + export default 方式。

Validates: Requirements 1.5

Property 6: Store 使用方式转换

对于任意 在组件中使用 Vuex store 的代码,转换后应该使用 useStore() 获取 store 实例,而不是 this.$store,并且所有 store 状态访问应该使用 computed() 包装以保持响应性。

Validates: Requirements 3.2, 3.3

Property 7: Props 和 Emits 声明

对于任意 接收 props 或发出事件的组件,转换后应该使用 defineProps()defineEmits() 进行声明,并且保持原有的类型定义、默认值和验证规则。

Validates: Requirements 7.1, 7.2, 7.3

Property 8: 模板引用转换

对于任意 使用 this.$refs 的代码,转换后应该使用 ref() 创建模板引用,并且在模板中使用 ref 属性绑定。

Validates: Requirements 6.3

Property 9: 计算属性和侦听器转换

对于任意 包含 computed 或 watch 的组件,转换后应该使用 computed()watch() 函数,并且保持原有的选项 (如 deep、immediate)。

Validates: Requirements 8.1, 8.2, 8.4

Property 10: 浏览器 API 移除

对于任意 转换后的代码,不应该包含 window.document. 的直接引用,所有浏览器特定功能应该使用 uni-app 提供的跨平台 API 替代。

Validates: Requirements 10.1, 10.2

Property 11: 条件编译正确使用

对于任意 平台特定的代码,应该使用 uni-app 的条件编译标记 (#ifdef#ifndef 等) 进行隔离,并且包含对 HarmonyOS 平台的支持。

Validates: Requirements 10.5, 13.5

Property 12: 模板结构保持不变

对于任意 Vue 组件,转换前后的 <template> 部分的 DOM 结构、元素层级、class 和 style 绑定应该保持完全一致 (除了必要的语法更新如 .sync → v-model:propName)。

Validates: Requirements 12.1, 12.3

Property 13: 样式保持不变

对于任意 Vue 组件,转换前后的 <style> 部分的内容应该保持完全一致,包括所有 CSS/SCSS 规则、选择器和属性。

Validates: Requirements 12.2

Property 14: v-model 语法更新

对于任意 在模板中使用 v-model 的自定义组件,转换后应该符合 Vue3 的 v-model 语法规范,并且 .sync 修饰符应该转换为 v-model:propName 语法。

Validates: Requirements 5.1, 5.4

Property 15: $listeners 移除

对于任意 使用 $listeners 的代码,转换后应该移除 $listeners 的引用,因为 Vue3 已将其合并到 $attrs 中。

Validates: Requirements 5.3

Property 16: 自定义指令钩子更新

对于任意 自定义指令,转换后应该更新指令钩子名称 (bind → beforeMount, inserted → mounted, update → updated, componentUpdated → updated, unbind → unmounted)。

Validates: Requirements 5.2

Property 17: TODO 注释添加

对于任意 无法自动迁移的代码,转换后应该添加明确的 TODO 注释,包含问题原因和推荐的解决方案。

Validates: Requirements 14.2, 17.1, 17.2

Error Handling

错误分类

1. 语法错误 (Syntax Errors)

  • 描述: 转换后的代码不符合 Vue3 语法规范
  • 处理策略:
    • 添加 TODO 注释标记问题位置
    • 提供详细的错误信息和修复建议
    • 在迁移报告中记录错误详情

2. 不兼容的第三方库 (Incompatible Dependencies)

  • 描述: 第三方库不支持 Vue3
  • 处理策略:
    • uview-ui: 升级到 uview-plus (Vue3 版本)
    • Jessibuca: 保持现有实现,使用条件编译隔离 H5 平台
    • 其他库: 寻找 Vue3 兼容的替代方案或自行实现

3. 复杂的 this 引用 (Complex this References)

  • 描述: 某些复杂的 this 引用难以自动转换
  • 处理策略:

    • 添加 TODO 注释
    • 提供手动转换指南
    • 示例:

      // TODO: 手动转换复杂的 this 引用
      // 原因: 动态属性访问难以自动推断
      // 推荐: 使用明确的变量引用替代 this[dynamicKey]
      

4. 全局混入 (Global Mixins)

  • 描述: 全局混入在 Vue3 中不推荐使用
  • 处理策略:
    • 评估是否可以转换为组合式函数
    • 如果必须保留,添加 TODO 注释说明
    • 提供组合式函数的重构建议

5. 过滤器 (Filters)

  • 描述: Vue3 移除了过滤器功能
  • 处理策略:
    • 转换为全局方法: app.config.globalProperties.$filterName
    • 或转换为组合式函数
    • 在模板中使用方法调用替代管道语法

错误恢复机制

interface ErrorRecovery {
  // 尝试自动修复
  autoFix(error: MigrationError): FixResult
  
  // 回退到安全状态
  rollback(filePath: string): void
  
  // 生成修复建议
  generateSuggestion(error: MigrationError): Suggestion
}

日志和报告

迁移日志格式:

[INFO] 开始迁移: pages/dashboard/index.vue
[SUCCESS] 转换 data → ref/reactive
[SUCCESS] 转换 methods → 函数定义
[SUCCESS] 转换 computed → computed()
[WARNING] 发现复杂的 this 引用,已添加 TODO 注释
[SUCCESS] 转换 mounted → onMounted
[INFO] 迁移完成: pages/dashboard/index.vue (1 warning, 0 errors)

迁移报告结构:

# 迁移报告

## 概览
- 总文件数: 45
- 成功: 40
- 部分成功: 4
- 失败: 1

## 详细信息

### 成功迁移 (40)
- pages/dashboard/index.vue
- pages/login/index.vue
- ...

### 部分成功 (4)
- pages/device/index.vue (1 warning)
  - 警告: 复杂的 this 引用需要手动处理
- ...

### 失败 (1)
- components/complex-component.vue
  - 错误: 使用了不支持的第三方库

## TODO 清单
1. pages/device/index.vue:123 - 手动转换复杂的 this 引用
2. components/complex-component.vue:45 - 替换不兼容的第三方库

Testing Strategy

测试方法论

本项目采用双重测试策略:

  1. 单元测试 - 验证具体示例、边缘情况和错误条件
  2. 属性测试 - 验证通用属性在所有输入下的正确性

两种测试方法互补:

  • 单元测试捕获具体的 bug 和边缘情况
  • 属性测试验证通用的正确性保证

属性测试配置

测试框架:

  • JavaScript/TypeScript: fast-check
  • 最小迭代次数: 100 次
  • 每个正确性属性对应一个属性测试

测试标记格式:

// Feature: vue2-to-vue3-migration, Property 1: Options API 完整转换
test('Options API should be fully converted to Composition API', () => {
  fc.assert(
    fc.property(
      generateVue2Component(), // 生成器
      (component) => {
        const result = migrateComponent(component)
        // 验证转换后不包含 Options API
        expect(result.code).not.toMatch(/export default\s*{/)
        expect(result.code).toMatch(/<script setup>/)
      }
    ),
    { numRuns: 100 }
  )
})

单元测试策略

1. 语法转换测试

  • 测试 data → ref/reactive 转换
  • 测试 methods → 函数定义转换
  • 测试 computed → computed() 转换
  • 测试 watch → watch() 转换
  • 测试生命周期钩子映射

2. 边缘情况测试

  • 空组件
  • 只有 template 的组件
  • 复杂嵌套的数据结构
  • 动态属性访问
  • 异步操作

3. 集成测试

  • 完整页面的迁移
  • Store 与组件的集成
  • 路由与组件的集成
  • 第三方库的集成

4. 功能测试

  • 用户登录流程
  • 数据列表加载和展示
  • 表单提交和验证
  • 页面跳转和参数传递
  • 地图定位功能
  • 视频播放功能

5. 跨平台测试

  • Android 平台功能测试
  • iOS 平台功能测试
  • H5 平台功能测试
  • HarmonyOS 平台功能测试

性能测试

关键指标:

  • 应用启动时间
  • 页面首次渲染时间
  • 页面切换时间
  • 列表滚动流畅度
  • 内存占用

测试方法:

  • 使用 uni-app 性能分析工具
  • 在真机上进行测试
  • 对比迁移前后的性能数据

视觉回归测试

工具: Percy 或 BackstopJS

测试范围:

  • 所有主要页面的截图对比
  • 不同屏幕尺寸的适配
  • 不同主题的显示

测试覆盖率目标

  • 单元测试覆盖率: > 80%
  • 集成测试覆盖率: > 60%
  • 关键业务流程: 100%

Implementation Notes

迁移顺序

阶段 1: 基础设施迁移

  1. 更新 package.json 依赖
  2. 更新 manifest.json 配置
  3. 迁移 main.js 入口文件
  4. 迁移 Vuex store

阶段 2: 工具函数和服务迁移

  1. 迁移 utils 工具函数
  2. 迁移 API 服务模块
  3. 更新全局配置和常量

阶段 3: 组件迁移

  1. 迁移公共组件 (components/common)
  2. 迁移页面组件 (pages)
    • 优先级: 登录页 → 首页 → 其他页面
  3. 迁移 App.vue

阶段 4: 测试和验证

  1. 运行单元测试
  2. 运行属性测试
  3. 进行功能测试
  4. 进行跨平台测试
  5. 进行性能测试

关键技术决策

1. 状态管理: Vuex 4.x vs Pinia

决策: 使用 Vuex 4.x

理由:

  • 项目已有 Vuex 使用经验
  • Vuex 4.x API 与 3.x 高度相似,迁移成本低
  • 不需要重新培训团队
  • 风险较低

2. UI 组件库: uview-ui 升级方案

决策: 升级到 uview-plus

理由:

  • uview-plus 是 uview-ui 的 Vue3 版本
  • API 基本保持一致
  • 官方维护,稳定性有保障

迁移步骤:

  1. 安装 uview-plus: npm install uview-plus
  2. 更新导入语句
  3. 测试所有使用的组件
  4. 处理 API 差异 (如有)

3. 视频播放: Jessibuca 处理方案

决策: 保持现有实现,使用条件编译

理由:

  • Jessibuca 主要用于 H5 平台
  • 不影响其他平台
  • 使用条件编译隔离

实现:

// #ifdef H5
import JessibucaPlugin from './utils/jessibuca-plugin'
app.use(JessibucaPlugin)
// #endif

HarmonyOS 特殊处理

1. 条件编译标记

// #ifdef H5 || MP-WEIXIN || APP-PLUS
// 通用代码
// #endif

// #ifdef APP-PLUS-NVUE
// nvue 特定代码
// #endif

// 添加 HarmonyOS 支持
// #ifdef H5 || MP-WEIXIN || APP-PLUS || MP-HARMONY
// 跨平台代码
// #endif

2. API 兼容性处理

地图功能:

// 使用 uni-app 统一 API
uni.getLocation({
  type: 'gcj02',
  success: (res) => {
    // 处理定位结果
  }
})

存储功能:

// 使用 uni-app 统一 API
uni.setStorageSync('key', 'value')
const value = uni.getStorageSync('key')

不可自动迁移的场景

以下场景需要手动处理,迁移工具会添加 TODO 注释:

  1. 动态组件名称

    // TODO: 手动转换动态组件引用
    // 原因: 动态 import 需要根据具体情况调整
    // 推荐: 使用 defineAsyncComponent 包装
    const component = () => import(`@/components/${dynamicName}.vue`)
    
  2. 复杂的 this 上下文

    // TODO: 手动转换复杂的 this 引用
    // 原因: 动态属性访问难以自动推断
    // 推荐: 重构为明确的变量引用
    const value = this[computedKey]
    
  3. render 函数

    // TODO: 手动转换 render 函数
    // 原因: render 函数在 Vue3 中有 API 变更
    // 推荐: 参考 Vue3 render 函数文档
    render(h) {
    return h('div', 'content')
    }
    
  4. 全局混入

    // TODO: 评估是否可以转换为组合式函数
    // 原因: 全局混入在 Vue3 中不推荐使用
    // 推荐: 使用组合式函数替代
    Vue.mixin({
    // ...
    })
    

迁移检查清单

  • package.json 依赖已更新
  • manifest.json vueVersion 已更新为 "3"
  • main.js 使用 createSSRApp
  • Vuex store 已升级到 4.x
  • 所有组件使用 <script setup>
  • 所有 this 引用已移除
  • 所有生命周期钩子已更新
  • 所有 store 使用已更新为 useStore()
  • 所有过滤器已转换为方法
  • 所有全局属性已迁移到 globalProperties
  • 所有 window/document 引用已移除
  • 所有条件编译已包含 HarmonyOS 支持
  • uview-ui 已升级到 uview-plus
  • 所有单元测试通过
  • 所有属性测试通过
  • 所有平台功能测试通过
  • 性能指标不低于原版本