详解SPA中前端路由基本原理与实现方式


Posted in Javascript onSeptember 12, 2018

在讲前端路由之前,先说下后端路由,以及为什么出现了前端路由。

后端路由: 浏览器在地址栏中切换不同的url时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接html文件传给前端显示,java web中的jsp就是如此实现的。常用的后台MVC模式的基本路由处理流程:浏览器输入一个url请求,从中找到Controller和Action的值,将请求传递给Controller处理,Controller获取Model数据对象,并且将Model传递给View,最后View呈现界面。

例如输入一个url:localhost/home/index

其中localhost是域名,对应结构{controller}/{action}/{id}

  • 优点:分担了前端的压力,html和数据的拼接都是由服务器完成。
  • 缺点:当项目十分庞大时,加大了服务器端的压力,同时在浏览器端不能输入制定的url路径进行指定模块的访问。另外一个就是如果当前网速过慢,那将会延迟页面的加载,对用户体验不是很友好。

前端路由: 随着(SPA)单页应用的不断普及,前后端开发分离,目前项目基本都使用前端路由,在项目使用期间页面不会重新加载。

  • 优点:1、用户体验好,和后台网速没有关系,不需要每次都从服务器全部获取,界面展现快。2、可以再浏览器中输入指定想要访问的url路径地址。3.实现了前后端的分离,方便开发。有很多框架都带有路由功能模块。
  • 缺点:1、对SEO不是很友好2、在浏览器前进和后退时候重新发送请求,没有合理缓存数据。3,初始加载时候由于加载所有模块渲染,会慢一点。

前端路由目前主要有两种方法:

1、利用url的hash,就是常用的锚点(#)操作,类似页面中点击某小图标,返回页面顶部,JS通过hashChange事件来监听url的改变,IE7及以下需要轮询进行实现。一般常用框架的路由机制都是用的这种方法,例如Angualrjs自带的ngRoute和二次开发模块ui-router,react的react-route,vue-route…

2、利用HTML5的History模式,使url看起来类似普通网站,以”/”分割,没有”#”,但页面并没有跳转,不过使用这种模式需要服务器端的支持,服务器在接收到所有的请求后,都指向同一个html文件,通过historyAPI,监听popState事件,用pushState和replaceState来实现。

SPA 前端路由原理与实现方式

通常 SPA 中前端路由有2中实现方式,本文会简单快速总结这两种方法及其实现:

  1. 修改 url 中 Hash
  2. 利用 H5 中的 history

Hash

我们都知道 url 中可以带有一个 hash, 比如下面 url 中的 page2

https://www.abc.com/index.html#page2

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

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

这个事件有 2 个重要的属性:oldURL 和 newURL,分别表示点击前后的 url

<!-- 该页面路径为 https://www.abc.com/index.html -->

<a href="#page2" rel="external nofollow" rel="external nofollow" >click me</a>
<script type="text/javascript">
 window.onhashchange = function(e){
  console.log(e.oldURL);  //https://www.abc.com/index.html
  console.log(e.newURL);  //https://www.abc.com/index.html#page2
 }
</script>

这样我们可以这样建立一个 DOM

<nav>
  <a class="item active" href="#page1" rel="external nofollow" data-target="#index">page1</a>
  <a class="item" href="#page2" rel="external nofollow" rel="external nofollow" data-target="#info">page2</a>
  <a class="item" href="#page3" rel="external nofollow" data-target="#detail">page3</a>
  <a class="item" href="#page4" rel="external nofollow" data-target="#show">paga4</a>
  <a class="item" href="#page5" rel="external nofollow" data-target="#contact">paga5</a>
 </nav>
 <div class="container">
  <div class="page active" id="index">
   <h1>This is index page</h1>
  </div>
  <div class="page" id="info">
   <h1>This is info page</h1>
  </div>
  <div class="page" id="detail">
   <h1>This is detail page</h1>
  </div>
  <div class="page" id="show">
   <h1>This is show page</h1>
  </div>
  <div class="page" id="contact">
   <h1>This is contact page</h1>
  </div>
 </div>

为了好看我们加上 css, 比较重要的样式已经在代码里标出来了。

body{
 padding:0;
 margin: 0;
}
h1{
 margin: 0 0 0 160px;
}
nav{
 width: 150px;
 height: 150px;
 float: left;
}
nav a{
 display: block;
 background: #888;
 border: 1px solid #fff;
 border-top: none;
 width: 150px;
 font-size: 20px;
 line-height: 30px;
 text-align: center;
 color: #333;
 text-decoration: none;
}
.container{
 height: 154px;
}
/* page 部分比较重要*/
.page{
 display: none;
}
.page.active{
 display: block;
}
/* page 部分比较重要*/
nav a.active, .container{
 background: #ddd;
 border-right-color: #ddd;
}

重点是下面的 javascript,这里 DOM 操作我们借助 jQuery

var containers = $('.container');
 var links = $('.item');

 window.onhashchange = function(e){
  var currLink = $('[href="'+ location.hash + '" rel="external nofollow" ]').eq(0);
  var target = currLink.attr('data-target');

  currLink.addClass('active').siblings('a.item').removeClass('active');
  $(target).addClass('active').siblings('.page').removeClass('active');
 }

实现的逻辑不难,但是利用 hash 总是没有所谓前端路由的那种味,必然也不是主流方法。同样的效果如果需求简单不考虑兼容性的话,利用 css3 中 target 伪类就可以实现,这不是本文的重点,这里就不展开了。

history

作为一种主流方法,我们下面看看 history 如何实现。

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

这里我们先简单认识一下 history:

go(n) 方法接受一个整数参数, n大于0会发生前进 n 个历史栈页面; n小于0会后退 -n 个历史栈页面;

forward() 前进一个页面

back() 后退一个页面

以上三个方法会静默失败

pushSate(dataObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。这里需要说明的有3点:

  1. 当 pushState 执行一个浏览器的 url 会发生变化,而页面不会刷新,只有当触发的前进后退等事件浏览器才会刷新;
  2. 这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户。所以当违背同源策略时将会报错;
  3. 火狐目前会忽略 title 参数

replaceState(dataObj, title, url) 这个和上一个的区别就在于它不是写入而是替换栈顶记录,其余和 pushState 一模一样

History 还有1个静态只读属性:

History.length:当然历史堆栈长度

还有的都是已经废除的老古董了,这里就不提了

了解了这么多,那么可以研究一下如何利用 history 实现一个前端路由了,这里只考虑 javascript 部分,css 部分和上文一致。这里我们修改部分 html:

<a class="item active" href="/page1.html" rel="external nofollow" data-target="#index">page1</a>
<a class="item" href="/page2.html" rel="external nofollow" data-target="#info">page2</a>
<a class="item" href="/page3.html" rel="external nofollow" data-target="#detail">page3</a>
<a class="item" href="/page4/subpage1.html" rel="external nofollow" data-target="#show">paga4-1</a>
<a class="item" href="/page5/subpage2.html" rel="external nofollow" data-target="#contact">paga4-2</a>

我们修改了 a 标签的 href,这样的链接看上去才不是锚点了,不过这个过程我们要处理比较多的工作。首先为了简单一些,我们的这里仅仅对 .html 或没有扩展名结尾的链接设置前端路由:

// 通过委托的方法组织默认事件
var routerRxp = /\.html$|\/[^.]*$|/;
$(document).click(function(e){
 var href = e.target.getAttribute('href');
 if(href && routrRxp.test(href)){
  e.preventDefault();
 }
});

而后我们在点击时把数据写入历史栈,并控制 class 效果:

$(document).click(function(e){
 var href = e.target.getAttribute('href');

 if(href && routerRxp.test(href)){
  var id = e.target.getAttribute('data-target');
  history.pushState({targetId: id}, 'History demo', href);
  $(id).addClass('active').siblings('.page').removeClass('active');
  e.preventDefault();
 }
});

到这里,链接就可以点击了,但是浏览器的前进后退还不能用,我们加入 onpopstate 事件:

$(window).on('popstate',function(e){
 var id = e.originalEvent.state.targetId;
 $(id).addClass('active').siblings('.page').removeClass('active');
});

这样前进后退就可以用了,但是我们发现这样并不能退回主页,因为我们的主页默认就是 page1 标签页,所以没有为主页添加 state,简单处理就让其自己刷新好啦:

var indexPage = location.href;
$(window).on('popstate',function(e){
 if(location.href === indexPage){
  location.reload();
 }
 var id = e.originalEvent.state.targetId;
 $(id).addClass('active').siblings('.page').removeClass('active');
});

注意,这里不能使用重置 location.href 的方法刷新,那样就不能再前进了。

下面附上 history 方法的完整 js 代码

var indexPage = location.href;
 var routerRxp = /\.html$|\/[^.]*$|/;
 $(document).click(function(e){
  var href = e.target.getAttribute('href');

  if(href && routerRxp.test(href)){
   var id = e.target.getAttribute('data-target');
   history.pushState({targetId: id}, 'History demo', href);
   $(id).addClass('active').siblings('.page').removeClass('active');
   e.preventDefault();
  }
 });

$(window).on('popstate',function(e){
 if(location.href === indexPage){
  location.reload();
 }
 var id = e.originalEvent.state.targetId;
 $(id).addClass('active').siblings('.page').removeClass('active');
});

最后,关于两种方法有一些比较重要的特性总结一下:

  1. 有的文章提到“Firfox,Chrome在页面首次打开时都不会触发popstate事件,但是safari会触发该事件”,经实测现在的 safari 不存在这个问题。
  2. Mozilla指出,在 popstate 事件中,e.originalEvent.state 属性是存在硬盘里的,触发该事件后读取的历史栈信息是通过 pushState 或 replaceState 写入的才会有值,否则改属性为 null。
  3. history.state 和 e.originalEvent.state 异曲同工,而且前者可以在该事件之外任何地方随时使用。
  4. 由于 pushSate, onpopstate 属于 H5 的部分,存在一定兼容问题,可以使用 History.js (Github) 的 polyfill 解决这个问题。

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

Javascript 相关文章推荐
一个加载js文件的小脚本
Jun 28 Javascript
JQuery 初体验(建议学习jquery)
Apr 25 Javascript
JavaScript格式化日期时间的方法和自定义格式化函数示例
Apr 04 Javascript
jquery实现TAB选项卡鼠标经过带延迟效果的方法
Jul 27 Javascript
Jquery实现遮罩层的简单实例(就是弹出DIV周围都灰色不能操作)
Jul 14 Javascript
基于JavaScript实现拖动滑块效果
Feb 16 Javascript
JavaScript数据结构之双向链表定义与使用方法示例
Oct 27 Javascript
原生JS实现网页手机音乐播放器 歌词同步播放的示例
Feb 02 Javascript
jQuery实现DIV响应鼠标滑过由下向上展开效果示例【测试可用】
Apr 26 jQuery
Vue拖拽组件开发实例详解
May 11 Javascript
JavaScript实现点击出现子菜单效果
Feb 08 Javascript
原生JS中应该禁止出现的写法
May 05 Javascript
对angular2中的ngfor和ngif指令嵌套实例讲解
Sep 12 #Javascript
vue-cli 使用axios的操作方法及整合axios的多种方法
Sep 12 #Javascript
Vue $emit $refs子父组件间方法的调用实例
Sep 12 #Javascript
bootstrap table表格插件之服务器端分页实例代码
Sep 12 #Javascript
详解html-webpack-plugin插件(用法总结)
Sep 12 #Javascript
解决Vue.js父组件$on无法监听子组件$emit触发事件的问题
Sep 12 #Javascript
vue elementUI tree树形控件获取父节点ID的实例
Sep 12 #Javascript
You might like
PHP 多进程 解决难题
2009/06/22 PHP
php下删除一篇文章生成的多个静态页面
2010/08/08 PHP
比较全面的PHP数组的使用方法小结
2010/09/23 PHP
PHP遍历数组的几种方法
2012/03/22 PHP
ThinkPHP实现多数据库连接的解决方法
2014/07/01 PHP
PHP中JSON的应用技巧
2015/10/10 PHP
全面解析PHP面向对象的三大特征
2017/06/10 PHP
PHP写API输出的时用echo的原因详解
2019/04/28 PHP
javascript 有趣而诡异的数组
2009/04/06 Javascript
JavaScript CSS修改学习第一章 查找位置
2010/02/19 Javascript
JSQL  一个 web DB 的封装
2010/05/05 Javascript
jQuery自定义事件的简单实现代码
2014/01/27 Javascript
用jQuery toggleClass 实现鼠标移上变色
2014/05/14 Javascript
jQuery结合ajax实现动态加载文本内容
2015/05/19 Javascript
用JavaScript显示浏览器客户端信息的超相近教程
2015/06/18 Javascript
JavaScript原型及原型链终极详解
2016/01/04 Javascript
jQuery EasyUI Tab 选项卡问题小结
2016/08/16 Javascript
简单谈谈关于 npm 5.0 的新坑
2017/06/08 Javascript
ES6中Class类的静态方法实例小结
2017/10/28 Javascript
使用vue-cli4.0快速搭建一个项目的方法步骤
2019/12/04 Javascript
python多重继承实例
2014/10/11 Python
关于Python中浮点数精度处理的技巧总结
2017/08/10 Python
python2.7无法使用pip的解决方法(安装easy_install)
2018/04/03 Python
不到40行代码用Python实现一个简单的推荐系统
2019/05/10 Python
教你一步步利用python实现贪吃蛇游戏
2019/06/27 Python
keras 两种训练模型方式详解fit和fit_generator(节省内存)
2020/07/03 Python
金士达面试非笔试
2012/03/14 面试题
中医药大学市场营销专业自荐信
2013/09/29 职场文书
小学生中国梦演讲稿
2014/04/23 职场文书
简单租房协议书(范本)
2014/10/13 职场文书
公司表扬信格式
2015/05/04 职场文书
写给纪委的违纪检讨书
2015/05/05 职场文书
《黄山奇石》教学反思
2016/02/18 职场文书
求职信:会计求职的写作技巧
2019/04/24 职场文书
导游词之徐州-云龙山
2019/09/29 职场文书
PyTorch 实现L2正则化以及Dropout的操作
2021/05/27 Python