前端路由与单页应用
什么是路由 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)定义,一般是页面或文件等静态资源的简单映射,即静态路由
- http://x.com/posts/1.html -> server:/www/posts/1.html
- 由服务端脚本语言来定义,如php\jsp\asp等: 功能更强,可实现动态路由,可对应于访问服务器上的任何资源,如文件、数据库、进程等
- http://x.com/posts/1 -> server:/www/blog/posts.php?id=1
- http://x.com/posts/1 -> server:/www/blog/index.php?q=posts/1
静态路由与动态路由
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>
路由实现技术比较
- 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脚本
多页应用的特点正好反之;并且,上述的多页应用指的是由服务端渲染完全主导的多页应用,实际上,也可以将应用按功能的相关性划分为两个或三个单页应用,从而达到平衡。
路由库与例子
- 框架中立的route.js https://github.com/riot/route
- 编写路由的中文例子 https://github.com/biaochenxuying/route
参考
- 前端路由hash和history概念及实现原理 https://zhuanlan.zhihu.com/p/408209439
- https://www.jianshu.com/p/d2aa8fb951e4
- https://developer.mozilla.org/zh-CN/docs/Web/API/History
- https://blog.csdn.net/weixin_42597880/article/details/102558077
- https://www.cnblogs.com/hixxcom/p/7741689.html
- https://juejin.cn/post/6844903906024095751