视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
详解SPA中前端路由基本原理与实现方式
2020-11-27 22:07:57 责编:小采
文档


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

后端路由: 浏览器在地址栏中切换不同的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) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于0kB),第二个参数是页面的 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 解决这个问题。

    下载本文
    显示全文
    专题