Skip to content

第五章:生产级功能实现

在前几章中,我们深入探讨了 Vue Router 的核心机制,包括路由匹配、导航流程和守卫机制。现在,让我们关注 Vue Router 的生产级功能实现,包括 router-view 组件、router-link 组件、懒加载和滚动行为控制等。

router-view 组件:根据 matched 数组递归渲染组件

router-view 是 Vue Router 的核心组件之一,它根据当前路由的匹配结果动态渲染对应的组件。

如何根据 matched 数组递归渲染组件?

javascript
// 简化的 router-view 实现
const RouterView = {
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  
  setup(props) {
    // 注入路由器实例
    const router = inject('router')
    const route = inject('route')
    
    // 获取当前视图的嵌套深度
    const depth = inject('routerViewDepth', 0)
    
    // 计算当前应该渲染的路由记录
    const matchedRouteRef = computed(() => {
      const matched = route.matched
      return matched[depth]
    })
    
    // 计算应该渲染的组件
    const viewRef = computed(() => {
      const matchedRoute = matchedRouteRef.value
      if (!matchedRoute) {
        return null
      }
      
      // 处理命名视图
      if (matchedRoute.components) {
        return matchedRoute.components[props.name]
      }
      
      // 处理默认视图
      if (props.name === 'default') {
        return matchedRoute.component
      }
      
      return null
    })
    
    // 提供给下一级 router-view 的深度
    provide('routerViewDepth', depth + 1)
    
    return () => {
      const component = viewRef.value
      const route = route.value
      
      if (!component) {
        return h('!--router-view--')
      }
      
      // 渲染组件并传递路由信息
      const attrs = {
        route,
        matchedRoute: matchedRouteRef.value
      }
      
      // 合并传入的属性
      Object.assign(attrs, attrs)
      
      return h(component, attrs)
    }
  }
}

// 更完整的 router-view 实现
const RouterViewImpl = {
  name: 'RouterView',
  props: {
    name: {
      type: String,
      default: 'default'
    },
    route: {
      type: Object,
      default: null
    }
  },
  
  setup(props, { attrs }) {
    const injectedRoute = inject('route')
    const routeToDisplay = computed(() => props.route || injectedRoute)
    
    const depth = inject('routerViewDepth', 0)
    const injectedStore = inject('routerViewStore', null)
    
    // 创建用于跟踪嵌套 router-view 的存储
    const viewStore = injectedStore || {}
    provide('routerViewStore', viewStore)
    
    // 当前视图的唯一标识
    const id = props.name + depth
    
    // 存储当前视图的实例
    const viewRef = ref()
    
    // 计算当前匹配的路由记录
    const matchedRouteRef = computed(() => {
      const matched = routeToDisplay.value.matched
      return matched[depth]
    })
    
    // 计算要渲染的组件
    const componentRef = computed(() => {
      const matchedRoute = matchedRouteRef.value
      if (!matchedRoute) {
        return null
      }
      
      return matchedRoute.components 
        ? matchedRoute.components[props.name]
        : matchedRoute.component
    })
    
    // 更新视图存储
    watch(() => [viewRef.value, matchedRouteRef.value], () => {
      viewStore[id] = {
        component: componentRef.value,
        route: routeToDisplay.value,
        instance: viewRef.value
      }
    }, { immediate: true })
    
    // 提供下一级深度
    provide('routerViewDepth', depth + 1)
    
    return () => {
      const component = componentRef.value
      const route = routeToDisplay.value
      const matchedRoute = matchedRouteRef.value
      
      if (!component) {
        return h('!--router-view--')
      }
      
      // 构建组件属性
      const componentProps = {
        ...attrs,
        route
      }
      
      // 如果组件需要路由信息,传递 matchedRoute
      if (component.props && component.props.matchedRoute) {
        componentProps.matchedRoute = matchedRoute
      }
      
      // 渲染组件
      const componentVNode = h(
        component,
        componentProps,
        attrs.default
          ? { default: attrs.default }
          : null
      )
      
      // 如果需要过渡效果,包装在 Transition 中
      if (matchedRoute && matchedRoute.meta && matchedRoute.meta.transition) {
        return h(
          Transition,
          { name: matchedRoute.meta.transition },
          () => componentVNode
        )
      }
      
      return componentVNode
    }
  }
}

name 属性:支持命名视图

命名视图允许在同一路由下渲染多个组件:

javascript
// 命名视图配置示例
const routes = [
  {
    path: '/',
    components: {
      default: Home,
      sidebar: Sidebar,
      footer: Footer
    }
  },
  {
    path: '/dashboard',
    component: Dashboard,
    children: [
      {
        path: 'stats',
        components: {
          default: Stats,
          sidebar: DashboardSidebar
        }
      }
    ]
  }
]

// 模板中使用命名视图
/*
<template>
  <div>
    <router-view name="sidebar" />
    <div class="main-content">
      <router-view />
    </div>
    <router-view name="footer" />
  </div>
</template>
*/

route 注入:让子组件访问 $route

Vue Router 通过依赖注入机制让子组件可以访问路由信息:

javascript
// 在路由器安装时提供路由信息
function installRouter(app, router) {
  app.provide('router', router)
  app.provide('route', router.currentRoute)
  
  // 全局属性
  Object.defineProperty(app.config.globalProperties, '$router', {
    get: () => router
  })
  
  Object.defineProperty(app.config.globalProperties, '$route', {
    get: () => router.currentRoute.value
  })
}

// 在组件中使用
export default {
  setup() {
    const route = inject('route')
    const router = inject('router')
    
    // 访问路由参数
    console.log(route.params)
    console.log(route.query)
    
    // 导航
    const goToUser = (userId) => {
      router.push(`/user/${userId}`)
    }
    
    return { route, goToUser }
  }
}

缓存与激活:<keep-alive> 如何与 router-view 协同?

Vue Router 与 Vue 的 <keep-alive> 组件可以很好地协同工作:

javascript
// keep-alive 与 router-view 结合使用
/*
<template>
  <div id="app">
    <keep-alive :include="cachedViews">
      <router-view :key="$route.fullPath" />
    </keep-alive>
  </div>
</template>
*/

export default {
  setup() {
    const cachedViews = ref([])
    
    // 根据路由元信息决定是否缓存
    const updateCachedViews = (route) => {
      if (route.meta.keepAlive) {
        const componentName = route.matched[route.matched.length - 1].components.default.name
        if (componentName && !cachedViews.value.includes(componentName)) {
          cachedViews.value.push(componentName)
        }
      }
    }
    
    // 监听路由变化
    watch(
      () => route.value,
      (newRoute) => {
        updateCachedViews(newRoute)
      },
      { immediate: true }
    )
    
    return { cachedViews }
  }
}

router-link 是 Vue Router 提供的声明式导航组件,它比直接使用 <a> 标签更智能。

to 属性:stringLocation 对象

javascript
// router-link 实现
const RouterLink = {
  name: 'RouterLink',
  props: {
    to: {
      type: [String, Object],
      required: true
    },
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    custom: Boolean,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    }
  },
  
  setup(props, { slots, attrs }) {
    const router = inject('router')
    const currentRoute = inject('route')
    
    // 解析目标位置
    const linkRoute = computed(() => {
      return router.resolve(props.to)
    })
    
    // 检查是否激活
    const isActive = computed(() => {
      return isSameRouteLocation(currentRoute.value, linkRoute.value)
    })
    
    // 检查是否精确激活
    const isExactActive = computed(() => {
      return isSameRouteLocation(currentRoute.value, linkRoute.value, true)
    })
    
    // 获取激活类名
    const activeClass = props.activeClass || router.options.linkActiveClass || 'router-link-active'
    const exactActiveClass = props.exactActiveClass || router.options.linkExactActiveClass || 'router-link-exact-active'
    
    // 导航函数
    const navigate = (e) => {
      // 处理修饰键
      if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
        return
      }
      
      // 处理右键点击
      if (e.button !== undefined && e.button !== 0) {
        return
      }
      
      // 处理 target="_blank"
      if (attrs.target && attrs.target === '_blank') {
        return
      }
      
      e.preventDefault()
      
      // 执行导航
      if (props.replace) {
        router.replace(props.to)
      } else {
        router.push(props.to)
      }
    }
    
    return () => {
      const children = slots.default && slots.default({
        route: linkRoute.value,
        href: linkRoute.value.href,
        isActive: isActive.value,
        isExactActive: isExactActive.value,
        navigate
      })
      
      if (props.custom) {
        return children
      }
      
      const linkAttrs = {
        ...attrs,
        href: linkRoute.value.href,
        onClick: navigate
      }
      
      // 添加激活类名
      if (isExactActive.value) {
        linkAttrs.class = [linkAttrs.class, exactActiveClass, activeClass].filter(Boolean).join(' ')
      } else if (isActive.value) {
        linkAttrs.class = [linkAttrs.class, activeClass].filter(Boolean).join(' ')
      }
      
      // 设置 aria-current 属性
      if (isExactActive.value) {
        linkAttrs['aria-current'] = props.ariaCurrentValue
      }
      
      return h('a', linkAttrs, children)
    }
  }
}

active-class:如何判断当前链接是否激活?

javascript
// 路由激活判断实现
function isSameRouteLocation(from, to, exact = false) {
  if (from.name !== to.name) {
    return false
  }
  
  if (from.path !== to.path) {
    return false
  }
  
  if (exact) {
    // 精确匹配需要参数和查询完全相同
    return isSameRouteLocationParams(from.params, to.params) &&
           isSameRouteLocationQuery(from.query, to.query)
  }
  
  // 非精确匹配只需要当前路由包含目标路由
  return isSameRouteLocationParams(from.params, to.params, true) &&
         isSameRouteLocationQuery(from.query, to.query, true)
}

function isSameRouteLocationParams(from, to, partial = false) {
  const fromKeys = Object.keys(from)
  const toKeys = Object.keys(to)
  
  if (partial) {
    // 部分匹配:to 的所有键都必须在 from 中有相同值
    return toKeys.every(key => from[key] === to[key])
  } else {
    // 完全匹配:两个对象必须有相同的键和值
    return fromKeys.length === toKeys.length &&
           fromKeys.every(key => from[key] === to[key])
  }
}

function isSameRouteLocationQuery(from, to, partial = false) {
  const fromKeys = Object.keys(from)
  const toKeys = Object.keys(to)
  
  if (partial) {
    return toKeys.every(key => {
      const fromValue = from[key]
      const toValue = to[key]
      
      if (Array.isArray(fromValue) && Array.isArray(toValue)) {
        return fromValue.length === toValue.length &&
               fromValue.every((val, i) => val === toValue[i])
      }
      
      return fromValue === toValue
    })
  } else {
    return fromKeys.length === toKeys.length &&
           fromKeys.every(key => {
             const fromValue = from[key]
             const toValue = to[key]
             
             if (Array.isArray(fromValue) && Array.isArray(toValue)) {
               return fromValue.length === toValue.length &&
                      fromValue.every((val, i) => val === toValue[i])
             }
             
             return fromValue === toValue
           })
  }
}

exact 匹配:完全匹配 vs 前缀匹配

javascript
// exact 匹配示例
/*
<!-- 精确匹配 -->
<router-link to="/" exact>Home</router-link>

<!-- 前缀匹配 -->
<router-link to="/user">User</router-link>
*/

// 当前路由: /user/profile
// / 的链接不会激活(精确匹配)
// /user 的链接会激活(前缀匹配)

懒加载(Lazy Loading):代码分割与按需加载

懒加载是现代前端应用优化的重要手段,Vue Router 通过动态导入支持组件的懒加载。

component: () => import('./Home.vue')

javascript
// 懒加载路由配置
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./views/About.vue')
  },
  {
    path: '/user/:id',
    component: () => import('./views/User.vue')
  }
]

// 懒加载实现原理
function lazyLoadComponent(importer) {
  let resolvedComponent = null
  
  return () => {
    // 如果组件已加载,直接返回
    if (resolvedComponent) {
      return resolvedComponent
    }
    
    // 如果正在加载,返回加载状态
    if (resolvedComponent === null) {
      resolvedComponent = importer().then(module => {
        resolvedComponent = module.default || module
        return resolvedComponent
      })
    }
    
    return resolvedComponent
  }
}

// 使用示例
const routes = [
  {
    path: '/home',
    component: lazyLoadComponent(() => import('./Home.vue'))
  }
]

代码分割:Webpack/Vite 如何生成独立 chunk?

现代构建工具会自动处理动态导入的代码分割:

javascript
// Webpack 魔法注释
const routes = [
  {
    path: '/user',
    // 命名 chunk
    component: () => import(/* webpackChunkName: "user" */ './views/User.vue')
  },
  {
    path: '/admin',
    // 预加载
    component: () => import(/* webpackPreload: true */ './views/Admin.vue')
  },
  {
    path: '/reports',
    // 预获取
    component: () => import(/* webpackPrefetch: true */ './views/Reports.vue')
  }
]

// Vite 中的动态导入
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

加载状态:如何显示 loading 组件?

javascript
// 使用 defineAsyncComponent 显示加载状态
import { defineAsyncComponent } from 'vue'

const AsyncUserComponent = defineAsyncComponent({
  loader: () => import('./User.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200, // 延迟显示加载组件
  timeout: 3000, // 超时时间
  suspensible: false, // 是否可被 Suspense 处理
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      // 重试之前延迟
      setTimeout(retry, 1000)
    } else {
      fail()
    }
  }
})

const routes = [
  {
    path: '/user/:id',
    component: AsyncUserComponent
  }
]

错误处理:chunk 加载失败如何降级?

javascript
// chunk 加载错误处理
function createAsyncComponent(importer, options = {}) {
  return defineAsyncComponent({
    loader: () => importer().catch(error => {
      console.error('Failed to load component:', error)
      
      // 如果是 chunk 加载错误,尝试重新加载
      if (error.name === 'ChunkLoadError') {
        // 可以尝试重新加载或显示错误页面
        return import('./components/ErrorFallback.vue')
      }
      
      throw error
    }),
    ...options
  })
}

// 使用示例
const routes = [
  {
    path: '/user/:id',
    component: createAsyncComponent(
      () => import('./views/User.vue'),
      {
        loadingComponent: () => h('div', 'Loading...'),
        errorComponent: () => h('div', 'Failed to load user component'),
        delay: 200,
        timeout: 5000
      }
    )
  }
]

scrollBehavior:控制页面滚动位置

Vue Router 提供了 scrollBehavior 选项来控制路由导航时的滚动行为。

scrollBehavior(to, from, savedPosition):控制滚动位置

javascript
// scrollBehavior 实现
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置(浏览器前进/后退)
    if (savedPosition) {
      return savedPosition
    }
    
    // 如果路由有 hash
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    
    // 默认滚动到顶部
    return { top: 0 }
  }
})

// 自定义 scrollBehavior 实现
function createScrollBehavior(options) {
  return function scrollBehavior(to, from, savedPosition) {
    // 等待页面渲染完成
    return new Promise((resolve) => {
      setTimeout(() => {
        let position = {}
        
        // 处理保存的位置
        if (savedPosition) {
          position = savedPosition
        } else if (to.hash) {
          // 处理 hash 定位
          position = {
            el: to.hash,
            behavior: 'smooth'
          }
        } else {
          // 默认行为
          position = options.defaultPosition || { top: 0 }
        }
        
        // 处理路由元信息
        if (to.meta.scrollToTop === false) {
          // 不滚动
          position = false
        } else if (to.meta.scrollTop) {
          // 滚动到指定位置
          position = { top: to.meta.scrollTop }
        }
        
        resolve(position)
      }, options.delay || 0)
    })
  }
}

savedPosition:浏览器前进/后退时的恢复位置

javascript
// savedPosition 使用示例
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 恢复滚动位置
    if (savedPosition) {
      return savedPosition
    }
    
    // 滚动到元素
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    
    // 默认滚动到顶部
    return { top: 0 }
  }
})

el 选择器:#idref 如何定位元素?

javascript
// scrollBehavior 中的元素定位
scrollBehavior(to, from, savedPosition) {
  if (to.hash) {
    // 使用 CSS 选择器
    return {
      el: to.hash, // 例如: #section1
      behavior: 'smooth'
    }
  }
  
  // 使用自定义选择器
  if (to.meta.scrollTo) {
    return {
      el: to.meta.scrollTo,
      behavior: 'smooth'
    }
  }
  
  return { top: 0 }
}

// 路由配置中使用
const routes = [
  {
    path: '/guide',
    component: Guide,
    meta: {
      scrollTo: '.guide-content'
    }
  }
]

小结

在本章中,我们深入探讨了 Vue Router 的生产级功能实现:

  1. router-view 组件 - 根据匹配结果动态渲染组件,支持嵌套和命名视图
  2. router-link 组件 - 智能路由链接,支持激活状态和自定义渲染
  3. 懒加载 - 通过动态导入实现代码分割,支持加载状态和错误处理
  4. scrollBehavior - 控制路由导航时的滚动行为

这些功能使得 Vue Router 成为一个功能完整、性能优秀的路由解决方案。

在下一章中,我们将探讨 Vue Router 在 SSR 环境中的应用,包括内存历史记录和状态同步。


思考题

  1. 你在项目中如何优化 router-view 的性能,特别是在大型嵌套路由结构中?
  2. 对于懒加载组件的错误处理,你通常采用什么策略来确保用户体验?