Vue3$View-VueRouter
0. Slot and Router
Slot
如果我们想在组件的某个地方插入某些内容,我们可以通过插槽 slot 来实现:
- where:
<slot></slot>定义的地方 (provider) - what:组件标签内部的内容 (consumer)
Router
如果我们想在组件的某个地方插入某些内容,我们也可以通过路由 vue-router 来实现:
- where:
<RouterView>定义的地方 (consumer) - what:路由对应的组件 (provider)
- when:路由改变的时候 / 调用 router 方法的时候 (consumer)
P. 安装
单独安装:
npm install vue-router@4
脚手架安装:
npm create vue@latest
1. 配置路由器
路由器参数
const router = createRouter({
history: createWebHistory(), // createWebHashHistory() | createMemoryHistory()
routes,
linkActiveClass: 'border-indigo-500',
linkExactActiveClass: 'border-indigo-700',
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
路由器参数介绍
history: createWebHistory(), // createWebHashHistory() | createMemoryHistory() // 需要后端配置
// 只考虑 path 和 params。
// 考虑 alias,不考虑 redirect。
linkActiveClass: 'border-indigo-500',
linkExactActiveClass: 'border-indigo-700',
scrollBehavior (to, from, savedPosition) {
// return desired position, if {} or falsy value => no scrolling
// always scroll to top
return { top: 0 }
// always scroll 10px above the element #main
return {
// could also be
// el: document.getElementById('main'),
el: '#main',
// 10px above the element
top: 10,
}
// savedPosition
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
// to anchor
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
// delay (for animation...)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ left: 0, top: 0 })
}, 500)
})
}
注册路由器
const app = createApp(App)
app.use(router)
处理导航故障
导航失败的原因:
- 导航守卫
return false - 导航守卫重定向
- 新的导航守卫替换了之前的
- 已经在所导航的位置
- 导航守卫 throw
Error
检测导航故障
router.push() 返回的是 Promise:
- 导航被阻止:Navigation Failure (带有额外属性的
Error) - 正常:falsy(一般是
undefined)
const navigationResult = await router.push('/my-profile')
if (navigationResult) {
// 导航被阻止
} else {
// 导航成功 (包括重新导航的情况)
this.isMenuOpen = false
}
全局导航故障
router.afterEach((to, from, failure) => {
if (failure) {
sendToAnalytics(to, from, failure)
}
})
鉴别导航故障
故障分为3类(throw error 不在其中,通过 router.onError() 处理):
aborted:在导航守卫中return falsecancelled:去了新的导航duplicated:导航被阻止,已经在目标位置了
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
// 给用户显示一个小通知
showToast('You have unsaved changes, discard and leave anyway?')
}
导航故障的属性
// 正在尝试访问 admin 页面
router.push('/admin').then(failure => {
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
failure.to.path // '/admin'
failure.from.path // '/'
}
})
检测重定向
导航守卫中返回一个新的位置
await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
// redirectedFrom 是解析出的路由地址,就像导航守卫中的 to 和 from
}
2. 配置路由
路由参数
const route = {
path: '/users/:id?',
name: 'user',
component: User,
meta: {
requiresAuth: true
},
redirect: '/customers/:id', // alias
strict: true, // sensitive
props: true, // boolean | object | function
beforeEnter(to, from){},
children: [{}]
}
路由参数介绍
path: '/users/:id?',
// $route.params.id
// id 改变是组件不变,可 watch / onBeforeRouteUpdate
// :id, :id?, :id*, :id+, id(\\d)?
// /:pathMatch(.*)* => match all, under `route.params.pathMatch`
// /user-:afteruser(.*) => match all starting with 'user-', under `route.params.afterUser`
name: 'user',
// 不能重复,否则:No match found for location with path...
// 可以和 param 一起使用, 和 path 不同
component: User,
// 静态 / 动态 () => import()
// webpack: [named chunks](https://webpack.js.org/guides/code-splitting/#dynamic-imports)
// () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
// vite: [rollupOptions](https://vitejs.dev/config/#build-rollupoptions)
// components: { name: component, name2: cmp2 }
meta: {
requiresAuth: true
},
// route.meta 会包含从父到子的并集,以子优先
// 官网中的例子有问题,下面两个并不同。如果子 true 父 false,第一个是 true,第二个是 false
// to.matched.some(record => record.meta.requiresAuth)
// if (to.meta.requiresAuth && !auth.isLoggedIn())
redirect: '/customers/:id', // alias
// redirect: Navigation Guards only apply to its target
// redirect: path can be relative
// alias: String | Array<String>
strict: true, // sensitive
// route / router level
props: true, // boolean | object | function
// Boolean Mode: component should define a prop whose name is the same with param
// Boolean with named views: props: { default: true, sidebar: false }
// Object Mode: static values
// Function Mode: props: route => ({ query: route.query.q })
// Via RouterView see RouterView
beforeEnter(to, from){},
children: [{}]
/* path:
if not starting with '/' => relative
can be '' // reload will show '' child component
*/
// component: can ignore (4.1+)
Routes' Matching Syntax
:userId=>([^/]+)/:orderId(\\d+),/:productName// orderId: only numbers, productName: anything else- Repeatable params:
/:chapters+: matches /one, /one/two, /one/two/three, etc./:chapters*: matches /, /one, /one/two, /one/two/three, etc./:chapters(\\d+)+: matches /1, /1/2, etc.
// given { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// produces /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// produces /a/b
// given { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// throws an Error because `chapters` is empty
Match all with pathMatch
// Match all with pathMatch
router.push({
name: 'NotFound',
// preserve current path and remove the first char to avoid the target URL starting with `//`
params: { pathMatch: route.path.substring(1).split('/') },
// preserve existing query and hash if any
query: route.query,
hash: route.hash,
})
vite rollupOptions for dynamic imports
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
// https://rollupjs.org/guide/en/#outputmanualchunks
output: {
manualChunks: {
'group-user': [
'./src/UserDetails',
'./src/UserDashboard',
'./src/UserProfileEdit',
],
},
},
},
},
})
动态路由
有时候我们需要动态路由。虽然说是动态路由,也需要提前准备好。我们之后可以通过配置来筛选这些配置好的路由。
添加路由
// router.addRoute()
router.addRoute({ path: '/about', component: About })
// 需要手动跳转
router.replace(router.currentRoute.value.fullPath)
// 在导航守卫中不使用 router.replace,而是直接返回要跳转的地址
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
删除路由
- 替换路由:通过添加相同名称的路由,会替换之前的路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// this will remove the previously added route because they have
// the same name and names are unique across all routes
router.addRoute({ path: '/other', name: 'about', component: Other })
- 调用添加路由返回的函数:
const removeRoute = router.addRoute(routeRecord)
removeRoute() // removes the route if it exists
router.removeRoute(name):
router.addRoute({ path: '/about', name: 'about', component: About })
// remove the route
router.removeRoute('about')
查看已存在的路由
router.hasRoute()router.getRoutes()
3. 导航守卫 Navigation Guards
路由守卫分为两类三种:
- 全局路由守卫
- 单个路由守卫
- 路由独享守卫
- 组件内守卫
3.1 Global Guards
1. Global Before Guards
Global before guards are called in creation order, whenever a navigation is triggered.
Guards may be resolved asynchronously, and the navigation is considered pending before all hooks have been resolved.
return value:
false: cancel- route location
- throw error:
router.onError()callbacks will be called
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// explicitly return false to cancel the navigation
return false
})
2. Global Resolve Guards
Global resolve guards triggers on every navigation, but are called right before the navigation is confirmed, after all in-component guards and async route components are resolved.
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... handle the error and then cancel the navigation
return false
} else {
// unexpected error, cancel the navigation and pass the error to the global handler
throw error
}
}
}
})
3. Global After Hooks
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
Global injections within guards
// main.ts
const app = createApp(App)
app.provide('global', 'hello injections')
// router.ts or main.ts
router.beforeEach((to, from) => {
const global = inject('global') // 'hello injections'
// a pinia store
const userStore = useAuthStore()
// ...
})
3.2 Per-Route Guards
beforeEnter guards only trigger when entering the route, navigating from a different route.
beforeEnter: Function | Array<Function>
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
3.3 In-Component Guards
Options API:
beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
Composition API: can be used by components in <RouterView/>
onBeforeRouteUpdateonBeforeRouteLeave
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
const userData = ref()
// same as beforeRouteUpdate option but with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
// same as beforeRouteLeave option but with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
</script>
3.x The Full Navigation Resolution Flow
- Navigation triggered.
- Call
beforeRouteLeaveguards in deactivated components. - Call global
beforeEachguards. - Call
beforeRouteUpdateguards in reused components. - Call
beforeEnterin route configs. - Resolve async route components.
- Call
beforeRouteEnterin activated components. - Call global
beforeResolveguards. - Navigation is confirmed.
- Call global
afterEachhooks. - DOM updates triggered.
- Call callbacks passed to
nextinbeforeRouteEnterguards with instantiated instances.
X. 使用路由
引入
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
0. 使用路由的步骤:
- 触发路由变化
- 使用
<RouterLink>标签 - 使用
router.push()/router.replace()/router.go()
- 使用
- 显示对应的组件:
<RouterView>
1.1 RouterLink
<RouterLink
activeClass="border-indigo-500"
exactActiveClass="border-indigo-700"
to="/"
>
1.2 编程式路由导航 router
router.push() / router.replace() 返回 Promise。
router.push('/users/eduardo')
router.push({ path: '/users/eduardo' })
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })
// params will be ignored with path
// params value should be String / Nubmer / Array<String | Nubmer>
// optional params: use '' or null to remove
// /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// /about#team
router.push({ path: '/about', hash: '#team' })
router.push({ path: '/home', replace: true })
// equivalent to
router.replace({ path: '/home' })
router.go()
// router.forward()
router.go(1)
// router.back()
router.go(-1)
// go forward by 3 records
router.go(3)
// fails silently if there aren't that many records
router.go(-100)
router.go(100)
2. <RouterView>
2.0 基本使用
<RouterView />
2.1 Named Views
<router-view class="view left-sidebar" name="LeftSidebar" />
<router-view class="view main-content" /> <!-- default -->
<router-view class="view right-sidebar" name="RightSidebar" />
{
path: '/',
components: {
default: Home,
// they match the `name` attribute on `<router-view>`
LeftSidebar,
RightSidebar,
},
},
2.2 RouterView slot
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
KeepAlive & Transition
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'"> <!-- 在 meta 里设置 -->
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
router.afterEach((to, from) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
to.meta.transition = toDepth < fromDepth ? 'slide-right' : 'slide-left'
})
Passing props and slots
<router-view v-slot="{ Component }">
<component :is="Component" some-prop="a value">
<p>Some slotted content</p>
</component>
</router-view>
Template refs
<router-view v-slot="{ Component }">
<component :is="Component" ref="mainContent" />
</router-view>