本设计文档描述了将「农小禹智慧农业系统」从 uni-app Vue2 (Options API) 迁移到 uni-app Vue3 (Composition API) 的技术方案。迁移的核心目标是支持 HarmonyOS 打包,同时确保在 Android、iOS 和 H5 平台上的功能和行为完全一致。
当前技术栈:
项目规模:
┌─────────────────────────────────────────────────────────────┐
│ 迁移执行层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 组件迁移引擎 │ │ Store迁移引擎 │ │ 配置迁移引擎 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 转换规则层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 语法转换规则 │ │ API映射规则 │ │ 生命周期映射 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 验证层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 语法验证器 │ │ 功能测试器 │ │ 性能测试器 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
迁移执行层:
转换规则层:
验证层:
职责: 将 Vue2 单文件组件转换为 Vue3 Composition API 格式
接口:
interface ComponentMigrationEngine {
// 迁移单个组件文件
migrateComponent(filePath: string): MigrationResult
// 批量迁移组件
migrateComponents(filePaths: string[]): MigrationResult[]
// 验证迁移结果
validateMigration(result: MigrationResult): ValidationResult
}
转换流程:
职责: 将 Vuex 3.x store 升级到 Vuex 4.x 或迁移到 Pinia
接口:
interface StoreMigrationEngine {
// 迁移 store 文件
migrateStore(storePath: string, target: 'vuex4' | 'pinia'): MigrationResult
// 更新组件中的 store 使用
updateStoreUsage(componentPath: string): MigrationResult
}
转换策略:
职责: 更新项目配置文件以支持 Vue3
接口:
interface ConfigMigrationEngine {
// 更新 package.json 依赖
updateDependencies(): void
// 更新 manifest.json 配置
updateManifest(): void
// 更新 main.js 入口文件
updateMainEntry(): void
}
需要更新的配置:
| 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 等 |
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>
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
}
}
属性 (Property) 是关于系统应该如何行为的形式化陈述,它应该在所有有效执行中保持为真。属性是人类可读规范和机器可验证正确性保证之间的桥梁。通过属性测试,我们可以验证代码在各种输入下的正确性。
对于任意 Vue2 组件,如果它包含 data、methods、computed、watch 中的任意选项,转换后的 Vue3 组件应该使用对应的 Composition API (ref/reactive、函数定义、computed()、watch()),并且不包含 Options API 的选项对象。
Validates: Requirements 1.1, 1.3
对于任意 Vue2 组件,如果它包含生命周期钩子 (mounted、created、beforeDestroy、destroyed 等),转换后的 Vue3 组件应该使用对应的 Composition API 钩子 (onMounted、setup 顶层、onBeforeUnmount、onUnmounted 等),并且钩子内的业务逻辑保持不变。
Validates: Requirements 1.2, 2.1, 2.2, 2.3, 2.4
对于任意 包含 uni-app 特有生命周期 (onLoad、onShow、onHide、onPullDownRefresh 等) 的组件,转换后这些生命周期钩子的名称和用法应该保持完全不变。
Validates: Requirements 2.5
对于任意 转换后的 Vue3 组件,代码中不应该包含 this. 的引用 (除了在注释中),所有数据和方法访问应该使用 Composition API 的直接引用。
Validates: Requirements 1.4
对于任意 转换后的 Vue3 组件,应该使用 <script setup> 语法糖,而不是传统的 <script> + export default 方式。
Validates: Requirements 1.5
对于任意 在组件中使用 Vuex store 的代码,转换后应该使用 useStore() 获取 store 实例,而不是 this.$store,并且所有 store 状态访问应该使用 computed() 包装以保持响应性。
Validates: Requirements 3.2, 3.3
对于任意 接收 props 或发出事件的组件,转换后应该使用 defineProps() 和 defineEmits() 进行声明,并且保持原有的类型定义、默认值和验证规则。
Validates: Requirements 7.1, 7.2, 7.3
对于任意 使用 this.$refs 的代码,转换后应该使用 ref() 创建模板引用,并且在模板中使用 ref 属性绑定。
Validates: Requirements 6.3
对于任意 包含 computed 或 watch 的组件,转换后应该使用 computed() 和 watch() 函数,并且保持原有的选项 (如 deep、immediate)。
Validates: Requirements 8.1, 8.2, 8.4
对于任意 转换后的代码,不应该包含 window. 或 document. 的直接引用,所有浏览器特定功能应该使用 uni-app 提供的跨平台 API 替代。
Validates: Requirements 10.1, 10.2
对于任意 平台特定的代码,应该使用 uni-app 的条件编译标记 (#ifdef、#ifndef 等) 进行隔离,并且包含对 HarmonyOS 平台的支持。
Validates: Requirements 10.5, 13.5
对于任意 Vue 组件,转换前后的 <template> 部分的 DOM 结构、元素层级、class 和 style 绑定应该保持完全一致 (除了必要的语法更新如 .sync → v-model:propName)。
Validates: Requirements 12.1, 12.3
对于任意 Vue 组件,转换前后的 <style> 部分的内容应该保持完全一致,包括所有 CSS/SCSS 规则、选择器和属性。
Validates: Requirements 12.2
对于任意 在模板中使用 v-model 的自定义组件,转换后应该符合 Vue3 的 v-model 语法规范,并且 .sync 修饰符应该转换为 v-model:propName 语法。
Validates: Requirements 5.1, 5.4
对于任意 使用 $listeners 的代码,转换后应该移除 $listeners 的引用,因为 Vue3 已将其合并到 $attrs 中。
Validates: Requirements 5.3
对于任意 自定义指令,转换后应该更新指令钩子名称 (bind → beforeMount, inserted → mounted, update → updated, componentUpdated → updated, unbind → unmounted)。
Validates: Requirements 5.2
对于任意 无法自动迁移的代码,转换后应该添加明确的 TODO 注释,包含问题原因和推荐的解决方案。
Validates: Requirements 14.2, 17.1, 17.2
处理策略:
示例:
// TODO: 手动转换复杂的 this 引用
// 原因: 动态属性访问难以自动推断
// 推荐: 使用明确的变量引用替代 this[dynamicKey]
app.config.globalProperties.$filterNameinterface 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 - 替换不兼容的第三方库
本项目采用双重测试策略:
两种测试方法互补:
测试框架:
测试标记格式:
// 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 }
)
})
关键指标:
测试方法:
工具: Percy 或 BackstopJS
测试范围:
阶段 1: 基础设施迁移
阶段 2: 工具函数和服务迁移
阶段 3: 组件迁移
阶段 4: 测试和验证
决策: 使用 Vuex 4.x
理由:
决策: 升级到 uview-plus
理由:
迁移步骤:
npm install uview-plus决策: 保持现有实现,使用条件编译
理由:
实现:
// #ifdef H5
import JessibucaPlugin from './utils/jessibuca-plugin'
app.use(JessibucaPlugin)
// #endif
// #ifdef H5 || MP-WEIXIN || APP-PLUS
// 通用代码
// #endif
// #ifdef APP-PLUS-NVUE
// nvue 特定代码
// #endif
// 添加 HarmonyOS 支持
// #ifdef H5 || MP-WEIXIN || APP-PLUS || MP-HARMONY
// 跨平台代码
// #endif
地图功能:
// 使用 uni-app 统一 API
uni.getLocation({
type: 'gcj02',
success: (res) => {
// 处理定位结果
}
})
存储功能:
// 使用 uni-app 统一 API
uni.setStorageSync('key', 'value')
const value = uni.getStorageSync('key')
以下场景需要手动处理,迁移工具会添加 TODO 注释:
动态组件名称
// TODO: 手动转换动态组件引用
// 原因: 动态 import 需要根据具体情况调整
// 推荐: 使用 defineAsyncComponent 包装
const component = () => import(`@/components/${dynamicName}.vue`)
复杂的 this 上下文
// TODO: 手动转换复杂的 this 引用
// 原因: 动态属性访问难以自动推断
// 推荐: 重构为明确的变量引用
const value = this[computedKey]
render 函数
// TODO: 手动转换 render 函数
// 原因: render 函数在 Vue3 中有 API 变更
// 推荐: 参考 Vue3 render 函数文档
render(h) {
return h('div', 'content')
}
全局混入
// TODO: 评估是否可以转换为组合式函数
// 原因: 全局混入在 Vue3 中不推荐使用
// 推荐: 使用组合式函数替代
Vue.mixin({
// ...
})
<script setup>