详解vue 单页应用(spa)前端路由实现原理


Posted in Javascript onApril 04, 2018

写在前面:通常 SPA 中前端路由有2种实现方式:

  1. window.history
  2. location.hash

下面就来介绍下这两种方式具体怎么实现的

一.history

1.history基本介绍

window.history 对象包含浏览器的历史,window.history 对象在编写时可不使用 window 这个前缀。history是实现SPA前端路由是一种主流方法,它有几个原始方法:

  1. history.back() - 与在浏览器点击后退按钮相同
  2. history.forward() - 与在浏览器中点击按钮向前相同
  3. history.go(n) - 接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back(),go(0)相当于刷新当前页面
  4. 如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败

在HTML5,history对象提出了 pushState() 方法和 replaceState() 方法,这两个方法可以用来向历史栈中添加数据,就好像 url 变化了一样(过去只有 url 变化历史栈才会变化),这样就可以很好的模拟浏览历史和前进后退了,现在的前端路由也是基于这个原理实现的。

2.history.pushState

pushState(stateObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。

  1. stateObj :一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  2. title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  3. url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

关于pushState,有几个值得注意的地方:

pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,只有当触发前进后退等事件(back()和forward()等)时浏览器才会刷新

这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户,所以当违背同源策略时将会报错

3.history.replaceState

replaceState(stateObj, title, url) 和pushState的区别就在于它不是写入而是替换修改浏览历史中当前纪录,其余和 pushState一模一样

4.popstate事件

定义:每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

注意:仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

用法:使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。

5.history实现spa前端路由代码

<a class="api a">a.html</a>
<a class="api b">b.html</a>
 // 注册路由
 document.querySelectorAll('.api').forEach(item => {
  item.addEventListener('click', e => {
  e.preventDefault();
  let link = item.textContent;
  if (!!(window.history && history.pushState)) {
   // 支持History API
   window.history.pushState({name: 'api'}, link, link);
  } else {
   // 不支持,可使用一些Polyfill库来实现
  }
  }, false)
 });

 // 监听路由
 window.addEventListener('popstate', e => {
  console.log({
  location: location.href,
  state: e.state
  })
 }, false)

popstate监听函数里打印的e.state便是history.pushState()里传入的第一个参数,在这里即为{name: 'api'}

二.Hash

1.Hash基本介绍

url 中可以带有一个 hash http://localhost:9000/#/a.html

window 对象中有一个事件是 onhashchange,以下几种情况都会触发这个事件:

  1. 直接更改浏览器地址,在最后面增加或改变#hash;
  2. 通过改变location.href或location.hash的值;
  3. 通过触发点击带锚点的链接;
  4. 浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。

2.Hash实现spa前端路由代码

// 注册路由
 document.querySelectorAll('.api').forEach(item => {
  item.addEventListener('click', e => {
  e.preventDefault();
  let link = item.textContent;
  location.hash = link;
  }, false)
 });

 // 监听路由
 window.addEventListener('hashchange', e => {
  console.log({
  location: location.href,
  hash: location.hash
  })
 }, false)

hash模式与history模式,这两种模式都是通过浏览器接口实现的,除此之外vue-router还为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能。当然,以上只是一些核心逻辑,为保证系统的鲁棒性源码中还有大量的辅助逻辑,也很值得学习。

三、两种模式比较

  • pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
  • pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中
  • pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
  • pushState可额外设置title属性供后续使用

四、history模式的一个问题

我们知道对于单页应用来讲,理想的使用场景是仅在进入应用时加载index.html,后续在的网络操作通过Ajax完成,不会根据URL重新请求页面,但是难免遇到特殊情况,比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。
hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

http://oursite.com/#/user/id // 如重新请求只会发送http://oursite.com/

故在hash模式下遇到根据URL请求页面的情况不会有问题。

而history模式则会将URL修改得就和正常请求后端的URL一样

http://oursite.com/user/id

在此情况下重新向后端发送请求,如后端没有配置对应/user/id的路由处理,则会返回404错误。

官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

参考资料:

1、浏览器History API :https://developer.mozilla.org/zh-CN/docs/Web/API/History_API

2、解决History模式访问404的方案:https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS实现左右拖动改变内容显示区域大小的方法
Oct 13 Javascript
JSON格式的时间/Date(2367828670431)/格式转为正常的年-月-日 格式的代码
Jul 27 Javascript
基于JavaScript实现购物车功能
Feb 07 Javascript
jQuery插件FusionCharts绘制的3D环饼图效果示例【附demo源码】
Apr 02 jQuery
vue.js 获取当前自定义属性值
Jun 01 Javascript
解读ES6中class关键字
Nov 20 Javascript
在一个页面实现两个zTree联动的方法
Dec 20 Javascript
JavaScript选择排序算法原理与实现方法示例
Aug 06 Javascript
layui 优化button按钮和弹出框的方法
Aug 15 Javascript
Javascript摸拟自由落体与上抛运动原理与实现方法详解
Apr 08 Javascript
Vue基于iview table展示图片实现点击放大
Aug 05 Javascript
如何开发一个渐进式Web应用程序PWA
May 10 Javascript
vue cli升级webapck4总结
Apr 04 #Javascript
JavaScript实现短暂提示框功能
Apr 04 #Javascript
Vue filter介绍及详细使用
Apr 04 #Javascript
在Vue项目中引入腾讯验证码服务的教程
Apr 03 #Javascript
详解如何用babel转换es6的class语法
Apr 03 #Javascript
为vue-router懒加载时下载js的过程中添加loading提示避免无响应问题
Apr 03 #Javascript
新版vue-cli模板下本地开发环境使用node服务器跨域的方法
Apr 03 #Javascript
You might like
为查询结果建立向后/向前按钮
2006/10/09 PHP
使用php发送有附件的电子邮件-(PHPMailer使用的实例分析)
2013/04/26 PHP
PHP图片裁剪与缩放示例(无损裁剪图片)
2017/02/08 PHP
jquery中dom操作和事件的实例学习 下拉框应用
2011/12/01 Javascript
用表格输出1-1000之间的数字实现代码(附特效)
2013/04/21 Javascript
jQuery 1.9.1源码分析系列(十)事件系统之绑定事件
2015/11/19 Javascript
jquery制作图片时钟特效
2020/03/30 Javascript
AngularJS使用ng-Cloak阻止初始化闪烁问题的方法
2016/11/03 Javascript
使用jQuery的load方法设计动态加载及解决被加载页面js失效问题
2017/03/01 Javascript
Agularjs妙用双向数据绑定实现手风琴效果
2017/05/26 Javascript
bootstrap table插件的分页与checkbox使用详解
2017/07/23 Javascript
node使用Koa2搭建web项目的方法
2017/10/17 Javascript
JS设计模式之访问者模式定义与用法分析
2018/02/05 Javascript
vue-cli构建项目下使用微信分享功能
2018/05/28 Javascript
js数据类型检测总结
2018/08/05 Javascript
express express-session的使用小结
2018/12/12 Javascript
原生js实现日历效果
2020/03/02 Javascript
在vue中实现清除echarts上次保留的数据(亲测有效)
2020/09/09 Javascript
初学Python实用技巧两则
2014/08/29 Python
Python手机号码归属地查询代码
2016/05/04 Python
python常用函数详解
2016/09/13 Python
详解Python的Lambda函数与排序
2016/10/25 Python
python日志记录模块实例及改进
2017/02/12 Python
python爬虫之xpath的基本使用详解
2018/04/18 Python
Python hashlib模块加密过程解析
2019/11/05 Python
pycharm安装及如何导入numpy
2020/04/03 Python
使用python实现CGI环境搭建过程解析
2020/04/28 Python
通过Python实现Payload分离免杀过程详解
2020/07/13 Python
Tensorflow使用Anaconda、pycharm安装记录
2020/07/29 Python
详解利用canvas实现环形进度条的方法
2019/06/12 HTML / CSS
英国最大的电子产品和家电零售企业:Currys PC World
2016/09/24 全球购物
2015年销售内勤工作总结
2015/04/27 职场文书
企业培训简报范文
2015/07/20 职场文书
大学生就业指导课心得体会
2016/01/15 职场文书
2019年大学生职业生涯规划书
2019/03/25 职场文书
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
2022/02/12 Redis