Vue 3 路由与懒加载入门:从零完成一个迷你学习中心
📘 本文简介
本文通过一个非常简单的"迷你学习中心"项目,从零学习 Vue Router 的基本用法,包括路由配置、页面切换、RouterLink、RouterView、父子路由、重定向和路由懒加载。完成后,可以看懂项目中常见的 component: () => import(...) 写法。
1. 学习背景
在普通 Vue 项目中,可以直接在 App.vue 中写:
vue
<HomeView />
<AboutView />
但是这样做会产生一个问题:
页面显示什么,完全由
App.vue写死,浏览器地址无法决定应该显示哪个页面。
实际项目通常希望实现下面的效果:
text
/ → 首页
/about → 关于我们
/courses/frontend → 前端课程
/courses/backend → 后端课程
这时就需要使用 Vue Router。
Vue Router 可以把"浏览器地址"和"Vue 页面组件"建立对应关系:
text
浏览器地址
↓
Vue Router 查找路由配置
↓
找到对应的页面组件
↓
显示到 RouterView 中
本次学习最终完成了一个简单的"迷你学习中心":
text
迷你学习中心
├── 首页
├── 课程中心
│ ├── 前端课程
│ └── 后端课程
└── 关于我们
2. 最终项目结构
text
route-lazy-demo/
├── src/
│ ├── router/
│ │ └── index.js
│ │
│ ├── views/
│ │ ├── HomeView.vue
│ │ ├── AboutView.vue
│ │ ├── CourseView.vue
│ │ └── courses/
│ │ ├── FrontendCourseView.vue
│ │ └── BackendCourseView.vue
│ │
│ ├── App.vue
│ └── main.js
│
├── index.html
└── package.json
各文件的职责如下:
| 文件或目录 | 作用 |
|---|---|
index.html |
提供 Vue 应用挂载的 HTML 容器 |
src/main.js |
Vue 应用入口,创建应用并注册路由 |
src/App.vue |
根组件,放置主导航和一级 RouterView |
src/router/index.js |
保存地址和页面组件的对应关系 |
src/views/ |
存放通过路由访问的页面组件 |
CourseView.vue |
课程中心父页面,内部包含子路由出口 |
3. Vue 项目是如何启动的
创建项目后,最先需要理解的是下面三个文件之间的关系:
text
index.html
↓
main.js
↓
App.vue
3.1 index.html
html
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
其中:
html
<div id="app"></div>
表示提前准备一个空容器。
而:
html
<script type="module" src="/src/main.js"></script>
表示浏览器继续执行 src/main.js。
3.2 main.js
javascript
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
执行过程是:
text
createApp(App)
创建一个以 App.vue 为根组件的 Vue 应用
↓
mount('#app')
把 Vue 应用挂载到 index.html 中的 #app
💡 核心结论
App.vue 之所以能够显示到浏览器中,是因为 main.js 使用 createApp(App).mount('#app'),把它挂载到了 index.html 的 <div id="app"> 中。
4. 页面组件和路由不是一回事
项目首先创建了两个普通页面组件:
text
HomeView.vue
AboutView.vue
例如首页:
vue
<template>
<div class="home-page">
<h2>首页</h2>
<p>欢迎来到迷你学习中心。</p>
</div>
</template>
<style scoped>
.home-page {
padding: 20px;
border: 1px solid #cccccc;
}
</style>
如果在 App.vue 中直接写:
vue
<script setup>
import HomeView from './views/HomeView.vue'
import AboutView from './views/AboutView.vue'
</script>
<template>
<HomeView />
<AboutView />
</template>
两个组件会同时显示。
这是因为:
text
App.vue 中写了 HomeView
↓
Vue 就显示 HomeView
App.vue 中写了 AboutView
↓
Vue 就显示 AboutView
此时浏览器地址并没有参与页面选择。
**⚠️ 注意:**页面组件只是一个 Vue 组件。只有把它放进路由配置后,它才会成为一个可以通过地址访问的页面。
5. 安装并注册 Vue Router
5.1 安装 Vue Router
bash
npm install vue-router
5.2 创建路由配置文件
新建:
text
src/router/index.js
基础路由配置如下:
javascript
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
const routes = [
{
path: '/',
component: HomeView,
},
{
path: '/about',
component: AboutView,
},
]
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
export default router
5.3 在 main.js 中注册路由
javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
新增的关键代码是:
javascript
.use(router)
它表示:
让当前 Vue 应用使用这个路由器。
执行顺序是:
text
createApp(App)
↓
use(router)
↓
mount('#app')
如果没有 .use(router),路由配置虽然已经写好,但 Vue 应用并不会真正使用它。
6. 路由配置中的核心字段
6.1 path
javascript
path: '/about'
表示这条路由匹配的浏览器地址。
例如:
text
http://localhost:5173/about
6.2 component
javascript
component: AboutView
表示当地址匹配 /about 时,应该显示 AboutView 页面组件。
因此:
javascript
{
path: '/about',
component: AboutView,
}
可以理解为:
text
访问 /about
↓
找到 AboutView
↓
显示 AboutView
6.3 routes
javascript
const routes = [
...
]
它本质上是一张"地址与页面对应表"。
text
/ → HomeView
/about → AboutView
6.4 createRouter
javascript
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
它负责创建真正的路由器。
6.5 createWebHistory
javascript
history: createWebHistory()
表示使用普通浏览器地址:
text
/about
/courses/frontend
而不是带 # 的地址形式。
7. RouterView:路由页面显示在哪里
App.vue 修改为:
vue
<template>
<div class="app">
<h1>迷你学习中心</h1>
<RouterView />
</div>
</template>
RouterView 可以理解为:
当前路由对应页面的显示窗口。
当浏览器地址是 / 时:
text
Vue Router 匹配 path: '/'
↓
找到 component: HomeView
↓
把 HomeView 放进 RouterView
当浏览器地址是 /about 时:
text
Vue Router 匹配 path: '/about'
↓
找到 component: AboutView
↓
把 AboutView 放进 RouterView
💡 核心结论
router/index.js 负责规定"显示谁",RouterView 负责规定"显示在哪里"。
8. RouterLink:让用户点击跳转
为了避免手动修改浏览器地址,可以在 App.vue 中加入:
vue
<nav class="nav">
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/courses">课程中心</RouterLink>
<RouterLink to="/about">关于我们</RouterLink>
</nav>
<RouterView />
8.1 RouterLink 的作用
vue
<RouterLink to="/about">关于我们</RouterLink>
表示:
页面上显示"关于我们",点击后跳转到
/about。
完整流程如下:
text
点击 RouterLink
↓
浏览器地址改变
↓
Vue Router 查找 routes
↓
匹配对应 component
↓
显示到 RouterView
8.2 RouterLink 与普通 <a> 标签的区别
| 对比项 | RouterLink |
<a> |
|---|---|---|
| 常见用途 | Vue 单页应用内部跳转 | 普通网页链接 |
| 是否通常刷新整个页面 | 否 | 是 |
| 是否由 Vue Router 接管 | 是 | 否 |
| 跳转时是否重新启动整个 Vue 应用 | 通常不会 | 通常会 |
8.3 当前激活链接
Vue Router 会自动给当前链接添加类似类名:
text
router-link-active
router-link-exact-active
可以利用它设置当前导航样式:
css
.router-link-active {
font-weight: bold;
}
9. 父路由、子路由和重定向
课程中心包含两个子页面:
text
课程中心
├── 前端课程
└── 后端课程
对应地址:
text
/courses/frontend
/courses/backend
9.1 创建父页面
CourseView.vue:
vue
<template>
<div class="course-page">
<h2>课程中心</h2>
<nav class="course-nav">
<RouterLink to="/courses/frontend">前端课程</RouterLink>
<RouterLink to="/courses/backend">后端课程</RouterLink>
</nav>
<RouterView />
</div>
</template>
这里又出现了一个 RouterView。
它不是显示首页或关于页面,而是显示课程中心的子页面。
9.2 配置子路由
javascript
{
path: '/courses',
component: CourseView,
redirect: '/courses/frontend',
children: [
{
path: 'frontend',
component: FrontendCourseView,
},
{
path: 'backend',
component: BackendCourseView,
},
],
}
9.3 children
javascript
children: [
...
]
表示当前路由下面还有子路由。
父路径:
text
/courses
子路径:
text
frontend
最终会拼接为:
text
/courses/frontend
9.4 为什么子路由不写 /
正确写法:
javascript
path: 'frontend'
这样它会和父路径拼接。
如果写成:
javascript
path: '/frontend'
它会更接近一个独立的绝对路径,而不是 /courses 下的相对路径。
9.5 redirect
javascript
redirect: '/courses/frontend'
表示访问:
text
/courses
时自动跳转到:
text
/courses/frontend
这样用户进入课程中心后,会默认看到前端课程页面。
10. 为什么父页面里还需要一个 RouterView
项目最终存在两个 RouterView:
text
App.vue
└── RouterView
└── CourseView.vue
└── RouterView
└── FrontendCourseView.vue
它们的职责不同:
| 所在文件 | 显示内容 |
|---|---|
App.vue 中的 RouterView |
首页、课程中心、关于我们等一级页面 |
CourseView.vue 中的 RouterView |
前端课程、后端课程等子页面 |
访问 /courses/frontend 时,执行过程是:
text
匹配父路由 /courses
↓
把 CourseView 显示到 App.vue 的 RouterView
↓
继续匹配子路由 frontend
↓
把 FrontendCourseView 显示到 CourseView.vue 的 RouterView
11. 路由懒加载
前面使用的是普通加载:
javascript
import AboutView from '../views/AboutView.vue'
{
path: '/about',
component: AboutView,
}
这表示读取路由文件时,就会导入 AboutView.vue。
后来把它改为:
javascript
{
path: '/about',
component: () => import('../views/AboutView.vue'),
}
这就是路由懒加载。
11.1 普通加载
text
项目启动
↓
读取 router/index.js
↓
立即加载 AboutView
↓
即使用户没有访问 /about,也已经加载
11.2 懒加载
text
项目启动
↓
只保存加载 AboutView 的函数
↓
暂时不加载 AboutView
↓
用户访问 /about
↓
Vue Router 调用函数
↓
动态加载 AboutView
11.3 拆解 component: () => import(...)
javascript
component: () => import('../views/AboutView.vue')
可以拆成三部分。
component
Vue Router 规定的路由配置项,用来说明当前地址对应哪个组件。
() =>
JavaScript 箭头函数。
javascript
() => import('../views/AboutView.vue')
等价于:
javascript
function () {
return import('../views/AboutView.vue')
}
import()
动态导入。
只有函数被执行时,才会真正加载目标文件。
11.4 为什么需要包一层函数
如果直接写:
javascript
component: import('../views/AboutView.vue')
读取路由配置时就会执行 import()。
而写成:
javascript
component: () => import('../views/AboutView.vue')
相当于先把"如何加载页面"的方法交给 Vue Router。
等用户真正访问 /about 时,Vue Router 才调用它。
💡 核心结论
不是加上 () => 页面就拥有了路由能力。真正建立路由关系的是 path 和 component;() => import() 只负责让组件延迟到真正访问时再加载。
12. 如何观察懒加载
可以通过浏览器开发者工具观察。
操作步骤:
- 打开首页
/; - 按
F12打开开发者工具; - 进入
Network面板; - 刷新首页;
- 过滤
AboutView; - 在没有访问
/about前,通常看不到对应模块; - 点击"关于我们";
- 此时可能看到
AboutView.vue或其构建后的 JavaScript 文件被加载。
第一次访问后,再次进入该页面可能直接使用浏览器缓存。
因此:
懒加载控制的是第一次何时开始加载,而不是每次进入页面都重新下载。
13. 优化前后对比
| 对比维度 | 优化前 | 优化后 | 带来的好处 |
|---|---|---|---|
| 页面显示方式 | 在 App.vue 中直接写组件 |
根据浏览器地址匹配组件 | 页面与地址建立对应关系 |
| 页面切换 | 手动修改组件代码 | 使用 RouterLink 切换 |
操作更自然,不需要重写页面 |
| 页面显示位置 | 固定写某个组件 | 使用 RouterView |
可以动态显示当前路由页面 |
| 课程页面组织 | 所有页面平级 | 使用父子路由 | 页面结构更清晰 |
| 默认子页面 | 没有默认页面 | 使用 redirect |
进入父路径后自动打开指定页面 |
| 页面加载时机 | 项目启动时加载 | 访问路由时加载 | 减少初始加载内容 |
| 页面代码组织 | 所有页面直接导入 | 使用 () => import() |
适合大型项目按页面拆分代码 |
14. 完整路由代码
下面是完成父子路由后的一种基础写法:
javascript
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import CourseView from '../views/CourseView.vue'
import FrontendCourseView from '../views/courses/FrontendCourseView.vue'
import BackendCourseView from '../views/courses/BackendCourseView.vue'
const routes = [
{
path: '/',
component: HomeView,
},
{
path: '/about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/courses',
component: CourseView,
redirect: '/courses/frontend',
children: [
{
path: 'frontend',
component: FrontendCourseView,
},
{
path: 'backend',
component: BackendCourseView,
},
],
},
]
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
export default router
这个版本中:
text
HomeView 普通加载
CourseView 普通加载
FrontendCourseView 普通加载
BackendCourseView 普通加载
AboutView 懒加载
后续还可以把其他页面也改成:
javascript
component: () => import('页面路径')
15. 完整执行流程
#mermaid-svg-VFVKztaHcvYC7ui5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VFVKztaHcvYC7ui5 .error-icon{fill:#552222;}#mermaid-svg-VFVKztaHcvYC7ui5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VFVKztaHcvYC7ui5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VFVKztaHcvYC7ui5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VFVKztaHcvYC7ui5 .marker.cross{stroke:#333333;}#mermaid-svg-VFVKztaHcvYC7ui5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VFVKztaHcvYC7ui5 p{margin:0;}#mermaid-svg-VFVKztaHcvYC7ui5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster-label text{fill:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster-label span{color:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster-label span p{background-color:transparent;}#mermaid-svg-VFVKztaHcvYC7ui5 .label text,#mermaid-svg-VFVKztaHcvYC7ui5 span{fill:#333;color:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 .node rect,#mermaid-svg-VFVKztaHcvYC7ui5 .node circle,#mermaid-svg-VFVKztaHcvYC7ui5 .node ellipse,#mermaid-svg-VFVKztaHcvYC7ui5 .node polygon,#mermaid-svg-VFVKztaHcvYC7ui5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VFVKztaHcvYC7ui5 .rough-node .label text,#mermaid-svg-VFVKztaHcvYC7ui5 .node .label text,#mermaid-svg-VFVKztaHcvYC7ui5 .image-shape .label,#mermaid-svg-VFVKztaHcvYC7ui5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-VFVKztaHcvYC7ui5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VFVKztaHcvYC7ui5 .rough-node .label,#mermaid-svg-VFVKztaHcvYC7ui5 .node .label,#mermaid-svg-VFVKztaHcvYC7ui5 .image-shape .label,#mermaid-svg-VFVKztaHcvYC7ui5 .icon-shape .label{text-align:center;}#mermaid-svg-VFVKztaHcvYC7ui5 .node.clickable{cursor:pointer;}#mermaid-svg-VFVKztaHcvYC7ui5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VFVKztaHcvYC7ui5 .arrowheadPath{fill:#333333;}#mermaid-svg-VFVKztaHcvYC7ui5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VFVKztaHcvYC7ui5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VFVKztaHcvYC7ui5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VFVKztaHcvYC7ui5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VFVKztaHcvYC7ui5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VFVKztaHcvYC7ui5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster text{fill:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 .cluster span{color:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VFVKztaHcvYC7ui5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VFVKztaHcvYC7ui5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-VFVKztaHcvYC7ui5 .icon-shape,#mermaid-svg-VFVKztaHcvYC7ui5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VFVKztaHcvYC7ui5 .icon-shape p,#mermaid-svg-VFVKztaHcvYC7ui5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VFVKztaHcvYC7ui5 .icon-shape .label rect,#mermaid-svg-VFVKztaHcvYC7ui5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VFVKztaHcvYC7ui5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VFVKztaHcvYC7ui5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VFVKztaHcvYC7ui5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
用户点击 RouterLink
浏览器地址发生变化
Vue Router 查找 routes
是否匹配到路由
不显示对应页面
读取 component
是否为懒加载函数
直接使用已导入组件
执行动态 import
加载页面组件
显示到 RouterView
对于子路由:
#mermaid-svg-q9FsmJLE0YFpHfUL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q9FsmJLE0YFpHfUL .error-icon{fill:#552222;}#mermaid-svg-q9FsmJLE0YFpHfUL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q9FsmJLE0YFpHfUL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q9FsmJLE0YFpHfUL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q9FsmJLE0YFpHfUL .marker.cross{stroke:#333333;}#mermaid-svg-q9FsmJLE0YFpHfUL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q9FsmJLE0YFpHfUL p{margin:0;}#mermaid-svg-q9FsmJLE0YFpHfUL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster-label text{fill:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster-label span{color:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster-label span p{background-color:transparent;}#mermaid-svg-q9FsmJLE0YFpHfUL .label text,#mermaid-svg-q9FsmJLE0YFpHfUL span{fill:#333;color:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL .node rect,#mermaid-svg-q9FsmJLE0YFpHfUL .node circle,#mermaid-svg-q9FsmJLE0YFpHfUL .node ellipse,#mermaid-svg-q9FsmJLE0YFpHfUL .node polygon,#mermaid-svg-q9FsmJLE0YFpHfUL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q9FsmJLE0YFpHfUL .rough-node .label text,#mermaid-svg-q9FsmJLE0YFpHfUL .node .label text,#mermaid-svg-q9FsmJLE0YFpHfUL .image-shape .label,#mermaid-svg-q9FsmJLE0YFpHfUL .icon-shape .label{text-anchor:middle;}#mermaid-svg-q9FsmJLE0YFpHfUL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q9FsmJLE0YFpHfUL .rough-node .label,#mermaid-svg-q9FsmJLE0YFpHfUL .node .label,#mermaid-svg-q9FsmJLE0YFpHfUL .image-shape .label,#mermaid-svg-q9FsmJLE0YFpHfUL .icon-shape .label{text-align:center;}#mermaid-svg-q9FsmJLE0YFpHfUL .node.clickable{cursor:pointer;}#mermaid-svg-q9FsmJLE0YFpHfUL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q9FsmJLE0YFpHfUL .arrowheadPath{fill:#333333;}#mermaid-svg-q9FsmJLE0YFpHfUL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q9FsmJLE0YFpHfUL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q9FsmJLE0YFpHfUL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q9FsmJLE0YFpHfUL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q9FsmJLE0YFpHfUL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q9FsmJLE0YFpHfUL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster text{fill:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL .cluster span{color:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q9FsmJLE0YFpHfUL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q9FsmJLE0YFpHfUL rect.text{fill:none;stroke-width:0;}#mermaid-svg-q9FsmJLE0YFpHfUL .icon-shape,#mermaid-svg-q9FsmJLE0YFpHfUL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q9FsmJLE0YFpHfUL .icon-shape p,#mermaid-svg-q9FsmJLE0YFpHfUL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q9FsmJLE0YFpHfUL .icon-shape .label rect,#mermaid-svg-q9FsmJLE0YFpHfUL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q9FsmJLE0YFpHfUL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q9FsmJLE0YFpHfUL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q9FsmJLE0YFpHfUL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 访问 /courses/frontend
匹配父路由 /courses
显示 CourseView
匹配子路由 frontend
显示 FrontendCourseView
放入 CourseView 的 RouterView
16. 本次学习总结
通过这个小项目,掌握了以下内容:
- 理解了 Vue 页面组件和路由之间的区别;
- 学会了安装并注册 Vue Router;
- 理解了
path和component的作用; - 理解了
RouterLink负责跳转; - 理解了
RouterView负责显示页面; - 学会了使用
children配置子路由; - 理解了父组件中为什么还需要一个
RouterView; - 学会了使用
redirect设置默认子页面; - 理解了普通导入和动态导入的区别;
- 理解了
component: () => import(...)的完整执行过程; - 学会了使用浏览器 Network 面板观察懒加载。
后续可以继续学习:
- 路由
name和命名跳转; router.push()编程式导航;- 动态路由参数,例如
/users/:id; - 路由守卫;
meta路由附加信息;- 权限路由;
- 404 页面;
- 所有页面统一使用懒加载。
问题与解答
问题 1:为什么写 component: () => import(...) 就能够路由?
我的困惑:
看到路由配置中加入:
javascript
component: () => import('../views/AboutView.vue')
容易误以为是 () => 让页面拥有了路由能力。
解答:
这个理解不够准确。
真正建立路由关系的是:
javascript
{
path: '/about',
component: ...
}
其中:
path决定匹配哪个浏览器地址;component决定匹配后显示哪个页面;RouterView决定页面显示在哪里;() => import()只决定这个组件什么时候加载。
执行过程如下:
text
访问 /about
↓
Vue Router 匹配 path
↓
读取 component
↓
发现 component 是一个函数
↓
执行函数
↓
动态加载 AboutView
↓
显示到 RouterView
因此,component 才是 Vue Router 认识的关键配置项,箭头函数只是实现懒加载的方式。
一句话记忆:
component决定显示谁,() => import()决定什么时候加载它。
问题 2:RouterView 是不是对应 router/index.js 里面的东西?
我的困惑:
RouterView 会显示路由页面,因此容易理解成它就是 router/index.js 里的路由内容。
解答:
不完全是。
router/index.js 保存的是路由规则:
text
什么地址
对应什么组件
例如:
javascript
{
path: '/about',
component: AboutView,
}
而 RouterView 是 Vue Router 提供的一个显示组件,它的作用是:
text
把当前地址匹配到的页面
显示在当前位置
两者关系如下:
text
router/index.js
规定显示谁
RouterView
规定显示在哪里
RouterView 不是路由表本身,也不是 index.js 文件的内容。它只是读取 Vue Router 当前匹配结果,并显示对应组件。
一句话记忆:
路由配置是地址表,
RouterView是页面显示窗口。
问题 3:这是 Vue 的基本操作吗?
我的困惑:
Vue 项目中经常出现 RouterView、RouterLink 和 component: () => import(),因此容易把它们全部理解为 Vue 本身的基础语法。
解答:
需要区分 Vue 和 Vue Router。
Vue 主要负责:
- 创建组件;
- 响应式数据;
- 模板渲染;
- 组件通信;
- 页面更新。
Vue Router 主要负责:
- 地址匹配;
- 页面跳转;
- 父子路由;
- 路由守卫;
- 路由参数;
- 页面组件切换。
因此:
vue
<RouterView />
<RouterLink />
以及:
javascript
createRouter()
createWebHistory()
属于 Vue Router 的功能。
而:
javascript
() => import(...)
本身是 JavaScript 动态导入和箭头函数语法,只是在 Vue Router 中常被用来实现路由懒加载。
一句话记忆:
路由切换是 Vue Router 的职责,
() => import()是 JavaScript 提供的动态加载方式。
问题 4:为什么父路由和子路由都需要 RouterView?
我的困惑:
项目中已经在 App.vue 写了一个 RouterView,课程中心父页面中为什么还要再写一个?
解答:
因为两个 RouterView 显示的层级不同。
App.vue 中的:
vue
<RouterView />
显示一级页面:
text
HomeView
AboutView
CourseView
CourseView.vue 中的:
vue
<RouterView />
显示课程中心内部的子页面:
text
FrontendCourseView
BackendCourseView
访问 /courses/frontend 时,会先显示父组件 CourseView,然后再把 FrontendCourseView 放进 CourseView 内部的 RouterView。
一句话记忆:
每一层嵌套路由,都需要对应层级的
RouterView作为显示出口。