基本概念
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
中的组件:
-
components
中的组件:
3. 组件职责
文件夹 |
职责 |
views |
处理路由参数、页面级状态(如 Vuex/Pinia)、路由守卫、页面布局组合。 |
components |
实现特定 UI 功能(如表单验证、数据展示),保持无状态或局部状态。 |
4. 命名规范
views
:
- 建议以
XxxView.vue
或 XxxPage.vue
命名(如 UserListView.vue
)。
components
:
- 使用功能明确的名称,可添加前缀表明用途(如
BaseButton.vue
、AppHeader.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
)。
跳转方式
router-link
在前面的实例中,如果用户需要跳转到不同的页面,则需要修改浏览器地址栏中的地址实现,而在网站中,用户通常需要通过超链接的文本或按钮进行跳转。
在 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-active
和 router-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'
})
|