多模块架构重构方案.md
17.7 KB
多模块架构重构方案
日期: 2026-02-09 目的: 解决
src/views/下 4 个重复模块(bieyuan、by、checkin、xys)的维护困难问题
📊 当前问题分析
问题概览
文件结构:
src/views/
├── bieyuan/
│ ├── index.vue # 首页入口
│ ├── map.vue # 地图页
│ ├── info.vue # 详情页
│ ├── scan.vue # 扫码页
│ └── info_w.vue # 详情页变体
├── by/
│ ├── index.vue # 与 bieyuan 完全相同
│ ├── map.vue # 与 bieyuan 有差异
│ ├── info.vue # 与 bieyuan 有差异
│ ├── scan.vue # 与 bieyuan 完全相同
│ └── info_w.vue # 与 bieyuan 有差异
├── checkin/
│ ├── index.vue # ...
│ ├── map.vue
│ ├── info.vue
│ └── scan.vue
└── xys/
├── index.vue
└── ...
代码重复统计:
- ✅ 完全相同:
index.vue、scan.vue(100% 重复) - ⚠️ 高度相似:
map.vue、info.vue(约 80% 相似) - 📝 累计重复代码: 约 800+ 行
维护痛点:
- ❌ 修改一个 bug 需要在 4 个地方修改
- ❌ 新增功能需要同步到 4 个模块
- ❌ 代码审查需要检查多个文件
- ❌ 容易出现不一致的问题
- ❌ 占用大量磁盘空间和构建时间
差异点分析
通过对比 bieyuan 和 by 的代码,发现主要差异:
-
路由路径不同
- bieyuan:
/bieyuan/* - by:
/by/* - checkin:
/checkin/* - xys:
/xys/*
- bieyuan:
-
功能开关
-
by模块有音频列表功能(show_audio) -
by模块有步行导航功能(goToWalk) -
bieyuan模块的 Logo 显示,by模块隐藏
-
-
API 调用
-
by额外调用mapAudioAPI获取音频列表
-
-
UI 细节
- 文案、图片、样式可能不同
🎯 重构方案设计
核心原则
- 配置驱动:通过配置文件管理模块差异
- 组件复用:抽取通用组件,通过 props 传递配置
- 路由简化:使用动态路由参数替代多个路由
- 向后兼容:保持旧路由可用,避免影响现有链接
📐 新架构设计
目录结构
src/
├── views/
│ ├── shared/ # 共享组件(新增)
│ │ ├── MapIndex.vue # 通用首页入口
│ │ ├── MapPage.vue # 通用地图页
│ │ ├── MapInfo.vue # 通用详情页
│ │ ├── MapScan.vue # 通用扫码页
│ │ └── composables/ # 共享业务逻辑
│ │ ├── useMapData.js # 地图数据管理
│ │ ├── useAudioList.js # 音频列表管理
│ │ └── useNavigation.js # 导航功能
│ ├── modules/ # 模块配置(新增)
│ │ ├── bieyuan.config.js # 别院配置
│ │ ├── by.config.js # BY 配置
│ │ ├── checkin.config.js # 打卡配置
│ │ └── xys.config.js # XYS 配置
│ ├── bieyuan/ # 保留(向后兼容)
│ ├── by/ # 保留(向后兼容)
│ ├── checkin/ # 保留(向后兼容)
│ └── xys/ # 保留(向后兼容)
└── utils/
└── module-config.js # 配置管理工具
配置文件结构
// src/views/modules/bieyuan.config.js
export default {
// 模块标识
id: 'bieyuan',
name: '别院',
// 路由配置
routes: {
index: '/bieyuan',
map: '/bieyuan/map',
info: '/bieyuan/info',
scan: '/bieyuan/scan',
},
// UI 配置
ui: {
logo: {
show: true,
src: 'https://cdn.ipadbiz.cn/bieyuan/map/icon/index_logo@3x.png',
},
slogan: '山水逢甘露,静心遇桃源',
theme: {
primary: '#DD7850',
// ... 其他主题配置
},
},
// 功能开关
features: {
audioList: false, // 音频列表
walkRoute: false, // 步行导航
qrScan: true, // 二维码扫描
checkin: false, // 打卡功能
},
// API 配置
api: {
mapData: 'mapAPI',
audioList: null, // 不使用音频列表 API
},
// 业务逻辑配置
business: {
defaultMapView: 'default', // 默认地图视图
enableMarkers: true, // 是否启用标记点
// ... 其他业务配置
},
}
// src/views/modules/by.config.js
export default {
id: 'by',
name: 'BY',
routes: {
index: '/by',
map: '/by',
info: '/by/info',
scan: '/by/scan',
},
ui: {
logo: {
show: false, // BY 模块不显示 Logo
src: '',
},
slogan: '山水逢甘露,静心遇桃源',
theme: {
primary: '#DD7850',
},
},
features: {
audioList: true, // ✅ BY 模块启用音频列表
walkRoute: true, // ✅ BY 模块启用步行导航
qrScan: true,
checkin: false,
},
api: {
mapData: 'mapAPI',
audioList: 'mapAudioAPI', // ✅ 使用音频列表 API
},
business: {
defaultMapView: 'default',
enableMarkers: true,
},
}
通用组件实现
1. 通用首页入口(MapIndex.vue)
<!-- src/views/shared/MapIndex.vue -->
<template>
<div class="map-index-page">
<div class="index-header">
<van-image
v-if="config.ui.logo.show"
width="12rem"
height="12rem"
fit="contain"
:src="config.ui.logo.src"
/>
<div class="index-slogan">{{ config.ui.slogan }}</div>
</div>
<div
@click="goTo"
class="index-enter-btn"
:style="{
borderColor: config.ui.theme.primary,
color: config.ui.theme.primary,
}"
>
进 入
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useModuleConfig } from '@/utils/module-config'
const $router = useRouter()
const $route = useRoute()
// 获取当前模块配置
const { config } = useModuleConfig()
const goTo = () => {
$router.push({
path: config.routes.map,
query: $route.query,
})
}
</script>
<style lang="less" scoped>
.map-index-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
.index-header {
display: flex;
flex-direction: column;
align-items: center;
}
.index-slogan {
margin-top: 2rem;
font-size: 0.95rem;
letter-spacing: 5px;
color: #47525F;
}
.index-enter-btn {
padding: 0.8rem 5.5rem;
border-radius: 5px;
font-size: 1.15rem;
background-color: white;
cursor: pointer;
}
}
</style>
2. 通用详情页(MapInfo.vue)
<!-- src/views/shared/MapInfo.vue -->
<template>
<div class="map-info-page">
<!-- 条件渲染:音频按钮 -->
<div
v-if="config.features.audioList && page_details.show_audio"
@click="onClickAudioList"
class="audio-btn"
>
<!-- 音频按钮内容 -->
</div>
<!-- 条件渲染:步行导航按钮 -->
<div
v-if="config.features.walkRoute"
@click="goToWalk"
class="walk-btn"
>
步行导航
</div>
<!-- 条件渲染:Logo -->
<div
v-if="config.ui.logo.show"
class="info-logo"
:style="{
marginBottom: audio_list_height
? `${audio_list_height * 1.5}px`
: '3rem',
}"
>
<img :src="config.ui.logo.src" alt="Logo" />
</div>
<!-- 其他通用内容 -->
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useModuleConfig } from '@/utils/module-config'
import { useMapData } from './composables/useMapData'
import { useAudioList } from './composables/useAudioList'
import { useNavigation } from './composables/useNavigation'
const $route = useRoute()
const $router = useRouter()
const { config } = useModuleConfig()
// 通用业务逻辑
const { page_details, fetchMapData } = useMapData(config)
const { audio_list_height, onClickAudioList } = useAudioList(config)
const { goToWalk } = useNavigation(config)
// 页面初始化
onMounted(async () => {
await fetchMapData($route.query.id)
// 条件加载:音频列表
if (config.features.audioList) {
await loadAudioList()
}
})
</script>
3. 模块配置管理工具
// src/utils/module-config.js
import { reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
// 模块配置导入
import bieyuanConfig from '@/views/modules/bieyuan.config.js'
import byConfig from '@/views/modules/by.config.js'
import checkinConfig from '@/views/modules/checkin.config.js'
import xysConfig from '@/views/modules/xys.config.js'
// 配置映射表
const MODULE_CONFIGS = {
bieyuan: bieyuanConfig,
by: byConfig,
checkin: checkinConfig,
xys: xysConfig,
}
// 全局当前模块状态
const currentModule = reactive({
id: null,
config: null,
})
/**
* 获取当前模块配置
* @returns {Object} { config, moduleId }
*/
export function useModuleConfig() {
const $route = useRoute()
// 根据路由路径识别模块
const moduleId = computed(() => {
const path = $route.path
if (path.startsWith('/bieyuan')) return 'bieyuan'
if (path.startsWith('/by')) return 'by'
if (path.startsWith('/checkin')) return 'checkin'
if (path.startsWith('/xys')) return 'xys'
// 默认模块
return 'bieyuan'
})
// 获取配置
const config = computed(() => {
const id = moduleId.value
return MODULE_CONFIGS[id] || MODULE_CONFIGS.bieyuan
})
// 更新全局状态
if (currentModule.id !== moduleId.value) {
currentModule.id = moduleId.value
currentModule.config = config.value
}
return {
config: config.value,
moduleId: moduleId.value,
}
}
/**
* 根据模块 ID 获取配置
* @param {string} moduleId 模块 ID
* @returns {Object} 模块配置
*/
export function getModuleConfig(moduleId) {
return MODULE_CONFIGS[moduleId] || MODULE_CONFIGS.bieyuan
}
/**
* 获取所有模块配置
* @returns {Object} 所有模块配置
*/
export function getAllModuleConfigs() {
return MODULE_CONFIGS
}
Composables 实现
1. 地图数据管理
// src/views/shared/composables/useMapData.js
import { ref } from 'vue'
import { mapAPI, mapAudioAPI } from '@/api/map.js'
export function useMapData(config) {
const page_details = ref({})
const audio_list_height = ref(0)
/**
* 获取地图数据
*/
const fetchMapData = async (id) => {
try {
const { data } = await mapAPI({ id })
if (data) {
page_details.value = data
}
} catch (error) {
console.error('获取地图数据失败:', error)
}
}
/**
* 加载音频列表
*/
const loadAudioList = async (mid, markerId) => {
if (!config.features.audioList) return
try {
const apiName = config.api.audioList
const API = apiName === 'mapAudioAPI' ? mapAudioAPI : null
if (!API) return
const { data } = await API({ mid, bid: markerId })
if (data && data.length) {
page_details.value.show_audio = true
}
} catch (error) {
console.error('获取音频列表失败:', error)
}
}
return {
page_details,
audio_list_height,
fetchMapData,
loadAudioList,
}
}
2. 导航功能
// src/views/shared/composables/useNavigation.js
import { useRouter } from 'vue-router'
import { useModuleConfig } from '@/utils/module-config'
export function useNavigation(configOverride) {
const $router = useRouter()
const { config } = useModuleConfig()
/**
* 前往地图页
*/
const goToMap = () => {
$router.push({
path: config.routes.map,
query: { id: $router.currentRoute.value.query.id },
})
}
/**
* 打开步行导航
*/
const goToWalk = (point) => {
if (!config.features.walkRoute) {
console.warn('当前模块不支持步行导航')
return
}
// 触发步行导航事件
// ...
}
/**
* 返回首页
*/
const goBack = () => {
$router.push({
path: config.routes.index,
query: { id: $router.currentRoute.value.query.id },
})
}
return {
goToMap,
goToWalk,
goBack,
}
}
路由配置优化
方案 1: 保持向后兼容(推荐)
// src/route.js(重构后)
export default [
// ... 其他路由
// 通用路由(新增)
{
path: '/map/:module',
component: () => import('@/views/shared/MapPage.vue'),
meta: {
title: '地图',
},
},
{
path: '/map/:module/info',
component: () => import('@/views/shared/MapInfo.vue'),
meta: {
title: '详情页',
},
},
{
path: '/map/:module/scan',
component: () => import('@/views/shared/MapScan.vue'),
meta: {
title: '扫描',
},
},
// 旧路由保留(向后兼容)
{
path: '/bieyuan',
redirect: '/map/bieyuan', // 重定向到新路由
},
{
path: '/bieyuan/map',
redirect: '/map/bieyuan',
},
{
path: '/by',
redirect: '/map/by',
},
{
path: '/checkin',
redirect: '/map/checkin',
},
// ...
]
方案 2: 完全重构(激进)
// src/route.js
export default [
// 只保留通用路由
{
path: '/map/:module',
component: () => import('@/views/shared/MapPage.vue'),
},
{
path: '/map/:module/info',
component: () => import('@/views/shared/MapInfo.vue'),
},
{
path: '/map/:module/scan',
component: () => import('@/views/shared/MapScan.vue'),
},
]
🚀 实施计划
阶段 1: 基础设施搭建(1-2 天)
任务:
-
创建
src/views/shared/目录 -
创建
src/views/modules/目录 -
实现
src/utils/module-config.js - 编写配置文件模板
- 编写 4 个模块的配置文件
验收标准:
- 配置文件结构清晰
-
useModuleConfig()可以正确返回配置
阶段 2: 组件抽取(2-3 天)
任务:
-
抽取
MapIndex.vue(通用首页) -
抽取
MapScan.vue(通用扫码页) -
抽取
MapInfo.vue(通用详情页) -
抽取
MapPage.vue(通用地图页) - 实现 Composables(useMapData、useAudioList、useNavigation)
验收标准:
- 4 个通用组件可以独立运行
- 通过配置可以控制功能显示/隐藏
- 代码覆盖 4 个模块的核心功能
阶段 3: 路由重构(1 天)
任务:
- 添加通用路由
- 配置旧路由重定向
- 更新导航链接
- 测试所有路由是否正常
验收标准:
- 新路由可以正常访问
- 旧路由自动重定向到新路由
- 现有链接不受影响
阶段 4: 测试与验证(1-2 天)
任务:
- 单元测试(Composables)
- 集成测试(组件功能)
- E2E 测试(关键流程)
- 性能测试(构建体积、加载时间)
验收标准:
- 所有测试通过
- 无功能回归
- 性能无明显下降
阶段 5: 清理与优化(1 天)
任务:
-
删除或标记
src/views/bieyuan/等旧目录为废弃 - 更新文档
- 代码审查
- 性能优化
验收标准:
- 代码仓库干净整洁
- 文档完整
- 无遗留问题
📈 预期收益
代码质量
| 指标 | 重构前 | 重构后 | 改善 |
|---|---|---|---|
| 代码重复 | ~800 行 | ~0 行 | ✅ -100% |
| 组件数量 | 20 个 | 4 个 + 4 配置 | ✅ -60% |
| 维护成本 | 修改 4 处 | 修改 1 处 | ✅ -75% |
| Bug 风险 | 高 | 低 | ✅ 显著降低 |
开发效率
添加新模块:
- 重构前:复制目录 + 修改文件(2-3 小时)
- 重构后:创建配置文件(15-30 分钟)
- 效率提升: 80%+
修改功能:
- 重构前:修改 4 个文件(1-2 小时)
- 重构后:修改 1 个组件(15-30 分钟)
- 效率提升: 75%+
可维护性
- ✅ 统一的代码风格
- ✅ 集中的配置管理
- ✅ 清晰的职责划分
- ✅ 易于代码审查
- ✅ 便于新人上手
⚠️ 风险与注意事项
风险
-
回归风险
- 重构过程中可能引入新 bug
- 缓解方案:完整的测试覆盖
-
性能风险
- 配置读取可能影响性能
- 缓解方案:使用 computed 缓存配置
-
兼容性风险
- 旧链接可能失效
- 缓解方案:保留旧路由重定向
注意事项
-
向后兼容
- 保留旧路由 6-12 个月
- 监控旧路由访问量
- 逐步废弃旧代码
-
渐进式重构
- 不要一次性重构所有模块
- 先重构 1-2 个模块验证方案
- 逐步推广到其他模块
-
测试优先
- 先编写测试用例
- 确保 100% 功能覆盖
- 测试通过后再部署
🎯 总结
核心思想
从"复制粘贴"到"配置驱动"
- 重构前:4 个独立目录,大量重复代码
- 重构后:1 套通用组件 + 4 个配置文件
关键技术
- 配置驱动架构:通过配置文件管理模块差异
- 组件复用:抽取通用组件,通过 props 传递配置
- Composables:封装可复用的业务逻辑
- 动态路由:使用路由参数替代多个路由
未来扩展
添加新模块只需:
- 创建配置文件(
src/views/modules/new-module.config.js) - 配置路由(如果需要)
- 完成!
耗时: 从 2-3 小时缩短到 15-30 分钟!
下一步行动:
- 审阅本方案
- 确认重构范围和优先级
- 制定详细的时间表
- 开始实施
建议: 先选择 1 个模块(如 by)进行试点重构,验证方案可行性后再推广到其他模块。