多模块架构重构方案.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.vuescan.vue(100% 重复)
  • ⚠️ 高度相似: map.vueinfo.vue(约 80% 相似)
  • 📝 累计重复代码: 约 800+ 行

维护痛点

  • ❌ 修改一个 bug 需要在 4 个地方修改
  • ❌ 新增功能需要同步到 4 个模块
  • ❌ 代码审查需要检查多个文件
  • ❌ 容易出现不一致的问题
  • ❌ 占用大量磁盘空间和构建时间

差异点分析

通过对比 bieyuanby 的代码,发现主要差异:

  1. 路由路径不同

    • bieyuan: /bieyuan/*
    • by: /by/*
    • checkin: /checkin/*
    • xys: /xys/*
  2. 功能开关

    • by 模块有音频列表功能(show_audio
    • by 模块有步行导航功能(goToWalk
    • bieyuan 模块的 Logo 显示,by 模块隐藏
  3. API 调用

    • by 额外调用 mapAudioAPI 获取音频列表
  4. UI 细节

    • 文案、图片、样式可能不同

🎯 重构方案设计

核心原则

  1. 配置驱动:通过配置文件管理模块差异
  2. 组件复用:抽取通用组件,通过 props 传递配置
  3. 路由简化:使用动态路由参数替代多个路由
  4. 向后兼容:保持旧路由可用,避免影响现有链接

📐 新架构设计

目录结构

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,
      }"
    >
      进&nbsp;入
    </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%+

可维护性

  • ✅ 统一的代码风格
  • ✅ 集中的配置管理
  • ✅ 清晰的职责划分
  • ✅ 易于代码审查
  • ✅ 便于新人上手

⚠️ 风险与注意事项

风险

  1. 回归风险

    • 重构过程中可能引入新 bug
    • 缓解方案:完整的测试覆盖
  2. 性能风险

    • 配置读取可能影响性能
    • 缓解方案:使用 computed 缓存配置
  3. 兼容性风险

    • 旧链接可能失效
    • 缓解方案:保留旧路由重定向

注意事项

  1. 向后兼容

    • 保留旧路由 6-12 个月
    • 监控旧路由访问量
    • 逐步废弃旧代码
  2. 渐进式重构

    • 不要一次性重构所有模块
    • 先重构 1-2 个模块验证方案
    • 逐步推广到其他模块
  3. 测试优先

    • 先编写测试用例
    • 确保 100% 功能覆盖
    • 测试通过后再部署

🎯 总结

核心思想

从"复制粘贴"到"配置驱动"

  • 重构前:4 个独立目录,大量重复代码
  • 重构后:1 套通用组件 + 4 个配置文件

关键技术

  1. 配置驱动架构:通过配置文件管理模块差异
  2. 组件复用:抽取通用组件,通过 props 传递配置
  3. Composables:封装可复用的业务逻辑
  4. 动态路由:使用路由参数替代多个路由

未来扩展

添加新模块只需:

  1. 创建配置文件(src/views/modules/new-module.config.js
  2. 配置路由(如果需要)
  3. 完成!

耗时: 从 2-3 小时缩短到 15-30 分钟


下一步行动

  1. 审阅本方案
  2. 确认重构范围和优先级
  3. 制定详细的时间表
  4. 开始实施

建议: 先选择 1 个模块(如 by)进行试点重构,验证方案可行性后再推广到其他模块。