uniapp动态路由加载

246 阅读1分钟

想要完全动态地根据路由路径加载对应的页面组件,而不是预定义组件映射。以下是修改后的实现方案:

1. 修改后的主页面结构

<!-- pages/index/index.vue -->
<template>
  <view class="page-container">
    <!-- 左侧菜单 -->
    <SideMenu 
      :menu-data="menuData" 
      v-model:is-collapsed="isCollapsed"
      @item-click="handleMenuClick"
    />
    
    <!-- 右侧内容区 -->
    <view class="content-area" :class="{ 'collapsed': isCollapsed }">
      <!-- 动态内容容器 -->
      <view v-if="currentView" class="content-wrapper">
        <component :is="currentView" />
      </view>
      <view v-else class="empty-content">
        <text>请从左侧菜单选择内容</text>
      </view>
    </view>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, shallowRef, defineAsyncComponent } from 'vue'
import SideMenu from '@/components/SideMenu.vue'
import { transformRoutesToMenu, updateMenuActiveStatus } from '@/utils/menu-utils'
import type { MenuItem } from '@/types/menu'

export default defineComponent({
  components: { SideMenu },
  setup() {
    const isCollapsed = ref(false)
    const currentView = shallowRef(null)
    const currentPath = ref('')

    // 菜单数据
    const menuData = ref(transformRoutesToMenu([
      {
        path: '/pages/dashboard/index',
        name: 'Dashboard',
        meta: { title: '控制台', icon: 'home' }
      },
      {
        path: '/pages/user/index',
        name: 'User',
        meta: { title: '用户管理', icon: 'person' }
      },
      {
        path: '/pages/system/index',
        name: 'System',
        meta: { title: '系统管理', icon: 'settings' },
        children: [
          {
            path: '/pages/system/role',
            name: 'Role',
            meta: { title: '角色管理', icon: 'staff' }
          },
          {
            path: '/pages/system/menu',
            name: 'Menu',
            meta: { title: '菜单管理', icon: 'list' }
          }
        ]
      }
    ]))

    // 动态加载组件
    const loadComponent = async (path: string) => {
      try {
        // 根据路径动态生成组件路径
        const componentPath = path.startsWith('/') ? path.slice(1) : path
        currentView.value = defineAsyncComponent(() => 
          import(`@/${componentPath}.vue`)
        )
      } catch (error) {
        console.error('组件加载失败:', error)
        currentView.value = null
      }
    }

    // 处理菜单点击
    const handleMenuClick = async (item: MenuItem) => {
      if (!item.path) return
      
      currentPath.value = item.path
      await loadComponent(item.path)
      updateMenuActiveStatus(menuData.value, item.path)
    }

    return {
      isCollapsed,
      menuData,
      currentView,
      handleMenuClick
    }
  }
})
</script>

2. 动态加载的关键实现

// 动态加载组件函数
const loadComponent = async (path: string) => {
  try {
    // 处理路径格式
    let componentPath = path
      .replace(/^\//, '')       // 去除开头斜杠
      .replace(/\/index$/, '')  // 去除index结尾
    
    // 特殊处理uniapp的页面路径
    if (path.startsWith('/pages/')) {
      componentPath = path.replace('/pages/', '')
    }
    
    currentView.value = defineAsyncComponent(() => 
      import(/* webpackChunkName: "dynamic-[request]" */ `@/pages/${componentPath}.vue`)
    )
  } catch (error) {
    console.error('组件加载失败:', error)
    // 可以加载一个错误提示组件
    currentView.value = defineAsyncComponent(() => 
      import('@/components/ErrorPage.vue')
    )
  }
}

3. 路由路径处理建议

为了使动态导入更可靠,建议遵循以下路由规则:

  1. 所有页面放在 pages 目录下
  2. 路由路径与文件路径保持一致,例如:
    • /pages/user/indexpages/user/index.vue
    • /pages/system/rolepages/system/role.vue

4. 修改 SideMenu 组件

保持之前的 SideMenu 组件不变,只需确保点击菜单时传递完整的路由路径即可。

5. 添加错误处理组件

<!-- components/ErrorPage.vue -->
<template>
  <view class="error-container">
    <uni-icons type="close-circle" size="64" color="#f5222d" />
    <text class="error-text">页面加载失败</text>
    <button @click="retry" class="retry-btn">重试</button>
  </view>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['retry'],
  setup(_, { emit }) {
    const retry = () => {
      emit('retry')
    }
    
    return { retry }
  }
})
</script>

<style scoped>
.error-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.error-text {
  margin: 20px 0;
  color: #f5222d;
  font-size: 18px;
}

.retry-btn {
  margin-top: 10px;
  padding: 0 20px;
}
</style>

6. 完整的主页面更新

<script lang="ts">
import { defineComponent, ref, shallowRef, defineAsyncComponent } from 'vue'
import SideMenu from '@/components/SideMenu.vue'
import { transformRoutesToMenu, updateMenuActiveStatus } from '@/utils/menu-utils'
import type { MenuItem } from '@/types/menu'

export default defineComponent({
  components: { SideMenu },
  setup() {
    const isCollapsed = ref(false)
    const currentView = shallowRef(null)
    const currentPath = ref('')

    // 菜单数据
    const menuData = ref(transformRoutesToMenu([
      // 路由配置...
    ]))

    // 动态加载组件
    const loadComponent = async (path: string) => {
      try {
        let componentPath = path
          .replace(/^\//, '')
          .replace(/\/index$/, '')
        
        if (path.startsWith('/pages/')) {
          componentPath = path.replace('/pages/', '')
        }
        
        currentView.value = defineAsyncComponent({
          loader: () => import(`@/pages/${componentPath}.vue`),
          loadingComponent: defineAsyncComponent(() => 
            import('@/components/Loading.vue')
          ),
          errorComponent: defineAsyncComponent(() => 
            import('@/components/ErrorPage.vue')
          ),
          timeout: 5000
        })
      } catch (error) {
        console.error('组件加载失败:', error)
        currentView.value = defineAsyncComponent(() => 
          import('@/components/ErrorPage.vue')
        )
      }
    }

    // 处理菜单点击
    const handleMenuClick = async (item: MenuItem) => {
      if (!item.path) return
      
      currentPath.value = item.path
      await loadComponent(item.path)
      updateMenuActiveStatus(menuData.value, item.path)
    }

    // 初始化加载默认页面
    onMounted(() => {
      const defaultPath = '/pages/dashboard/index'
      currentPath.value = defaultPath
      loadComponent(defaultPath)
      updateMenuActiveStatus(menuData.value, defaultPath)
    })

    return {
      isCollapsed,
      menuData,
      currentView,
      handleMenuClick
    }
  }
})
</script>

7. 实现优势

  1. 完全动态加载:无需预定义路由组件映射
  2. 代码分割:每个页面自动生成独立 chunk
  3. 错误处理:完善的加载失败处理机制
  4. 路径灵活:支持多种路径格式
  5. 性能优化:异步加载 + 加载状态提示

8. 注意事项

  1. 确保所有页面文件都放在 pages 目录下
  2. 路由路径必须与文件路径匹配
  3. 生产环境部署时需要确保服务器正确返回这些动态页面
  4. 对于复杂项目,建议添加路由白名单验证

这样实现后,你的应用将完全动态地根据菜单路由加载对应的页面组件,无需预先定义组件映射关系,同时保持了良好的用户体验和错误处理能力。