大家好,我是G探险者。
最近在调试前端代码的时候,遇到一个问题。首先我们有一个门户页面,该页面里面有很多的豆腐块,每个豆腐块会配置一个系统的跳转连接。
我的系统就是其中一个豆腐块,我第一次登录进来之后,点击我的系统豆腐块可以正常跳转,页面加载都正常,但是切出之后,再次点击我的豆腐块,报了404,页面莫名其妙就找不到了。
为了弄清这个原因,最后研究发现有几个关键的技术点:前端路由,路由模式的两种模式(history模式和hash模式),单页面应用(SPA).
下面我将从这几个概念介绍。
一、单页面(SPA)与多页面(MPA)
什么是单页面应用(SPA)?
单页面应用(Single Page Application,简称 SPA)是一种通过 JavaScript 和客户端路由来实现的 web 应用程序,它的特点是:
- 一次性加载:SPA 应用在首次加载时加载整个应用的 HTML、CSS 和 JavaScript 文件。之后的所有页面更新都是通过 JavaScript 动态加载数据并在当前页面中更新视图,而不会重新加载整个页面。
- 页面切换无刷新:用户在浏览不同页面时,SPA 不会向服务器请求新的 HTML 页面,而是通过 JavaScript 动态更新视图,模拟页面跳转的效果。页面切换时,URL 通常也会发生变化,但不会引起浏览器的全页面刷新。
- 客户端渲染:所有的视图渲染工作主要在客户端完成,后端通常只负责提供数据接口(如 API)而不参与页面渲染。
SPA 的工作原理:
- 当用户首次访问时,浏览器从服务器加载一个 HTML 文件,加载相关的 JavaScript 文件和资源。
- 后续用户与应用交互时,通过 JavaScript 操控页面内容,通常使用客户端路由(如
vue-router
、react-router
)来管理不同的视图状态。 - URL 会根据不同的路由更新,但页面内容不会刷新,只有相关的数据和视图组件会被更新。
SPA 的优缺点:
- 优点:
- 流畅的用户体验:因为没有页面刷新,用户的交互体验更加平滑、响应迅速。
- 前后端分离:后端主要提供数据接口,前端可以独立于后端开发。
- 减少网络请求:页面资源一次加载后,后续操作仅需请求 API 接口而不是完整页面。
- 缺点:
- SEO 问题:传统的 SPA 使用 JavaScript 动态渲染页面内容,搜索引擎的爬虫可能无法正确索引这些动态内容。为了解决这个问题,通常需要使用服务器端渲染(SSR)或预渲染技术。
- 首次加载较慢:SPA 通常需要在初始加载时加载大量的 JavaScript 代码,可能会导致首次加载较慢。
- 浏览器性能压力:因为所有的渲染都在客户端进行,大量的 JavaScript 代码和复杂的应用逻辑可能会导致浏览器性能下降。
什么是多页面应用(MPA)?
多页面应用(Multi-Page Application,简称 MPA)是传统的 web 应用形式,指的是一个应用包含多个独立的页面,每个页面由服务器根据请求动态渲染,并且每次用户跳转到新页面时,浏览器都会发起新的 HTTP 请求,重新加载整个页面。
MPA 的工作原理:
- 每次用户请求一个新的页面时,浏览器都会向服务器发起请求,服务器返回一个完整的 HTML 页面,并加载该页面的 CSS 和 JavaScript 资源。
- 页面间的跳转会导致整个页面的重新加载,服务器负责渲染新的页面内容并返回。
- MPA 的每个页面通常是相互独立的,互不干扰。
MPA 的优缺点:
-
优点:
- SEO 友好:每个页面都是一个独立的 HTML 页面,可以被搜索引擎正常索引,因此 SEO 表现更好。
- 简单实现:没有太多复杂的前端 JavaScript 逻辑,适合于简单的网站或传统的多页面网站。
- 更好的初始加载速度:由于每个页面都是独立加载的,首屏加载速度通常较快。
-
缺点:
- 较差的用户体验:每次页面跳转都会重新加载整个页面,体验上不如 SPA 流畅。
- 页面之间的数据共享困难:每个页面都是独立的,页面之间的状态和数据不容易共享。
- 需要更多的网络请求:每次跳转都会向服务器请求完整的页面和资源,增加了服务器负担和网络流量。
SPA 和 MPA 的区别
特性 | 单页面应用(SPA) | 多页面应用(MPA) |
---|---|---|
页面切换 | 页面不刷新,内容局部更新,流畅的用户体验 | 每次跳转都刷新整个页面 |
请求次数 | 初次加载时请求资源,后续通过 API 获取数据 | 每次跳转都向服务器请求完整的页面资源 |
URL 变化 | 使用前端路由控制,URL 改变但不刷新页面 | 每次跳转都会改变 URL 并刷新页面 |
开发方式 | 前后端分离,前端多使用 JavaScript 控制视图 | 后端渲染页面,前端通常更依赖服务器渲染内容 |
SEO | SEO 难度较大,通常需要服务器端渲染(SSR)或预渲染 | 由于每个页面是独立的,SEO 更加友好 |
性能 | 初次加载较慢,但后续操作快速 | 每个页面都重新加载,可能导致性能问题 |
适用场景 | 适用于需要流畅交互、丰富功能的现代 Web 应用 | 适用于传统的网站、内容较少、页面之间差异大的场景 |
适用场景分析
-
SPA 适用场景:
- 动态内容丰富的 Web 应用:如社交媒体平台、在线编辑器、管理后台、即时聊天应用等。
- 需要良好用户体验的应用:例如需要实现流畅的动画效果、快速切换视图等的应用。
- 前后端分离的项目:适合团队分工明确,前端和后端开发可以独立进行的项目。
-
MPA 适用场景:
- 内容较少且结构简单的网站:如公司官网、博客、新闻网站等。
- SEO 需求较高的项目:例如电商平台、搜索引擎优化要求较高的博客或内容网站。
- 需要快速初始加载速度的网站:MPA 通常首屏加载速度较快,适用于静态内容较多的网站。
二、前端路由
什么是前端路由?
前端路由是指,在用户的浏览器端(客户端)处理路由跳转,而无需向服务器请求新的页面。在传统的多页面应用(MPA)中,每次用户点击链接或刷新页面时,浏览器会向服务器发送请求,服务器返回完整的页面。然而,在前端路由的 SPA 应用中,页面加载一次后,应用的所有路由跳转都只是在浏览器端通过 JavaScript 控制视图的切换,而不会重新加载整个页面。
Vue Router 的基本工作原理
Vue Router 基本上是根据 URL 的变化来决定显示哪个视图(组件)。其核心原理是:
- 通过监听浏览器的 URL(或地址栏)的变化,Vue Router 决定展示哪个组件。
- 根据定义的路由规则,URL 的不同部分(路径、查询参数、哈希等)会映射到不同的组件。
- Vue Router 在 URL 改变时,更新浏览器的历史记录(这通过
History API
或hashchange
事件来实现)。
具体来说,Vue Router 通过以下机制工作:
- 路由规则(Routes):定义 URL 路径与视图组件的映射关系。
- 路由模式(Mode):定义 URL 是如何呈现的,常见的有
hash
模式和history
模式。 - 路由守卫(Navigation Guards):在路由切换时,提供钩子函数来做路由拦截、权限验证等操作。
Vue Router 核心概念
路由定义与路由表
Vue Router 的核心是路由表,它通过路径匹配将 URL 映射到组件。通常,路由表定义在一个 routes
数组中,每个路由对象包含以下内容:
- path:路由的路径。
- component:该路径对应的组件。
- name:可选的路由名字,可以用于通过名称进行导航。
- children:嵌套路由,定义父子路由关系。
- meta:用于存放额外信息,如权限等。
视图组件与 <router-view>
路由表定义的组件会通过 <router-view>
显示在页面中。<router-view>
是一个占位符,它会根据路由的变化渲染对应的组件。
<!-- App.vue -->
<template><div><h1>Vue Router 示例</h1><router-view></router-view> <!-- 根据路由渲染对应的组件 --></div>
</template>
当你在浏览器中切换不同的 URL 路径时,<router-view>
会自动渲染出与当前路径匹配的组件。
动态路由与路由参数
Vue Router 还支持动态路由,它允许你在 URL 中定义动态部分。例如,路径 /user/:id
中的 :id
就是一个动态参数,可以在组件中获取到这个参数的值。
const routes = [{path: '/user/:id',component: User,props: true // 使动态路径参数通过 props 传递给组件}
];
通过 this.$route.params.id
访问动态参数 id
。
路由嵌套(Nested Routes)
你可以通过 children
属性定义嵌套路由,这样可以实现多层视图的嵌套展示。例如,父组件内嵌套多个子组件。
const routes = [{path: '/dashboard',component: Dashboard,children: [{path: 'profile',component: Profile},{path: 'settings',component: Settings}]}
];
在父组件的模板中,使用 <router-view>
来嵌套显示子路由的视图:
<!-- Dashboard.vue -->
<template><div><h2>Dashboard</h2><router-view></router-view> <!-- 子路由会渲染在这里 --></div>
</template>
路由守卫(Navigation Guards)
路由守卫是 Vue Router 提供的一种机制,可以在路由跳转时执行特定的逻辑,比如权限验证、数据预加载等。常见的路由守卫有:
- 全局守卫:可以在任何路由切换前、后或解析时触发。
beforeEach
: 在路由跳转前执行。afterEach
: 在路由跳转后执行。
router.beforeEach((to, from, next) => {// 执行权限检查等if (to.meta.requiresAuth && !isAuthenticated) {next('/login'); // 跳转到登录页} else {next(); // 继续路由跳转}
});
- 路由独享守卫:每个路由配置项中可以定义
beforeEnter
来指定独享的守卫。 - 组件内守卫:在组件内部定义
beforeRouteEnter
、beforeRouteUpdate
和beforeRouteLeave
来管理路由状态。
程序化导航(Programmatic Navigation)
除了 <router-link>
组件,Vue Router 还提供了编程式导航的方式来跳转路由。你可以通过调用 this.$router.push()
或 this.$router.replace()
方法来导航。
this.$router.push('/home'); // 导航到 /home
this.$router.push({ name: 'user', params: { id: 123 } }); // 根据路由名称和参数跳转
路由模式(Hash 与 History)
Vue Router 支持两种主要的路由模式:hash
模式和 history
模式。
Hash 模式
工作原理
- 在
hash
模式 中,路由会通过 URL 中的 hash(#
)符号 来进行区分。例如,http://example.com/#/home
。 #
后面的部分不会被浏览器提交给服务器,所有的页面变化都发生在浏览器端,而不会导致页面刷新。- 当用户访问
http://example.com/#/home
时,#
后面的部分会被用作路由的标识,浏览器不会向服务器发送请求。
优点
- 无需服务器配置:在
hash
模式下,浏览器不会向服务器发送带有#
的 URL 部分。因此,不需要特别的服务器配置就能支持路由。 - 兼容性好:
hash
模式的历史记录由浏览器内建支持,且几乎所有现代浏览器(包括早期版本的浏览器)都能兼容hash
模式。 - 易于实现:
hash
模式实现简单,开发时不需要考虑与服务器的配合,适合快速开发和部署。
缺点
- URL 不够美观:URL 中有
#
符号,可能影响用户的体验,并且不如history
模式的 URL 更符合常规的 URL 格式。例如,http://example.com/#/home
。 - SEO 较差:由于哈希部分(
#
后面的部分)不被浏览器作为正常 URL 发送给服务器,搜索引擎的爬虫可能无法抓取和索引这些页面,影响 SEO。
适用场景
- 静态网站:如果你在开发的是一个静态网站或不依赖于复杂服务器配置的应用,
hash
模式是一个简单且快速的选择。 - 没有后端服务器支持的应用:如果你没有对服务器进行配置(比如静态托管在某些第三方平台上,无法配置服务器的 URL 重写规则),
hash
模式是最适合的选择。 - 早期浏览器兼容:如果你的应用需要支持较旧的浏览器,
hash
模式因为其广泛的兼容性,通常是首选。
History 模式
工作原理
history
模式 基于 HTML5 的 History API,通过调用pushState
和replaceState
来改变浏览器的 URL,而无需刷新页面。例如,http://example.com/home
。- 这种模式下,URL 不包含
#
符号,看起来与传统的多页面应用相似。
优点
- 美观的 URL:
history
模式生成的 URL 更加清晰和符合常规,例如http://example.com/home
,不包含#
符号,更加符合 SEO 和用户习惯。 - 更好的 SEO:因为 URL 中不包含哈希符号,搜索引擎可以正常地抓取页面内容,优化 SEO 表现。
- 更接近传统网站:
history
模式下的 URL 和多页面应用的行为类似,因此适合需要多页面结构的应用,且能够提供无刷新页面切换的体验。
缺点
- 需要服务器配置:
history
模式依赖于 HTML5 的 History API,浏览器的 URL 会发生变化,因此需要服务器端配置支持路由。例如,如果用户直接访问http://example.com/home
,而服务器没有配置该路径的处理规则,可能会返回 404 错误。 - 浏览器兼容性:虽然大多数现代浏览器都支持
history
API,但某些老旧浏览器可能不兼容,尤其是 Internet Explorer 9 及以下版本。
适用场景
- 服务器支持:当你可以控制服务器的配置,能够对所有 URL 路径进行重定向处理时,
history
模式是一个理想的选择。例如,在服务器上配置 URL 重写规则,使得所有路径都指向应用的index.html
文件。 - SEO 要求较高的应用:如果你的应用需要考虑 SEO,且你希望 URL 结构尽量简洁、干净,
history
模式是首选,因为它不会在 URL 中添加#
符号。 - 现代浏览器和单页面应用:适用于现代浏览器,尤其是需要构建复杂、功能丰富的 SPA 应用时,
history
模式提供更好的体验。
如何选择:Hash 模式 vs History 模式
选择标准 | Hash 模式 | History 模式 |
---|---|---|
SEO 需求 | SEO 支持较差,因为 URL 中有 # 。 | SEO 更好,因为 URL 看起来是常规的路径。 |
浏览器兼容性 | 兼容性广,支持所有浏览器。 | 现代浏览器支持较好,较老版本浏览器支持差。 |
服务器支持 | 不需要服务器配置。 | 需要服务器配置,支持 URL 重写。 |
URL 美观度 | URL 包含 # ,不太美观。 | URL 简洁、符合标准,像传统的多页面应用。 |
适用场景 | 静态网站、没有后端支持的应用、早期浏览器支持 | 需要 SEO 和美观 URL 的应用、需要服务器支持 |
为什么History 模式需要服务器配置?
在 History 模式 中,前端应用使用 HTML5 History API(pushState
和 replaceState
)来控制 URL 的变化。当用户访问某个路径时,例如 http://example.com/home
,浏览器不会向服务器请求 home
页面,而是由前端路由框架(如 Vue Router)来渲染视图。但当用户直接刷新页面或通过直接访问 URL 时,浏览器会向服务器请求 http://example.com/home
这个路径。
问题:服务器并不知道该如何处理这个请求。因为这个路径 /home
并不是服务器上存在的文件或页面,而是由前端路由生成的。
- 如果你没有进行服务器配置,服务器会返回一个 404 错误,说明它找不到
/home
这个资源。 - 如果你配置了服务器,使得所有路径都指向你的前端应用的入口页面(例如
index.html
),前端路由框架会接管路由控制,正确渲染页面。
举例说明:
假设你有一个 Vue.js 项目,使用了 Vue Router 并且设置了 History 模式。你的应用有两个页面:
/home
(主页)/about
(关于页)
如果你访问 http://example.com/home
,前端路由框架会根据 URL 渲染 home
页面,但 如果没有服务器配置,直接刷新页面时,浏览器会向服务器请求 http://example.com/home
,而此时服务器并没有 /home
这个路径的实际文件,所以它会返回 404 错误。
服务器配置的作用:
为了避免这个问题,通常会进行如下配置:
- 服务器需要将所有的路由请求(如
/home
、/about
)都指向应用的index.html
文件,然后由前端的路由框架(如 Vue Router)来接管具体的路由渲染。
举例:Apache 服务器配置
在 Apache 服务器中,可以通过设置 .htaccess
文件来实现 URL 重写,将所有的请求都指向 index.html
:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
- 解释:
RewriteEngine On
:开启重写功能。RewriteCond %{REQUEST_FILENAME} !-f
:如果请求的文件不存在。RewriteCond %{REQUEST_FILENAME} !-d
:如果请求的目录不存在。RewriteRule ^ index.html [L]
:将所有请求重写到index.html
,并由前端路由来处理。
通过这种配置,当用户直接访问 http://example.com/home
或者刷新这个页面时,Apache 会返回 index.html
页面,然后 Vue Router 会根据 URL 自动渲染对应的视图。
举例:Nginx 服务器配置
在 Nginx 中,你可以使用以下配置:
server {listen 80;server_name example.com;root /path/to/your/project/dist;location / {try_files $uri $uri/ /index.html;}
}
- 解释:
root /path/to/your/project/dist;
:设置静态文件目录。location / { try_files $uri $uri/ /index.html; }
:如果用户请求的路径(如/home
)对应的文件不存在,Nginx 会返回index.html
,然后交由前端的 Vue Router 进行处理。