详解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 相关文章推荐
Javascript保存网页为图片借助于html2canvas库实现
Sep 05 Javascript
Jquery遍历Json数据的方法
Apr 20 Javascript
chrome浏览器当表单自动填充时如何去除浏览器自动添加的默认样式
Oct 09 Javascript
详解javascript事件冒泡
Jan 09 Javascript
你所未知的3种Node.js代码优化方式
Feb 25 Javascript
原生JS取代一些JQuery方法的简单实现
Sep 20 Javascript
浅谈regExp的test方法取得的值变化的原因及处理方法
Mar 01 Javascript
提升页面加载速度的插件InstantClick
Sep 12 Javascript
JavaScript门面模式详解
Oct 19 Javascript
jQuery实现的中英文切换功能示例
Jan 11 jQuery
vxe-table vue table 表格组件功能
May 26 Javascript
微信小程序 行的删除和增加操作实现详解
Sep 29 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
php基础知识:控制结构
2006/12/13 PHP
PHP json_decode函数详细解析
2014/02/17 PHP
ThinkPHP结合ajax、Mysql实现的客户端通信功能代码示例
2014/06/23 PHP
php中switch语句用法详解
2015/08/17 PHP
Zend Framework教程之Zend_Db_Table表关联实例详解
2016/03/23 PHP
js 颜色选择器(兼容firefox)
2009/03/05 Javascript
jquery实现简单的拖拽效果实例兼容所有主流浏览器(优化篇)
2013/06/28 Javascript
js控制页面的全屏展示和退出全屏显示的方法
2015/03/10 Javascript
AngularJS+Node.js实现在线聊天室
2015/08/28 Javascript
javascript实现禁止复制网页内容汇总
2015/12/30 Javascript
详解JavaScript中的构造器Constructor模式
2016/01/14 Javascript
jQuery使用zTree插件实现树形菜单和异步加载
2016/02/25 Javascript
JavaScript Promise 用法
2016/06/14 Javascript
React创建组件的三种方式及其区别
2017/01/12 Javascript
用React-Native+Mobx做一个迷你水果商城APP(附源码)
2017/12/25 Javascript
在 Angular6 中使用 HTTP 请求服务端数据的步骤详解
2018/08/06 Javascript
Vue.js实现数据响应的方法
2018/08/13 Javascript
详解服务端预渲染之Nuxt(介绍篇)
2019/04/07 Javascript
JS 封装父页面子页面交互接口的实例代码
2019/06/25 Javascript
理解python多线程(python多线程简明教程)
2014/06/09 Python
Python操作Oracle数据库的简单方法和封装类实例
2018/05/07 Python
Django模型序列化返回自然主键值示例代码
2019/06/12 Python
Python pandas.DataFrame调整列顺序及修改index名的方法
2019/06/21 Python
Python 下载及安装详细步骤
2019/11/04 Python
浅谈python输出列表元素的所有排列形式
2020/02/26 Python
AVI-8手表美国官方商店:AVI-8 USA
2019/04/10 全球购物
如何开启linux的ssh服务
2013/06/03 面试题
销售实习自我鉴定
2013/12/07 职场文书
经济担保书范文
2014/04/02 职场文书
弘扬民族精神演讲稿
2014/05/07 职场文书
男性健康日的活动方案
2014/08/18 职场文书
小学竞选班干部演讲稿
2014/08/20 职场文书
支部书记四风问题对照检查材料
2014/10/04 职场文书
商务宴请邀请函范文
2015/02/02 职场文书
2015年乡镇统计工作总结
2015/04/22 职场文书
Sql Server 行数据的某列值想作为字段列显示的方法
2022/04/20 SQL Server