第五章:生产级功能实现
在前几章中,我们深入探讨了 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 组件:智能路由链接
router-link 是 Vue Router 提供的声明式导航组件,它比直接使用 <a> 标签更智能。
to 属性:string 或 Location 对象
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 选择器:#id 或 ref 如何定位元素?
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 的生产级功能实现:
router-view组件 - 根据匹配结果动态渲染组件,支持嵌套和命名视图router-link组件 - 智能路由链接,支持激活状态和自定义渲染- 懒加载 - 通过动态导入实现代码分割,支持加载状态和错误处理
scrollBehavior- 控制路由导航时的滚动行为
这些功能使得 Vue Router 成为一个功能完整、性能优秀的路由解决方案。
在下一章中,我们将探讨 Vue Router 在 SSR 环境中的应用,包括内存历史记录和状态同步。
思考题:
- 你在项目中如何优化
router-view的性能,特别是在大型嵌套路由结构中? - 对于懒加载组件的错误处理,你通常采用什么策略来确保用户体验?