Featured image of post 第五章 路由

第五章 路由

基本概念

SPA 和 MPA

单页应用应用(Single-Page Application,SPA),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。

单页应用程序在切换功能页面的时候,如点击导航栏的时候,变化的只有主体内容,且页面不会刷新;多页应用程序(Multi-Page Application,MPA) 在点击导航栏时,会跳转到其他页面或刷新整个页面。

对比项 SPA(Vue Router) MPA(传统 HTML)
页面结构 单个 HTML 文件,动态更新内容 多个独立 HTML 文件,完整刷新
加载方式 首次加载所有核心资源,后续仅更新变化部分 每次请求新页面时重新加载全部资源
路由实现 前端路由(通过 URL hash 或 History API)通过前端路由拦截,URL 变化但不触发服务器请求 后端路由(服务器根据 URL 返回对应页面)URL 变化会向服务器请求新页面
SEO 较差(需特殊处理,如 SSR 或预渲染) 较好(原生支持搜索引擎抓取)
用户体验 流畅无刷新,适合复杂交互场景 可能有明显加载延迟,适合内容型网站
开发难度 较高(需处理状态管理、路由、性能优化) 较低(传统开发模式,技术栈简单)
维护成本 大型项目可能复杂,需合理架构 页面间耦合度低,维护相对简单
适用场景 社交平台、管理后台、在线游戏等 新闻网站、博客、电商产品列表页等

搜索引擎优化,简称 SEO(Search Engine Optimization),是一种通过分析搜索引擎的排名规则,了解搜索引擎如何进行搜索、抓取网页以及如何确定特定关键词的排名的技术。

SEO 通过采用易于被搜索引擎引用的方法,针对性地优化网站,从而提高其在搜索引擎中的自然排名。这样可以吸引更多用户访问网站,增加网站的流量,提升网站的销售和宣传能力,进而增强网站的品牌效应。

代码示例

SPA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// router/index.js (Vue Router配置)
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: About }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

MPA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// server.js (Express路由)
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/public/index.html')
})

app.get('/about', (req, res) => {
  res.sendFile(__dirname + '/public/about.html')
})

app.listen(3000, () => {
  console.log('Server running on port 3000')
})

路由

在 Vue 3 中,路由是实现单页应用(SPA)页面导航的核心机制。它允许用户在不刷新整个页面的情况下,通过 URL 变化切换视图内容。Vue Router 是 Vue.js 官方的路由管理器,专为 Vue 3 设计,提供了声明式导航、路由参数、路由守卫等高级功能。

Vue 中的路由:路径和组件之间的映射关系。

在 Html 中,实现跳转时,都使用了 a 标签。标签中有一个属性 herf,为其赋一个网络地址或者一个路径后,它就会跳转对应的页面。

Vue.js 的路由和 a 标签实现的功能是一样的,都是实现一个对应的功能,只不过路由的性能更佳,a 标签不管单击多少次,都会发生对应的网络请求,页面会不断地进行刷新;但是使用路由机制,单击之后,不会出现网络请求页面刷新,而会直接跳转到要链接的地址,这是使用路由的好处。

随着前后端分离开发模式的兴起,前端路由的概念出现:前端通过 AJAX 获取数据后通过一定的方式渲染到页面中,改变 url 不会向服务器发送请求,同时,前端可以监听 url 变化,可以解析 url 并执行相应的操作,而后端只负责提供 api 来返回数据,在 Vue中,通过路由跳转到不同的页面,实际上就是加载不同的组件

路由的安装和使用

路由的安装

Vue Router | The official Router for Vue.js

在已经创建的 vue3 项目中,使用以下指令安装路由。

1
npm install vue-router@4

路由基本配置

安装成功后,使用路由时需要经过以下几个步骤:

(1)在 src 下创建路由文件夹 router,并在 index.js 文件中创建路由对象,并配置路由规则。

index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { createRouter, createWebHashHistory } from "vue-router";

// 创建路由规则
const routes = [
    {
        path:...,
        component:...
    }
]

// 创建路由器
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 导出路由器
export default router

(2)安装路由插件。在 main.js 文件中使用 import router from './router' 导入需要使用的路由,导入后使用 app.use(router) 命令加载 router 插件用于安装插件挂载属性。

main.js

1
2
3
4
5
6
import { createApp } from 'vue'
import App from './App.vue'

import router from './router'

createApp(App).use(router).mount('#app')

(3)在 App 组件的 template 中使用路由挂载其他组件。

App.vue

1
2
3
<template>
    <router-view></router-view>
</template>

路由文件结构

在使用路由时,通常会将页面文件放在 src 下的 views 文件夹中而不是 components 文件夹中,views 文件夹通常用来存放页面级别的组件,也就是每个路由对应的页面。

比如,首页、关于我们、用户个人资料这些页面应该放在 views 里,因为它们对应不同的路由路径。而 components 文件夹里的组件是更小的、可复用的 UI 部分,比如按钮、导航栏、卡片等,这些可能在多个页面中被使用。

1. 功能与用途

文件夹 用途 典型场景
views 存放页面级组件,对应路由路径,负责完整页面的布局和路由逻辑。 HomeView.vue(首页)、UserProfile.vue(用户资料页)、LoginView.vue(登录页)。
components 存放可复用 UI 组件,用于构建页面的局部功能,不直接绑定路由。 Button.vue(按钮)、Navbar.vue(导航栏)、Card.vue(卡片组件)。

2. 路由关联

  • views 中的组件:

    • 在路由配置文件(router/index.js)中直接引用。

    • 示例:

      1
      2
      3
      4
      5
      6
      
      // router/index.js
      import HomeView from '@/views/HomeView.vue';
      
      const routes = [
        { path: '/', component: HomeView },
      ];
      
  • components 中的组件:

    • 不会在路由配置中直接引用,而是在其他组件(包括 views)中通过 import 引入。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      
      <!-- views/HomeView.vue -->
      <template>
        <div>
          <Navbar /> <!-- 来自 components/Navbar.vue -->
          <UserList /> <!-- 来自 components/UserList.vue -->
        </div>
      </template>
      

3. 组件职责

文件夹 职责
views 处理路由参数、页面级状态(如 Vuex/Pinia)、路由守卫、页面布局组合。
components 实现特定 UI 功能(如表单验证、数据展示),保持无状态或局部状态。

4. 命名规范

  • views
    • 建议以 XxxView.vueXxxPage.vue 命名(如 UserListView.vue)。
  • components
    • 使用功能明确的名称,可添加前缀表明用途(如 BaseButton.vueAppHeader.vue)。

5. 项目结构示例

1
2
3
4
5
6
7
8
9
src/
├── router/
│   └── index.js          # 路由配置,引用 views 中的组件
├── views/
│   ├── HomeView.vue      # 首页
│   └── UserProfile.vue   # 用户资料页
└── components/
    ├── BaseButton.vue    # 基础按钮
    └── UserCard.vue      # 用户卡片组件

6. 边界情况处理

  • 共享的复杂组件
    • 如果某个组件在多个页面中使用且包含复杂逻辑,可以放在 components 中(如 ProductList.vue)。
  • 子路由页面
    • 子路由对应的组件仍属于页面级,应放在 views 的子目录中(如 views/users/UserDetail.vue)。

跳转方式

在前面的实例中,如果用户需要跳转到不同的页面,则需要修改浏览器地址栏中的地址实现,而在网站中,用户通常需要通过超链接的文本或按钮进行跳转。

在 Vue 中,用户通常是使用 router-link 渲染一个 a 标签来实现跳转的,例如,使用 <router-link to=/about'></router-link> 跳转到关于about 页面,其中,to 是一个属性,指向目标页面,可以使用 v-bind 进行动态设置。

Sign.vue

1
2
3
4
5
6
<template>
    登录页面
    <router-link to="/register">
        <button>切换</button>
    </router-link>
</template>

Register.vue

1
2
3
4
5
6
<template>
    注册页面
    <router-link to="/sign">
        <button>切换</button>
    </router-link>
</template>

之前的小米商场案例中,使用到了 v-for 循环渲染导航栏,此时如果需要为不同导航栏标题添加跳转,需要将渲染的导航栏数据修改为对象数组形式,存储导航栏标题和跳转路径。

Header.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
    <div class="bg">
        <div class="container">
            ...
            <ul class="right">
                <li v-for="(item, index) in rightList">
                    <router-link :to="item.linkTo" class="title">
                        {{ item.title }}
                    </router-link>
                    ...
                </li>
                ...
            </ul>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            ...,
            rightList: [
                { title: '登录', linkTo: '/sign' },
                { title: '注册', linkTo: '/register' },
                { title: '消息通知', linkTo: '/info' },
            ],
        }
    },
    ...
}
</script>

<style scoped>
...
</style>

通过事件调用函数

可以通过 v-on 指令绑定事件,在函数中使用 this.$router.push() 方法,传入指定地址即可实现页面跳转。

Sign.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
    登录页面
    <router-link to="/register">
        <button>切换</button>
    </router-link>
    <button v-on:click="handleRouter">跳转</button>
</template>

<script>
export default{
    methods: {
        handleRouter(){
            this.$router.push('/register')
        }
    }
}
</script>

Register.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
    注册页面
    <router-link to="/sign">
        <button>切换</button>
    </router-link>
    <button v-on:click="handleRouter">跳转</button>
</template>

<script>
export default{
    methods: {
        handleRouter(){
            this.$router.push('/sign')
        }
    }
}
</script>

商品页面切换

Tab

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
<template>
    <div class="container">
        <img src="../../../images/xiaomi/logo-mi2.png" alt="">
        <ul>
            <!-- <li v-for="item in list">{{ item }}</li> -->
            <li v-for="item in list">
                <router-link :to="item.linkTo">
                    {{ item.title }}
                </router-link>
            </li>
        </ul>
        <div class="search">
            <input type="text" placeholder="2025米粉节">
            <button>
                <svg t="1745200046674" class="icon" viewBox="0 0 1024 1024" version="1.1"
                    xmlns="http://www.w3.org/2000/svg" p-id="3786" width="20" height="20">
                    <path
                        d="M959.266 879.165c0 81.582-81.582 81.582-81.582 81.582l-233.38-233.381c-60.529 43.977-134.777 70.217-215.318 70.217-202.755 0-367.117-164.362-367.117-367.117S226.23 63.349 428.985 63.349s367.117 164.362 367.117 367.117c0 80.541-26.241 154.785-70.217 215.318l233.381 233.381zM428.985 144.931c-157.697 0-285.536 127.838-285.536 285.536s127.838 285.536 285.536 285.536 285.536-127.838 285.536-285.536-127.839-285.536-285.536-285.536z"
                        fill="#8a8a8a" p-id="3787"></path>
                </svg>
            </button>
        </div>
    </div>

</template>

<script>
export default {
    data() {
        return {
            // list: [
            //     'Xiaomi 手机', 'Redmi 手机', '电视', '笔记本', '平板', '家电', '路由器', '服务中心', '社区'
            // ],
            list: [
                {title: 'Xiaomi 手机', linkTo: '/shop/phone'},
                {title: 'Redmi 手机', linkTo: '/shop/phone'},
                {title: '电视', linkTo: '/shop/pad'},
                {title: '笔记本', linkTo: '/shop/pad'},
                {title: '平板', linkTo: '/shop/pad'},
                {title: '家电', linkTo: '/shop/elec'},
                {title: '路由器', linkTo: '/shop/smart'},
                {title: '服务中心', linkTo: '/'},
                {title: '社区', linkTo: '/'},
            ]
        }
    }
}
</script>

<style scoped>
img {
    width: 56px;
    float: left;
    position: relative;
    top: 22px;
}

.container {
    height: 100px;
    width: 1226px;
    margin: 0 auto;
    overflow: hidden;
}

ul {
    height: 24px;
    line-height: 24px;
    position: relative;
    top: 38px;
    left: 160px;
    width: 700px;
}

ul li {
    float: left;
    list-style: none;
    padding: 0 10px;
}

ul li a{
    text-decoration: none;
    color: black;
}

ul li a:hover {
    color: rgb(255, 128, 9);
    cursor: pointer;
}

.search {
    width: 296px;
    height: 50px;
    display: flex;
    float: right;
}

button {
    width: 52px;
    border: 1px solid rgb(224, 224, 224);
    background: #fff;
}

input {
    width: 243px;
    border: 1px solid rgb(224, 224, 224);
    outline: none;
    box-sizing: border-box;
    padding: 0 10px;
    font-size: 14px;
    border-right: 0;
}
</style>

ViewPhone

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
    <Header></Header>
    <Tab></Tab>
    <Wrapper></Wrapper>
    <Banner></Banner>
    <PartPhone></PartPhone>
    <Footer></Footer>
</template>

<script>
import Header from '@/components/xiaomi/Header.vue';
import Tab from '@/components/xiaomi/Tab.vue';
import PartPhone from './PartPhone.vue';
import Footer from './Footer.vue';
import Wrapper from './Wrapper.vue';
import Banner from './Banner.vue';

export default {
    components: {
        Header,
        Tab,
        PartPhone,
        Footer,
        Wrapper,
        Banner
    },
    data() {
        return {

        }
    }
}
</script>

router

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { createRouter, createWebHashHistory } from "vue-router";

const routes = [
  {
    path: '/',
    component: () => import('@/views/Page.vue')
  },
  {
    path: '/shop/phone',
    component: () => import('@/components/xiaomi/ViewPhone.vue')
  },
  {
    path: '/shop/pad',
    component: () => import('@/components/xiaomi/ViewPad.vue')
  },
  {
    path: '/shop/elec',
    component: () => import('@/components/xiaomi/ViewElec.vue')
  },
  {
    path: '/shop/smart',
    component: () => import('@/components/xiaomi/ViewSmart.vue')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

模糊匹配和精确匹配

样式匹配

前面我们已经了解到,用户可以使用 router-link 渲染一个 a 标签来实现跳转,此时,点击标签 vue 会为当前被点击的 a 标签添加两个类,我们可以通过这两个类来为当前元素添加样式。

1
2
3
4
5
<li data-v-a5978fdc="">
    <a data-v-a5978fdc="" aria-current="page" href="#/shop/pad" class="router-link-active router-link-exact-active">
        电视
    </a>
</li>

可以发现,两个类分别为 router-link-activerouter-link-exact-active ,可以直接通过 style 设置相关样式。

1
2
3
ul li a.router-link-exact-active{
    color: rgb(255, 128, 9);
}

其中, router-link-active 表示模糊匹配,可以使对应样式在当前跳转目录,如 /shop 下的所有子页面中都生效,如 /shop/phone/shop/pad 等,适用于在切换子页面时不改变父元素样式的情况,使用相对较多。

router-link-exact-active 表示精确匹配,即只有在目录完全与当前 a 标签跳转的位置完全相同时才使得相应的样式生效。

自定义类名

可以在 router 中自定义模糊匹配和精确匹配对应的类名。

1
2
3
4
5
6
const router = createRouter({
  history: createWebHashHistory(),
  routes,
  linkActiveClass: 'fuzzy-active',
  linkExactActiveClass: 'exact-active'
})
Blog for Sandy Memories