SPA架构
什么是 SPA 架构
我们知道 SPA 是单页面的前端架构,但是,单页面应用(SPA)不是简单的只有一个HTML文件的形式,而是一种应用架构模式:
- SPA = Single Page Application(单页应用)
- 整个应用只加载一次 HTML 文件,后续所有页面切换、数据交互都在这一个页面内完成,不重新加载整个页面。
SPA 就像一个「容器」,首次加载时把所有必需的 JS、CSS 都加载完成(或按需加载),之后用户操作时,只动态更新页面内的「部分内容」,URL 变化但浏览器不刷新。
SPA 架构做的前端有这样的核心特征
- 应用仅包含 1 个核心 HTML(比如
index.html),所有页面都是这个 HTML 内的「组件片段」; - 无刷新切换,页面切换靠 JS 动态渲染组件(销毁旧组件、挂载新组件),浏览器地址栏 URL 可变化,但不会触发页面刷新(区别于传统网站的「跳转 = 刷新」);
- 数据驱动视图,页面内容更新依赖「数据变化」(比如 Vue 的响应式),而非重新加载 DOM;交互时只通过 AJAX/Fetch 向后端请求「数据」(JSON 格式),不请求完整 HTML。
SPA架构是如何工作的
SPA 架构是这样工作的
- 首次加载:只加载「容器 + 核心资源」
- 当用户输入
https://xxx.shop访问应用时:- 浏览器向服务器发送请求,只获取 1 个 HTML
文件(
index.html); - 这个 HTML 非常简洁,只有一个空的「根容器」(比如
<div id="app"></div>),以及引入的 Vue、Vue Router、应用 JS/CSS 等资源; - 浏览器加载完资源后,Vue 实例挂载到根容器,执行初始化逻辑(比如路由匹配);
- 根据当前 URL(默认是
/),Vue Router 匹配到「首页组件」,动态渲染到根容器中 —— 首次页面展示完成。
- 浏览器向服务器发送请求,只获取 1 个 HTML
文件(
- 当用户输入
- 页面切换:只更新「组件 + URL」,不刷新页面
- 当用户点击「详情页」按钮时(由 Vue Router 的
<router-link>实现):- 前端 JS 拦截点击事件(阻止浏览器默认的「跳转刷新」行为);
- 通过浏览器的
historyAPI 或hash特性,修改地址栏 URL(比如从/变成/detail/123); - Vue Router 监听 URL 变化,匹配到「详情页组件」;
- 前端 JS 动态操作 DOM:销毁当前的「首页组件」DOM,把「详情页组件」的
DOM 插入到
<router-view>中; - (一般会有)详情页组件通过 AJAX 请求后端,获取
productId=123的产品数据,渲染到页面 —— 整个过程无刷新。
- 当用户点击「详情页」按钮时(由 Vue Router 的
- 数据交互:只请求「数据」,不请求「HTML」
- SPA 中所有数据交互(列表查询、提交表单、详情查询等),都是通过 AJAX/Fetch 向后端请求「JSON 格式的数据」,而非完整的 HTML 页面。
- 后端只需要返回 JSON 数据(比如
{ "id": 123, "name": "手机", "price": 3999 }),不需要渲染完整 HTML—— 这也是 SPA 后端常被称为「接口服务」的原因。
SPA的优缺点
SPA 能成为现代前端开发的主流架构,核心是解决了 MPA 的「交互体验差」问题,具体优势有:
- 极致的交互流畅度:无刷新切换页面,用户操作没有「等待刷新」的延迟感,体验接近原生 App(这是 SPA 最核心的价值);
- 减少服务器压力:后端只需要提供数据接口,不用渲染 HTML 页面,计算成本降低,相同服务器能支撑更多用户;
- 前后端彻底分离:前端专注于「视图渲染和交互」,后端专注于「数据处理和接口提供」,开发职责清晰,可并行开发(比如前端先 Mock 数据开发,后端同步写接口);
- 组件复用性高:SPA 基于组件化开发(比如 Vue 组件),页面片段(导航栏、列表项、按钮)可复用,降低代码冗余;
- 可维护性强:前端逻辑集中在一个应用中,路由、状态、组件的管理更规范(配合 Vuex/Pinia 等状态管理库),后期迭代更高效。
但是 SPA 不是银弹,也存在明显缺点,实际开发中需要针对性解决:
- 首次加载速度慢:首次需要加载所有核心 JS、CSS 和框架(比如 Vue + Vue
Router + Vuex + 业务 JS),如果资源体积大,会导致「白屏时间长」;
- 解决方案:代码分割(按需加载)、资源压缩、CDN 加速、首屏渲染优化(比如骨架屏);
- SEO 不友好:搜索引擎爬虫默认只爬取 HTML 内容,而 SPA 首次渲染的 HTML
是空的(内容由 JS 动态生成),导致爬虫无法抓取页面信息,影响搜索排名;
- 解决方案:服务端渲染(SSR,比如 Nuxt.js)、预渲染(Prerender)、动态渲染(根据访问者是爬虫还是用户,返回不同内容);
- 前进 / 后退按钮问题:早期 SPA 用
hash模式(URL 带#),浏览器默认的前进 / 后退按钮不生效,后来通过historyAPI 解决了这个问题(Vue Router 已内置支持); - 路由管理依赖前端:MPA 的路由由后端控制,而 SPA 必须靠前端路由(如 Vue Router)管理 URL 与组件的映射,需要额外学习和配置;(这算缺点吧)))
- 内存泄漏风险:SPA 长时间运行(比如原生 App 内嵌的 H5
页面),组件频繁创建和销毁如果处理不当,可能导致内存泄漏,影响性能;
- 解决方案:合理销毁定时器、解绑事件监听、清理全局变量等。
前端路由在SPA架构的重要性
那么前端路由在 SPA 架构中就是最重要的了,可以说前端路由 使得 SPA 架构得以实现然后大规模流行
假设你做了一个 Vue 单页应用(SPA),里面有 3 个核心页面:
- 首页(展示产品列表)
- 详情页(展示单个产品信息)
- 个人中心(展示用户信息)
如果没有路由管理,你可能会这么写代码:
1 | <!-- App.vue --> |
这种写法能实现页面切换,但有 4 个致命问题:
- 无法通过 URL 直接访问页面:比如用户想分享「详情页」,复制链接发给别人,打开还是首页(因为 URL 没变);
- 没有浏览器历史记录:点击浏览器「后退 / 前进」按钮,不会切换到之前看过的页面;
- 难以传递参数:比如想访问「产品 ID=123 的详情页」,没法通过 URL 携带参数(只能用全局变量 / 状态管理,麻烦且不直观);
- 代码冗余:页面多了之后,v-if 判断会越来越复杂,导航逻辑和页面渲染混在一起,维护困难。
而路由管理的出现,就是解决单页应用的 URL 与页面的映射关系的问题,让 URL 能对应到具体的页面,同时支持浏览器历史、参数传递、导航控制等功能。
路由相关内容
什么是路由
路由(Router)的本质是「映射规则」:URL 路径 → 页面组件。
路由在前端开发中扮演着至关重要的角色,它的核心本质是 URL 与 UI 组件之间的映射关系管理。从传统的多页面应用到现代单页面应用,路由的演进体现了Web开发理念的重大转变。
传统网站的路由是每次URL变化都向服务器请求新的页面,这样每次请求都会导致完整页面刷新,体验很不好。也就是说,传统路由是交给后端来做的,URL → 后端资源(返回 HTML / 数据),每次路由切换都要向后端发请求,这就是以前的多页应用架构 MPA,比如 JSP/PHP 网站
前端路由管理,就是在前端(浏览器端)维护这套映射规则,让用户在不刷新页面的情况下,通过改变 URL 来切换不同的页面组件(因为是单页应用 SPA,整个应用只有一个 HTML 文件,刷新会重新加载整个应用)。
前端路由有 3 个核心能力
- URL 与组件的绑定:比如
/→ 首页组件,/detail/123→ ID=123 的详情组件; - 无刷新页面切换:通过 JS 监听 URL 变化,动态渲染对应的组件(不触发浏览器刷新,体验流畅);
- 浏览器历史兼容:支持点击「后退 /
前进」按钮切换页面(本质是利用浏览器的
historyAPI 或hash特性)。
举一个直观的例子:
你打开 Vue Router 的官方文档,打开入门部分(https://router.vuejs.org/zh/guide/),我们学习命名视图,点击发现,上面的 URL 变成了https://router.vuejs.org/zh/guide/essentials/named-views.html,可以发现是 URL 的变化带我们找到了正确的前端内容
前端路由与后端的关系
前端路由本身不依赖后端,但页面数据、权限等可能需要和后端交互。
新型的前端路由摆脱了每次路由都需要向后端发送请求,而是监听 URL 变化 → 渲染对应的组件,这个过程完全在浏览器端完成
举个例子:
- 你在 Vue 应用中配置了
{ path: '/detail', component: Detail }; - 当用户点击「详情页」导航时,前端路由会做两件事:
- 改变浏览器 URL(比如从
/变成/detail); - 销毁当前的首页组件,渲染 Detail 组件;
- 改变浏览器 URL(比如从
- 整个过程没有发送任何 HTTP 请求到后端,完全是前端 JS 操作。
虽然路由切换是纯前端的,但也有很多场景需要和后端交互(不是路由本身要交互,而是路由对应的页面 / 功能需要):
- 页面数据获取:进入详情页
/detail/123后,需要向后端请求productId=123的产品数据; - 路由权限控制:某些路由(比如
/admin管理员页面)需要先向后端验证用户是否有管理员权限,没有的话跳转到登录页;
这是很常见的需求,所以说前端路由完全抛开后端内容是不现实的
Vue Router 在 Vue 中扮演者什么角色
Vue Router 是 Vue 官方提供的路由管理库,专门为 Vue 应用设计,和 Vue 无缝集成。它的核心价值就是帮我们解决前面提到的无前端路由管理的痛点
- 简洁的路由配置:用少量代码就能实现 URL
与组件的映射(不用写一堆
v-if); - 内置导航功能:提供
<router-link>组件(替代普通按钮),点击自动切换 URL 和组件,还支持高亮当前路由; - 参数传递:支持通过 URL 传递参数(比如
/detail/:id),组件内可直接获取; - 嵌套路由:支持路由嵌套(比如
/user/profile、/user/orders),对应组件的嵌套渲染; - 路由守卫:可以在路由切换前后做拦截(比如权限验证、数据预加载);
- 历史模式支持:支持两种 URL 模式(
hash模式:#/detail;history模式:/detail),满足不同需求。
Vue Router如何安装配置
一句话,都前端了,直接 npm 把包导进来不就好了
Vue 3 对应的路由包是 vue-router@4
使用 Vue CLI
如果你的项目是通过 vue create xxx 创建的(Vue CLI
脚手架)
1 | # npm(推荐,最常用) |
--save:可选(npm 5+ 已默认添加到package.json依赖中),表示将包写入生产依赖;- 安装成功后,
package.json中会新增依赖:"vue-router": "^4.x.x"。
使用 Vite
安装和配置逻辑和 Vue CLI 基本一致
1 | # npm |
直接下载 / CDN
https://unpkg.com/vue-router@4
Unpkg.com 提供了基于 npm 的 CDN
链接。上述链接将始终指向 npm 上的最新版本。 你也可以通过像
https://unpkg.com/vue-router@4.0.15/dist/vue-router.global.js
这样的 URL 来使用特定的版本或 Tag。
这将把 Vue Router 暴露在一个全局的 VueRouter
对象上,例如 VueRouter.createRouter(...)。
一套路由配置的完整架构
在下好了包之后,来看一下完整的 Vue 路由结构
安装 Vue Router
创建路由配置文件
路由器实例是通过调用
createRouter()函数创建的: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// src/router/index.js
// 1. 从 vue-router 中导入核心函数
import { createRouter, createWebHistory } from 'vue-router'
// 2. 导入需要映射的页面组件(可自己创建,这里举示例)
// 建议:页面组件放在 src/views 文件夹下(和公共组件 src/components 区分)
import Home from '../views/Home.vue' // 首页组件
import Detail from '../views/Detail.vue' // 详情页组件
import NotFound from '../views/NotFound.vue' // 404 组件
// 3. 配置路由规则:URL 路径 → 对应组件
const routes = [
{
path: '/', // 访问路径(根路径)
name: 'Home', // 路由名称(可选,用于编程式导航)
component: Home // 对应的组件
},
{
path: '/detail/:id', // 动态路由(带参数 id,比如 /detail/123)
name: 'Detail',
component: Detail
},
{
path: '/:pathMatch(.*)*', // 404 路由(匹配所有未定义的路径)
name: 'NotFound',
component: NotFound
}
]
// 4. 创建路由实例
const router = createRouter({
// Vite 中 import.meta.env.BASE_URL 会读取 vite.config.js 中的 base 配置(默认是 '/')
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 5. 导出路由实例(供 main.js 导入)
export default router- 这里的
routes选项定义了一组路由,把 URL 路径映射到组件。其中,由component参数指定的组件就是被<RouterView>渲染的组件。这些路由组件通常被称为视图,但本质上它们只是普通的 Vue 组件。
- 这里的
在 Vue 根实例中注入路由
1
2
3
4
5
6
7
8// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App)
.use(router) // 注入路由
.mount('#app')创建组件 + 配置 App.vue
1
2
3
4
5
6
7
8
9// src/views/Home.vue
<template>
<div>
<h1>首页</h1>
<!-- 路由链接:替代 a 标签,无刷新跳转 -->
<router-link to="/detail/123">查看产品 123 详情</router-link>
</div>
</template>1
2
3
4
5
6
7
8
9// src/views/Detail.vue
<template>
<div>
<h1>详情页</h1>
<p>当前产品 ID:{{ $route.params.id }}</p> <!-- 获取路由参数 -->
<router-link to="/">返回首页</router-link>
</div>
</template>1
2
3
4
5
6// src/views/NotFound.vue
<template>
<h1>404 - 页面不存在</h1>
<router-link to="/">返回首页</router-link>
</template>在 App.vue 中使用路由视图
修改
src/App.vue,用<router-view>作为组件渲染的占位符(路由匹配到的组件会在这里显示):1
2
3
4
5
6
7<!-- src/App.vue -->
<template>
<div id="app">
<!-- 路由视图:核心占位符 -->
<router-view />
</div>
</template>运行测试
也就是说,一套 Vue Router 完整的前端路由是这样的
1 | src/ |
我只是展示了其中最关键的一部分
Vue Router 基础
创建路由模块文件
首先,在使用 Vue Router 进行路由管理的时候,我们需要进行路由配置文件的定义
一般情况下,我们会在 项目 src 目录下新建
router/index.js,集中管理路由配置和实例创建。
那么,路由模块文件如何编写呢?
首先,一个准的 Vue 路由模块通常包含以下部分:
- 依赖导入(Vue、VueRouter)
- 页面组件导入
- 路由规则配置(
routes数组) - 路由实例创建(
new VueRouter()) - 路由守卫配置(全局导航守卫)
- 导出路由实例
首先导入依赖
1 | import VueRouter from 'vue-router' |
然后,依赖导入了就要被使用,我们需要为路由模块文件安装 VueRouter 插件,这是 Vue2 的写法,Vue3 下面说
1 | Vue.use(VueRouter) |
Vue.use()是 Vue 提供的一个插件安装方法,用于在 Vue 全局范围内注册插件,使其功能可以在整个应用中使用。它接受一个插件对象(或函数)作为参数,并会自动调用插件内部的install方法(如果是对象的话)。Vue.use(VueRouter)具体做了如下内容- 全局注册路由相关组件
- 注入路由实例到 Vue 原型:在 Vue 原型上添加
$router(路由实例)和$route(当前路由信息)属性,让所有组件都能通过this.$router或this.$route访问路由功能,这个其实算是 Vue2 的写法 - 初始化路由相关逻辑
而路由配置的核心是 routes
数组,每个路由对象的关键属性如下
1 | { |
- 其中,
component参数指定的这些路由组件通常被称为视图,但本质上它们只是普通的 Vue 组件。 - 注意,路由规则的 path 要遵从唯一性,避免定义重复的
path(如两个/about),否则后定义的会覆盖前一个。 - 注意,不要直接修改
routes数组,因为数组是只读的。
路由可以当然可以嵌套
当路由足够大的时候,就需要涉及到路由拆分了,以我的项目为例子(我们的前端不习惯拆路由)
1 | src/ |
在路由模块组件中的 index.js 进行路由配置的合并
1 | import authRoutes from './auth' |
生产环境下,我们为了避免一次初始化的内容太多,通常会使用路由懒加载,它能让路由对应的组件在第一次被访问时才加载,而不是在应用初始化时就全部加载。
Vue
中通过动态import()语法实现路由懒加载,webpack
等构建工具会自动将其打包为单独的代码块。
将直接导入的组件替换为动态导入函数,例如:
1 | const routes = [ |
创建路由器实例
路由器实例是通过调用 createRouter()
函数创建的,传入核心配置项:
1 | // 创建路由器实例 |
其中,routers
就是传入前面定义的路由规则数组(const routes = [...]),即把所有页面的路由配置注入到路由器实例中。
路由的模式常见有两种,在下面细说,先知道history
选项控制了路由和 URL 路径是如何双向映射的就可以
'history':使用 HTML5 History API(依赖浏览器支持),URL 中不包含#(如https://example.com/home),更美观但需要后端配置支持(避免刷新 404)。'hash'(默认值):使用 URL 中的哈希值(#后面的部分,如https://example.com/#/home),兼容性更好,但 URL 中会带#。
还有一种写法来创建路由器实例
通过 new VueRouter(...) 创建的,这是 Vue Router
3.x 版本的语法(适用于 Vue 2)。而 createRouter 是
Vue Router 4.x 版本(适用于 Vue 3)的语法,两者属于不同版本的
API,核心作用一致但写法不同。
1 | const router = new VueRouter({ |
- 单独说一下这个配置项:
base: process.env.BASE_URL- 定义应用的基础路径,所有路由路径都会基于这个值拼接。
process.env.BASE_URL是 Vue CLI 项目中默认的环境变量,默认值为'/',可在vue.config.js中通过publicPath配置修改
最后,路由器组件中还会有这样一段代码
1 | export default router |
这行代码的作用是 将创建好的路由器实例导出,供整个项目使用
导出的 router
是一个单例对象(整个项目中只有一个路由器实例),所有组件通过
this.$router
访问的都是同一个实例,确保路由状态(如当前路径、历史记录)在全局一致。
注册路由器插件
一旦创建了我们的路由器实例,我们就需要将其注册为插件,这一步骤可以通过调用
use() 来完成。
在 Vue 中,app.use(router)(Vue 3)或
Vue.use(VueRouter)配合根实例注入(Vue 2)
的核心作用是将路由器实例与 Vue
应用关联,让路由功能在整个应用中生效。这一步是路由系统融入 Vue
生态的关键,类似于 “激活” 路由功能。
1 | // Vue2 |
或等价地:
1 | // Vue3 |
- 一个 Vue 应用只能挂载一个路由器实例,避免多次
app.use(router)。
app.use(router) 具体做了什么?
- 全局注入路由实例
- 向 Vue
应用注入路由器实例(
router),使得所有组件都能通过this.$router访问路由实例(如调用this.$router.push()实现编程式导航),通过this.$route访问当前路由信息(如路径、参数等)。 - 例如你的代码中,组件内可以通过
this.$router.push('/home')跳转到首页,这依赖于路由实例被全局注入。
- 向 Vue
应用注入路由器实例(
- 注册全局路由组件
- 自动注册
<router-view>和<router-link>两个核心组件,无需在每个组件中单独导入即可使用:<router-view>:用于渲染当前路由匹配的组件(例如访问/login时,它会渲染Login组件)。<router-link>:用于生成路由链接(替代<a>标签,避免页面刷新)。
- 自动注册
- 绑定路由与应用生命周期
- 将路由系统与 Vue 应用的生命周期钩子关联,确保当路由状态变化(如 URL 改变)时,Vue 能自动触发组件更新,实现 “URL 变化 → 视图更新” 的响应式联动。
- 例如用户从
/login跳转到/home时,<router-view>会自动卸载Login组件并挂载Home组件。
- 初始化路由监听
- 启动对浏览器 URL 变化的监听(如
popstate事件),并将 URL 变化同步到路由实例的状态中,反之,当通过$router.push改变路由时,也会同步更新浏览器 URL。
- 启动对浏览器 URL 变化的监听(如
在, Vue 项目中,Vue.use(VueRouter)(Vue 2)或
app.use(router)(Vue
3)通常是在入口文件中执行的(一般是
main.js 或 main.ts)
在 Vue 2 项目中,对应逻辑是在 main.js 中先导入
router 并注入根实例,再调用 $mount()
1 | // main.js(Vue 2 示例) |
而在 index.js 中,我们通过
Vue.use(VueRouter) 完成了Vue Router的全局注册
这个例子是 Vue2 的写法,完整的 Vue3
的相关写法如下,此时,就通过应用实例的 app.use(router)
注册Vue Router
1 | // main.js(Vue3入口文件) |
很多人以为是在 App.vue 中,实际上并不是,原因是:
- 入口文件是初始化 Vue 实例(或应用)的地方,需要在这里完成全局插件的注册
- 路由插件需要在应用启动时就完成注册,才能保证整个应用的路由功能正常工作
访问路由器和当前路由
我们经常需要在应用的其他地方访问路由器。
- 路由器(Router):全局路由实例,用于控制路由跳转、配置路由规则等。
- 当前路由(Route):表示当前活跃的路由信息,包含路径、参数、查询参数等。
如果你是从 ES 模块导出路由器实例的,你可以将路由器实例直接导入到你需要它的地方。在一些情况下这是最好的方法,但如果我们在组件内部,那么我们还有其他选择。
路由访问方式的 Vue2 和 Vue3 写法差异如下
Vue2:组件中通常通过
this.$router访问路由器实例,通过this.$route访问当前路由信息Vue3:
选项式 API 仍可使用
this.$router/this.$route组合式 API 中使用
useRouter()和useRoute()访问路由器实例和当前路由。1
2
3
4import { useRouter, useRoute } from 'vue-router'
const router = useRouter() // 替代 this.$router
const route = useRoute() // 替代 this.$route
访问路由器基本就是用于跳转,历史操作记录等,我们下面的例子都用 Vue3 的形式写
1 | import { useRouter } from 'vue-router'; |
- 注意,Vue3 中
useRoute()返回的route对象是响应式的,需通过watch监听变化。
访问当前路由(Route)
1 | import { useRoute } from 'vue-router'; |
为项目添加路由入口
现在我们所有准备工作都完成了,但是其实还是不能直接显示各路由地址对应的组件,因为我们还缺少关键的一步——在项目的页面中,写入一个路由入口。
先了解<RouterLink>标签,这个标签是干什么的
1 | <RouterLink to="/">Home</RouterLink> |
- 类似于 HTML 中的
<a>标签 - 其中的一些核心属性如下
to:指定目标路由,必填项。- 字符串形式:直接指定路径,如
to="/project-detail" - 对象形式:支持更复杂的路由配置(如携带参数、查询参数)
- 字符串形式:直接指定路径,如
replace:设置为true时,跳转不会留下历史记录(类似router.replace())。(无痕访问)active-class:指定当前路由匹配时的激活样式类(默认是router-link-active)。exact:精确匹配路由(默认是模糊匹配)。
但是,不要这样做,因为我们不可能每加一个路由信息,都在这里维护对应的RouterLink代码。
当然如果想测试自己上面路由信息是否维护正确,可以用这个代码测试一下。
我们一般直接写 <RouterView/>
,Vue2中就是<router-view/>
<RouterView/>
是路由匹配组件的占位符,用于渲染当前路由对应的组件。当路由切换时,<RouterView/>
会自动替换为对应的组件内容。
它会根据当前 URL
匹配的路由规则,自动渲染对应的组件。如果路由配置中有嵌套路由,内层的
<RouterView/> 会渲染嵌套的子组件。
1 | <template> |
别忘了。js部分引入的模块也会有所变化
也就是说,我们需要先通过 app.use(router)
启用路由功能,然后才能在 App.vue 中用 <RouterView/>
展示路由对应的组件内容。这样,路由就正式启用了





