Vue 3 路由与懒加载入门:从零完成一个迷你学习中心

Vue 3 路由与懒加载入门:从零完成一个迷你学习中心

📘 本文简介
本文通过一个非常简单的"迷你学习中心"项目,从零学习 Vue Router 的基本用法,包括路由配置、页面切换、RouterLinkRouterView、父子路由、重定向和路由懒加载。完成后,可以看懂项目中常见的 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 />
vue 复制代码
<RouterLink to="/about">关于我们</RouterLink>

表示:

页面上显示"关于我们",点击后跳转到 /about

完整流程如下:

text 复制代码
点击 RouterLink
    ↓
浏览器地址改变
    ↓
Vue Router 查找 routes
    ↓
匹配对应 component
    ↓
显示到 RouterView
对比项 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 才调用它。
💡 核心结论
不是加上 () => 页面就拥有了路由能力。真正建立路由关系的是 pathcomponent() => import() 只负责让组件延迟到真正访问时再加载。


12. 如何观察懒加载

可以通过浏览器开发者工具观察。

操作步骤:

  1. 打开首页 /
  2. F12 打开开发者工具;
  3. 进入 Network 面板;
  4. 刷新首页;
  5. 过滤 AboutView
  6. 在没有访问 /about 前,通常看不到对应模块;
  7. 点击"关于我们";
  8. 此时可能看到 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;
  • 理解了 pathcomponent 的作用;
  • 理解了 RouterLink 负责跳转;
  • 理解了 RouterView 负责显示页面;
  • 学会了使用 children 配置子路由;
  • 理解了父组件中为什么还需要一个 RouterView
  • 学会了使用 redirect 设置默认子页面;
  • 理解了普通导入和动态导入的区别;
  • 理解了 component: () => import(...) 的完整执行过程;
  • 学会了使用浏览器 Network 面板观察懒加载。

后续可以继续学习:

  1. 路由 name 和命名跳转;
  2. router.push() 编程式导航;
  3. 动态路由参数,例如 /users/:id
  4. 路由守卫;
  5. meta 路由附加信息;
  6. 权限路由;
  7. 404 页面;
  8. 所有页面统一使用懒加载。

问题与解答

问题 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 项目中经常出现 RouterViewRouterLinkcomponent: () => 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 作为显示出口。

相关推荐
Cachel wood20 小时前
hands-on-modern-rl:动手学强化学习 贝尔曼方程
开发语言·网络·python·学习·embedding
万亿少女的梦16820 小时前
基于Spring Boot、Vue.js与MySQL的流浪猫狗救助系统设计与实现
vue.js·spring boot·mysql·javaweb·权限管理
要加油GW20 小时前
[ACTF2020 新生赛]Include
学习
猫猫不是喵喵.21 小时前
Vue 3 项目创建指南:基于 Vue CLI 与 Vite 的两种方式
前端·javascript·vue.js
minglie121 小时前
蚂蚁S9矿板PS 将驱动编译到内核
学习
华如锦21 小时前
【AGENT开发】python学习 Python Level 5
python·学习·elasticsearch
张元清21 小时前
React useThrottle Hook:节流值与回调(2026)
javascript·react.js
名字还没想好☜1 天前
Next.js App Router 约定式文件实战:loading、error、not-found 怎么兜住加载态与异常
开发语言·前端·javascript·react·next.js
console.log('npc')1 天前
React 跨项目集成实战:iframe 实现子项目详情弹窗
前端·javascript·react.js