详解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 上下左右键控制焦点(示例代码)
Dec 14 Javascript
查找页面中所有类为test的结点的方法
Mar 28 Javascript
js中的setInterval和setTimeout使用实例
May 09 Javascript
javascript面向对象特性代码实例
Jun 12 Javascript
js实现图片360度旋转
Jan 22 Javascript
微信小程序实战之仿android fragment可滑动底部导航栏(4)
Apr 16 Javascript
深入探究node之Transform
Jul 20 Javascript
jQuery实现base64前台加密解密功能详解
Aug 29 jQuery
详解最新vue-cli 2.9.1的webpack存在问题
Dec 16 Javascript
vue中关闭eslint的方法分析
Aug 04 Javascript
JavaScript数据结构与算法之检索算法示例【二分查找法、计算重复次数】
Feb 22 Javascript
JS字符串与二进制的相互转化实例代码详解
Jun 28 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和ACCESS写聊天室(八)
2006/10/09 PHP
php 常用算法和时间复杂度
2013/07/01 PHP
PHP函数实现从一个文本字符串中提取关键字的方法
2015/07/01 PHP
Jquery升级新版本后选择器的语法问题
2010/06/02 Javascript
JavaScript内核之基本概念
2011/10/21 Javascript
jquery全选/全不选/反选另一种实现方法(配合原生js)
2013/04/07 Javascript
JS JSON对象转为字符串的简单实现方法
2013/11/18 Javascript
jquery ajax跨域解决方法(json方式)
2014/02/04 Javascript
js学习之----深入理解闭包
2016/11/21 Javascript
JS高阶函数原理与用法实例分析
2019/01/15 Javascript
JS严格模式原理与用法实例分析
2020/04/27 Javascript
JavaScript如何实现监听键盘输入和鼠标监点击
2020/07/20 Javascript
详解JavaScript中的链式调用
2020/11/27 Javascript
[02:51]DOTA2 2015国际邀请赛中国区预选赛第一日战报
2015/05/27 DOTA
Python 解析XML文件
2009/04/15 Python
Python的内存泄漏及gc模块的使用分析
2014/07/16 Python
浅谈python函数之作用域(python3.5)
2017/10/27 Python
Python使用pandas对数据进行差分运算的方法
2018/12/22 Python
python+selenium实现QQ邮箱自动发送功能
2019/01/23 Python
Python selenium 加载并保存QQ群成员,去除其群主、管理员信息的示例代码
2020/05/28 Python
SQL Server的固定数据库角色都有哪些?对应的服务器权限有哪些?
2013/05/18 面试题
建筑班组长岗位职责
2014/01/02 职场文书
创业计划书的主要内容有哪些
2014/01/29 职场文书
有创意的广告词
2014/03/18 职场文书
会计系毕业生求职信
2014/05/28 职场文书
2014年办公室工作总结范文
2014/11/12 职场文书
2014年人事科工作总结
2014/11/19 职场文书
六年级学生期末评语
2014/12/26 职场文书
信贷客户经理岗位职责
2015/04/09 职场文书
经理岗位职责范本
2015/04/15 职场文书
2016年度优秀辅导员事迹材料
2016/02/26 职场文书
承诺书怎么写 ?
2019/04/16 职场文书
银行求职信范文
2019/05/13 职场文书
使用golang编写一个并发工作队列
2021/05/08 Golang
MySQL数据库如何使用Shell进行连接
2022/04/12 MySQL