前端路由与单页应用

什么是路由 route?

http://x.com/posts -> server:/www/posts/index.html
http://x.com/posts/1.html -> server:/www/posts/1.html

指符号到实体、地址到实物之间的对应或映射关系,典型例子如ip地址到主机的映射

  • route: 路由项,即一个映射规则
  • router:
    • 路由表:作为一组相关路由项的容器
    • 路由器:管理路由项、执行路由转换

服务端路由

在服务端定义的路由,即当用户在浏览器中访问左边的url时,对应实际访问的服务端资源

  • 由Web服务器(如Nginx)定义,一般是页面或文件等静态资源的简单映射,即静态路由
  • 由服务端脚本语言来定义,如php\jsp\asp等: 功能更强,可实现动态路由,可对应于访问服务器上的任何资源,如文件、数据库、进程等

静态路由与动态路由

  • x.com/posts/1.html 一对一映射关系,称之为静态路由
  • x.com/posts/show.php?id=[id] 支持查询参数,一对多映射关系,称之为动态路由

客户端路由:隐含的定义

<a onclick='function(){...list posts...}'>index</a>

<a onclick='show(32)'>show</a>
<script> function show(id){...}  </script>

上述代码相当于定义了两个路由

  • 文本为 index<a>元素到一个匿名函数之间的映射
  • 文本为 show<a>元素到show()函数之间的映射,它接受一个动态的id参数

隐式的定义虽然一时方便,但它改变了元素的语义和功能

重构后的客户端路由:显式的声明

<a href='/posts'>index</a>
<a href='/posts#32'>show<a>
<script> 
function index{...} 
function show(id){...} 
</script>

虽然页面是由客户端动态渲染生成,但程序员们希望回到传统的Web模式,使得标记恢复其链接的语义

  • 给每一个动态页面或客户端过程显式命名或编址,当用户访问该地址时,即渲染对应的动态页面或调用对应的客户端过程
  • 有了地址,就可以支持浏览器收藏、上一步、下一步等Web传统交互方式
    • 可收藏:意味着可生成分享的链接,可以实现互联、可以恢复到应用的任一状态,这是Web应用相比于桌面应用的最大优势
  • 以路由为中心,利于代码的组织,如模块化、可重用等

客户端路由的实现机制:基于Hash

http://x.com/index.html为例,浏览器的行为是:

  • 如果该Url的路径(path)部分发生变化,譬如http://x.com/index.html?key=sport则浏览器会视用一个新的网络地址,即向服务端发起新的http请求,这意味着刷新当前的页面
    • 由于发起了新的请求,需要由服务端来处理,故这类路由称之为服务端路由
  • 但如果改变的是锚点#之后内容,譬如上述url改变为http://x.com/index.html#posts/show/32,因为#的语义是页内寻址,浏览器视为当前页面不变,即不会向服务端发送新的请求从而导致页面被刷新
    • 由于保持当前页面不变,仍由客户端来处理,故这类路由称之为客户端路由

锚点#之后的内容称之为hash,浏览器提供onhashchange事件来监听hash的变化

hash方案的简单实现

window.onhashchange = function() {
  var hash = window.location.hash
  var path = hash.substring(1)

  switch (path) {
    case 'posts':
      postIndex()
      break
    case 'posts/show':    
      postShow(id) // 可以进一步对path做解析和判断,获取id参数
      break
    default:
      show404NotFound()
  }
}

客户端路由的实现机制:基于History API

  • 在 HTML5 规范中,提供了增强的 History API
    • history.pushState(state,title,url); // 添加新的状态到历史状态栈
    • history.replaceState(state,title,url); // 用新的状态代替当前状态
  • 开发者可使用上述API,管理浏览器的浏览历史记录,并且url(即地址栏)的变化不会导致浏览器发起新的http 请求,即类似#方案,不触发页面刷新
  • 浏览器的默认交互操作,如上一步、下一步、刷新等都会触发history state的出栈或入栈
  • 提供 onpopstate 事件,监听history state的变化

详情参考 https://developer.mozilla.org/zh-CN/docs/Web/API/History

history方案的简单实现

window.onpopstate = function() {
  var path = window.location.pathname

  switch (path) {
    case 'posts':
      postIndex()
      break
    case 'posts/show':    
      postShow(id) // 可以进一步对path做解析和判断,获取id参数
      break
    default:
      show404NotFound()
  }
}
  • 上述代码仅支持由浏览器的上一步、下一步操作引发的状态改变
  • 开发人员需要显式地将自己的代码与history state进行关联

history方案的增强实现

<p id="example">
 <a href="/index" title="index">index</a>
 <a href="/show" title="age" data-id='32' >show</a>?
</p>
<div class="main" id="main"></div>
<script>
(function(){
    var examplebox = document.getElementById('example')
    var mainbox = document.getElementById('main')

    examplebox.addEventListener('click', function(e){
        e.preventDefault()
        var elm = e.target
        var uri = elm.href
        var tlt = elm.title
        history.pushState({path:uri,title:tlt}, null, uri)
        mainbox.innerHTML = 'current page is ' + tlt
    })
    window.addEventListener('popstate',function(e){
        var state = e.state
        mainbox.innerHTML = 'current page is ' + state.title
    })
})()
</script>

相关库 https://github.com/mjackson/history

路由实现技术比较

  • hash 兼容低版本浏览器,且不会与服务端路由发生冲突
  • history API 更符合URL格式,但可能与服务端路由冲突,需要对服务端进行改造

history API模式下的路由冲突与改造

假设服务端路由为:

  • x.com/ => index.html
  • x.com/posts => posts/index.php

客户端路由为:

  • x.com/posts => function postsIndex(){}

  • 初始访问/时,浏览器将访问服务端的index.html,该文档会加载客户端路由的相关js文件。此时,当点击/posts链接时,由于click事件被客户端路由拦截,它不会触发服务端请求

  • 但如果将/posts分享或收藏时,即直接在浏览器中访问/posts,则显然会触发服务端请求posts/index.php,这对于单页应用而言,可能是个错误
  • 解决办法是,在服务端做改造,即拦截所有除/之外的服务端路由,将其统一重定向到/

简单的使用办法是使用http-server-spa 包,它默认会支持上述解决办法,无需单独配置

前端路由库 director.js

https://github.com/flatiron/director 是一个简单的 client-side router库,支持基于hash技术的路由,方便处理对hash的监听和解析,使得代码更清晰、易用

    var routes = {      
      '/posts': posts_index,
      '/posts/new': posts_new,
      '/posts/:id': posts_show,
      '/posts/:id/edit': posts_edit      
    };

    var router = Router(routes).configure({strict:false});
    router.init();

    function posts_index(){
      render_index()
    }

    function posts_new(){
      render_new()
    }
    function posts_show(id){      
       render_show(id) 
    }

详见 https://www.cnblogs.com/Showshare/p/director-chinese-tutorial.html

前端路由库 page.js

page.js 支持基于History api的客户端路由,并且提供了诸如重定向,页面刷新等实用功能

<script src="cdn_or_your_server/page.js"></script>
<script>
// user是一个自定义对象,可在前端动态生成CRUD各页面
page('/', user.list)
page('/user/:id', user.load, user.show)
page('/user/:id/edit', user.load, user.edit)
page('*', notfound)
// 重定向
page.redirect('/user/1');
</script>

需要配合 http-server-spa 使用

单页应用

对于采用前端路由技术的应用来说,通常只有一个主页面Index.html,该页面负责加载所有前端路由信息及js代码等,而其它所有页面实际上都是由客户端动态生成,故称之为单页应用 SPA

单页应用 Vs. 多页应用

  • 页面切换迅速,用户体验好: 因为新页面系在客户端生成,无需网络请求
  • 首屏渲染时间慢:因为首屏渲染时,既需要网络请求加载html及资源(全部js脚本等),同时还需执行js脚本,动态生成各个页面,首屏渲染的延迟时间较长
  • SEO效果差:因为搜索引擎只识别静态的html文档,不能识别js代码或生成后内容,换方之,浏览器不会执行单页应用中的js脚本

多页应用的特点正好反之;并且,上述的多页应用指的是由服务端渲染完全主导的多页应用,实际上,也可以将应用按功能的相关性划分为两个或三个单页应用,从而达到平衡。

https://juejin.cn/post/6844903906024095751

路由库与例子

参考

results matching ""

    No results matching ""