Vue Router 基础
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:
- 嵌套路由映射
- 动态路由选择
- 模块化、基于组件的路由配置
- 路由参数、查询、通配符
- 展示由 Vue.js 的过渡系统提供的过渡效果
- 细致的导航控制
- 自动激活 CSS 类的链接
- HTML5 history 模式或 hash 模式
- 可定制的滚动行为
- URL 的正确编码
详细内容请查阅官网 -> 官网链接
从发展趋势来看,后续的前端路由都会往函数式的编程方式发展(如V4版本的useRouter
, useRoute
)。不管是React Router
还是 Vue Router
,也都趋向于Hooks
的使用。
路由的目的:将我们的组件映射到路由上,让 Vue Router
知道在哪里渲染它们。
安装
- v4
npm install vue-router@4
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件进行导航 -->
<!-- 通过传递 `to` 来指定链接 -->
<!-- `<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签 -->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
- v3
npm install vue-router
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
router-link
类似a
标签,这使得Vue Router
可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码;router-view
将显示与 url 对应的组件;
使用
- v4
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 1.导入路由组件
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
// 2. 创建路由实例并传递 `routes` 配置
const router = createRouter({
history: createWebHistory(),
// 3.定义路由
routes: [
{
path: '/',
component: HomeView
},
{
path: '/about',
component: AboutView
}
]
})
export default router
// main.js
import { createApp } from "vue"
// 引入App组件
import App from "./App.vue"
// 引入路由实例
import router from "./route"
// 创建并挂载根实例
const app = createApp(app)
// 确保整个应用支持路由,此步骤添加了全局组件 router-view、router-link
app.use(router)
app.mount(app)
app.vue
<template>
<header>
<div class="wrapper">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
- v3
// router/index.js
import VueRouter from 'vue-router'
// 1.导入路由组件
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
// 2. 创建路由实例并传递 `routes` 配置
const router = new VueRouter({
// 3.定义路由
routes: [
{
path: '/',
component: HomeView
},
{
path: '/about',
component: AboutView
}
]
})
export default router
// main.js
import Vue from "vue"
// 引入App组件
import App from "./App.vue"
// 引入路由实例
import router from "./route"
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
new Vue({
router,
render: h => h(App)
}).$mount('#app')
访问路由
- v4
import { useRouter,useRoute } from "vue-router"
const router = useRouter()
const route = useRoute()
const toAbout = ()=>{
router.push("/about")
}
console.log(route.query)
- v3
this.$router.push("/about")
this.$route.query
router和route的区别
router:路由管理对象,管理路由
route:当前路由对象
动态路由
- 路由配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 1.导入路由组件
import HomeView from '../views/HomeView.vue'
import UserView from '../views/UserView.vue'
// 2. 创建路由实例并传递 `routes` 配置
const router = createRouter({
history: createWebHistory(),
// 3.定义路由
routes: [
{
path: '/',
component: HomeView
},
{
// 动态字段以冒号开始
path: '/user/:id',
component: UserView
}
]
})
export default router
- 获取params
路径参数 用冒号
:
表示。当一个路由被匹配时,它的 params 的值将在每个组件中以this.$route.params
的形式暴露出来。因此,我们可以通过更新User
的模板来呈现当前的用户 ID:
const User = {
template: '<div>User {{ $route.params.id }}</div>',
}
同一个路由可设置多个路径参数:
匹配模式 | 匹配路径 | $route.params |
---|---|---|
/users/:username | /users/eduardo | { username: 'eduardo' } |
/users/:username/posts/:postId | /users/eduardo/posts/123 | { username: 'eduardo', postId: '123' } |
相应路由参数变化
当用户从 /users/johnny
导航到 /users/jolyne
时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。
要对同一个组件中参数的变化做出响应的话,你可以简单地 watch监听 $route
对象上的任意属性:
created() {
this.$watch(
() => this.$route.params,
(toParams, previousParams) => {
// 对路由变化做出响应...
}
)
},
或者使用路由导航守卫 beforeRouteUpdate
:(导航守卫后面讲)
async beforeRouteUpdate(to, from,next) {
// 对路由变化做出响应...
this.userData = await fetchUser(to.params.id)
next()
},
参数中自定义正则
当定义像 :userId
这样的参数时,我们内部使用以下的正则 ([^/]+)
(至少一个不是斜杠 /
的字符)来从 URL 中提取参数。
const routes = [
// /:orderId -> 仅匹配数字
{ path: '/:orderId(\\d+)' },
// /:productName -> 匹配其他任何内容
{ path: '/:productName' },
]
可重复参数
如果你需要匹配具有多个部分的路由,如 /first/second/third
,你应该用 *
(0 个或多个)和 +
(1 个或多个)将参数标记为可重复:
const routes = [
// /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
{ path: '/:chapters+' },
// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
{ path: '/:chapters*' },
]
// 这些也可以通过在右括号后添加它们与自定义正则结合使用:
const routes = [
// 仅匹配数字
// 匹配 /1, /1/2, 等
{ path: '/:chapters(\\d+)+' },
// 匹配 /, /1, /1/2, 等
{ path: '/:chapters(\\d+)*' },
]
这将为你提供一个参数数组,而不是一个字符串,并且在使用命名路由时也需要你传递一个数组:
// 给定 { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 产生 /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// 产生 /a/b
// 给定 { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 抛出错误,因为 `chapters` 为空
Sensitive 与 strict 路由配置
默认情况下,所有路由是不区分大小写的 , 并且能匹配带有或不带有尾部斜线的路由
- strict: true 不允许尾部斜线
- sensitive: true 严格区分大小写
const router = createRouter({
history: createWebHistory(),
routes: [
// 将匹配 /users/posva 而非:
// - /users/posva/ 当 strict: true
// - /Users/posva 当 sensitive: true
{ path: '/users/:id', sensitive: true },
// 将匹配 /users, /Users, 以及 /users/42 而非 /users/ 或 /users/42/
{ path: '/users/:id?' },
],
strict: true, // applies to all routes
})
可选参数
你也可以通过使用 ?
修饰符(0 个或 1 个)将一个参数标记为可选:
请注意,*
在技术上也标志着一个参数是可选的,但 ?
参数不能重复。
const routes = [
// 匹配 /users 和 /users/posva
{ path: '/users/:userId?' },
// 匹配 /users 和 /users/42
{ path: '/users/:userId(\\d+)?' },
]
嵌套路由
通过 Vue Router 来表达多层嵌套 的组件结构
注意,以 /
开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的 URL。
所以嵌套子路由不需要使用 / 开头
const routes = [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功
{
path: '',
component: UserDefault,
},
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]
// 父文件
<div id="app">
<router-view></router-view>
</div>
// 子文件
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
嵌套路由命名
const routes = [
{
path: '/user/:id',
component: User,
// 请注意,只有子路由具有名称
children: [{ path: '', name: 'user', component: UserHome }],
},
]
在一些场景中,你可能希望导航到命名路由而不导航到嵌套路由。例如,你想导航 /user/:id
而不显示嵌套路由。那样的话,你还可以命名父路由,但请注意重新加载页面将始终显示嵌套的子路由,因为它被视为指向路径/users/:id
的导航,而不是命名路由:
const routes = [
{
path: '/user/:id',
name: 'user-parent',
component: User,
children: [{ path: '', name: 'user', component: UserHome }],
},
]
编程式导航
push
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
在组件内部,你可以使用 $router
属性访问路由,例如 this.$router.push(...)
。如果使用组合式 API,你可以通过调用 useRouter()
来访问路由器。
router.push
可以接收 字符串 和 对象
let router = useRouter()
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
如果提供了 path
,params
会被忽略 ,所以需要以下写法:
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
由于属性 to
与 router.push
接受的对象种类相同,所以两者的规则完全相同。
router.push
和所有其他导航方法都会返回一个 Promise,让我们可以等到导航完成后才知道是成功还是失败。
replace
它的作用类似于 router.push
,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。
声明式 | 编程式 |
---|---|
<router-link :to="..." replace> | router.replace(...) |
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
push 和 replace 区别:
push :导航时会向history中添加路由记录
replace :不会向history中添加路由记录
go
横跨历史 router.go()
该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)
。
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
命名路由
为路由配置 name ,有以下优点:
- 没有硬编码的 URL
params
的自动编码/解码。- 防止你在 url 中出现打字错误。
- 绕过路径排序(如显示一个)
const routes = [
{
path: '/user/:username',
name: 'user',
component: User,
},
]
使用 name 实现路径导航:
// to
<router-link :to="{ name: 'user', params: { username: 'erina' }}">
User
</router-link>
// router.push
router.push({ name: 'user', params: { username: 'erina' } })
注意: 所有路由的命名都必须是唯一的。如果为多条路由添加相同的命名,路由器只会保留最后那一条。
命名视图
一个路由同时 (同级) 展示多个视图
你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
。
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
路由表配置:此时使用 components ,为多个组件命名。 一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
重定向 & 别名
重定向
从 /home
重定向到 /
:
const routes = [{ path: '/home', redirect: '/' }]
重定向的目标也可以是一个命名的路由:
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
甚至是一个方法,动态返回重定向目标:
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
请注意,导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在上面的例子中,在 /home
路由中添加 beforeEnter
守卫不会有任何效果。
在写 redirect
的时候,可以省略 component
配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是嵌套路由:如果一个路由记录有 children
和 redirect
属性,它也应该有 component
属性。
相对重定向
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
// 相对位置不以`/`开头
// 或 { path: 'profile'}
return 'profile'
},
},
]
别名
配置:
const routes = [{ path: '/', component: Homepage, alias: '/home' }]
上方 当用户访问 /home
时,URL 仍然是 /home
,但会被匹配为用户正在访问 /
的组件。
多个别名:
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// 为这 3 个 URL 呈现 UserList
// - /users
// - /users/list
// - /people
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]
动态路由别名:
const routes = [
{
path: '/users/:id',
component: UsersByIdLayout,
children: [
// 为这 3 个 URL 呈现 UserDetails
// - /users/24
// - /users/24/profile
// - /24
{ path: 'profile', component: UserDetails, alias: ['/:id', ''] },
],
},
]
路由组件传参
将 props 传递给路由组件
封装组件时使用 $route
会与路由紧密耦合,这限制了组件的灵活性,因为它只能用于特定的 URL。虽然这不一定是件坏事,但我们可以通过 props
配置来解除这种行为:
// 将
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]
// 改为:
const User = {
// 请确保添加一个与路由参数完全相同的 prop 名
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]
这允许你在任何地方使用该组件,使得该组件更容易重用和测试。
布尔模式
当 props
设置为 true
时,route.params
将被设置为组件的 props。
对象模式
const routes = [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
// props 中可接收到 newsletterPopup: false
函数模式
const routes = [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
URL /search?q=vue
将传递 {query: 'vue'}
作为 props 传给 SearchUser
组件。
请尽可能保持 props
函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 vue 才可以对状态变化做出反应。
命名视图
命名视图需要单独为每个视图定义props配置
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
通过 RouterView
你还可以通过插槽传递任意参数:
<RouterView v-slot="{ Component }">
<component
:is="Component"
view-prop="value"
/>
</RouterView>
// 在这种情况下,所有视图组件都会接收到 view-prop。通常这并不是一个好主意,因为这意味着所有的视图组件都声明了一个 view-prop prop,但这未必需要。所以请尽可能使用上述的其他选项。
不同的历史模式
Hash 模式
hash 模式是用 createWebHashHistory()
创建的:
const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})
它在内部传递的实际 URL 之前使用了一个哈希字符(#
)。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。如果你担心这个问题,可以使用 HTML5 模式。
Memory 模式
Memory 模式不会假定自己处于浏览器环境,因此不会与 URL 交互也不会自动触发初始导航。这使得它非常适合 Node 环境和 SSR。它是用 createMemoryHistory()
创建的,并且需要你在调用 app.use(router)
之后手动 push 到初始导航。
它不会有历史记录,这意味着你无法后退或前进。
import { createRouter, createMemoryHistory } from 'vue-router'
const router = createRouter({
history: createMemoryHistory(),
routes: [
//...
],
})
HTML5 模式
用 createWebHistory()
创建 HTML5 模式, 当使用这种历史模式时,URL 会看起来很 “正常”,例如 https://example.com/user/id
。 由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id
,就会得到一个 404 错误。这就尴尬了。 要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html
相同的页面。
服务器配置示例
注意:以下示例假定你正在从根目录提供服务。如果你部署到子目录,你应该使用Vue CLI 的 publicPath
配置和相关的路由器的 base
属性。你还需要调整下面的例子,以使用子目录而不是根目录(例如,将RewriteBase/
替换为 RewriteBase/name-of-your-subfolder/
)。
- Apache
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
也可以使用 FallbackResource
代替 mod_rewrite
。
- nginx
location / {
try_files $uri $uri/ /index.html;
}
- 原生 Node.js
const http = require('http')
const fs = require('fs')
const httpPort = 80
http
.createServer((req, res) => {
fs.readFile('index.html', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.html" file.')
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
})
res.end(content)
})
})
.listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
404
这有一个注意事项。你的服务器将不再报告 404 错误,因为现在所有未找到的路径都会显示你的 index.html
文件。为了解决这个问题,你应该在你的 Vue 应用程序中实现一个万能的路由来显示 404 页面。
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:pathMatch(.*)', component: NotFoundComponent }],
})