前端路由是指在浏览器端控制页面内容切换显示的机制。在没有服务器端参与的情况下,前端路由可以根据URL的变化,对应展现不同的内容,实现页面的“伪”跳转。
在学习路由之前首先要了解一下SPA单页面应用
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
其实就是说,我们点击页面上的一些东西,并没有真正的发送请求进行页面跳转,而是在组件之间切换而已,仅仅刷新局部资源。
我们熟知的JS框架如react,vue,angular,ember都属于SPA
与之对应的是多页面应用,他们的区别如下
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
- 基于上面一点,SPA 相对对服务器压力小
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
为了实现前端路由,SPA需要监听URL的变化,并据此渲染对应的组件或页面不同部分,无需重新加载整个页面。下面让我们分别深入了解两种路由模式的原理。
hash和history
hash模式原理:
- 浏览器原生支持通过
window.location.hash
读写URL中的hash值,并且当hash值变化时,页面不会触发重新加载。 - SPA可以监听
hashchange
事件,在URL的hash部分变化时根据定义好的路由映射关系来动态渲染内容。 - 早期的前端路由的实现就是基于
location.hash
来实现的,location.hash
的值就是URL中#后面的内容 其实现原理就是监听#后面的内容来发起Ajax请求来进行局部更新,而不需要刷新整个页面。 - 使用
hashchange
事件来监听 URL 的变化,以下这几种情况改变 URL 都会触发hashchange
事件:浏览器前进后退改变 URL、<a>
标签改变 URL、window.location改变URL。
// Hash模式的简易实现
window.addEventListener('hashchange', routeChange);
function routeChange() {
const hash = window.location.hash.slice(1); // Remove the '#' symbol
// 基于hash值显示不同内容
routerView.innerHTML = routes[hash] ? routes[hash] : routes['404'];
}
我使用了vue中的router.push,发现没有触发hashchange事件, 这是因为hashchange是浏览器的事件,push是vue内部的机制在处理路由变化。
History模式原理:
- History API 允许SPA在浏览历史记录中添加、修改记录而不会触发页面加载。
- 通过history.pushState和history.replaceState可以改变URL且不重新加载页面。
- SPA可以监听popstate事件来响应浏览器前进、后退操作。
- history 提供了 pushState 和 replaceState 两个方法来记录路由状态,这两个方法改变 URL 不会引起页面刷新
- history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
- pushState(state, title, url) 和 replaceState(state, title, url)都可以接受三个相同的参数:
- state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
- title:标题,基本没用,一般传 null
- url:设定新的历史记录的 url,新的 url 与当前 url 的 origin 必须是一样的,否则会抛错,url可以是绝对路径,也可以是相对路径。
// History模式的简易实现
window.addEventListener('popstate', routeChange);
function navigate(path) {
history.pushState({}, "", path);
routeChange();
}
function routeChange() {
const path = window.location.pathname;
// 根据pathname来渲染不同的页面组件
routerView.innerHTML = routes[path] ? routes[path] : routes['404'];
}
// navigate('/user'); // 导航至用户页面
关于刷新404的问题
为什么history模式下会出现?
根据nginx的配置,当我们在地址栏输入 http://www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们再跳转路由进入到 http://www.xxx.com/login
关键在这里,当我们在 http://website.com/login 页执行刷新操作,会向真正的服务器发送请求资源,nginx location 是没有相关配置的,所以就会出现 404 的情况
为什么hash模式下不会出现?
router hash 模式我们都知道是用符号#表示的,如 http://website.com/#/login, hash 的值为 #/login
它的特点在于:hash 虽然出现在 URL 中,但不会被包括在内 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://website.com/#/login ...只有 http://website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误
简单来说: 前端打包后的 dist 包中,只有 index.html。所以,history 模式下发送的请求地址,服务端是找不到的。
- hash 模式:只将 hash 前面的部分当作地址
- history 模式:会将地址栏中的地址全部看作请求地址
hash模式的优缺
- 兼容低版本浏览器,Angular1.x和Vue默认使用的就是hash路由
- 只有#符号之前的内容才会包含在请求中被发送到后端,也就是说就算后端没有对路由全覆盖,但是不会返回404错误
- hash值的改变,都会在浏览器的访问历史中增加一个记录,所以可以通过浏览器的回退、前进按钮控制hash的切换
- 会覆盖锚点定位元素的功能
- 不太美观,#后面传输的数据复杂的话会出现问题
本文由“壹伴编辑器”提供技术支持
大致到这里就差不多了,又看见一篇写的比较好的文章,可以看一下。
单页应用
当我们在浏览器地址栏输入一个地址时,浏览器就会去服务端去请求内容。但每次点击一个链接,就去服务端请求,这样会有页面加载的等待。
后来慢慢就出现了单页应用,在第一次访问时,就把 html 文件,以及其他静态资源都请求到了客户端。之后的操作,只是利用 js 实现组件的展示和隐藏。除非需要刷新数据,才会利用 ajax 去请求。
但是纯粹的单页应用不方便管理,尤其是开发复杂应用的时候,需要有“多页面”的概念,并且很多用户习惯浏览器的前进后退的导航功能。
能不能有一种方法,可以在不向服务器发送请求的条件下,改变浏览器的 URL,以此来实现“多页面”概念?
答案是有,Vue Router 就是官方开发的一个插件,专门来做这件事。
URL 相关 API
最早改变 URL,但不向服务器发送请求的方式就是 hash。比如这种:
代码语言:javascript复制https://music.163.com/#/discover/toplist
同时浏览器也提供了一个事件来监听 hash 的改变,当 URL 的片段标识符更改时,将触发 hashchange 事件 (跟在#符号后面的URL部分,包括#符号)。
代码语言:javascript复制window.addEventListener('hashchange', function() {
console.log('The hash has changed!')
}, false);
后来 HTML5 发布,history 对象又增加了两个方法,用来改变浏览器的 URL。只是改变浏览器的访问记录栈,但是不会向服务器发起请求。
代码语言:javascript复制history.pushState(state, title[, url])
// 该方法会向浏览器会话的历史堆栈中添加一个状态。
history.replaceState(stateObj, title[, url])
//该方法与上一个方法类似,但区别是它会在历史堆栈中替换掉当前的记录。
代码语言:javascript复制const state = {
'page_id': 1, 'user_id': 5 }
const title = ''
const url = 'hello-world.html'
history.pushState(state, title, url)