# 前端路由原理

访问一个网站,假如我们访问这三个页面:

http://www.sina.com.cn/nba/

http://www.sina.com.cn/tennis/

http://www.sina.com.cn/csl/

那么其路径就分别是 /nba,/tennis,/csl

当用户使用http://www.sina.com.cn/nba/来访问该页面时,Web 服务会接收到这个请求,然后会解析 URI 中的路径 /nba,在 Web 服务的程序中,该路径对应着相应的处理逻辑,程序会把请求交给路径所对应的处理逻辑,这样就完成了一次「路由分发」,这个分发就是通过「路由」来完成的。

# 前端路由由来

随着前端技术的发展,现在很多web应用都采用了SPA的形式,之前是通过服务端根据 url 的不同返回不同的页面实现无法满足需求,所以这里需要把不同路由对应不同的内容或页面的任务交给前端来做。

# 优势与劣势

通过这种前端路由实现的单页面应用有几个好处:

  1. 良好的前后端分离。SPA和RESTful架构一起使用,后端不再负责模板渲染、输出页面工作,web前端和各种移动终端地位对等,后端API通用化。
  2. 用户体验好、快,内容的改变不需要重新加载整个页面,web应用更具响应性和更令人着迷
  3. 同一套后端程序代码,不用修改就可以用于Web界面、手机、平板、等多种客户端

但是同样也有其缺点:

  1. 不利于SEO。
  2. 初次加载耗时相对增多。
  3. 导航不可用,如果一定要导航需要自行实现前进、后退。

接下来我们来看一下前端的路由是如何实现的。

# 路由实现原理

在HTML5的 history API 出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器。当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不同的操作,进行不同的内容的展示。

# Hash路由

html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<div class="container">
		<nav>
			<ul>
				<li><a href="#/">首页</a></li>
				<li><a href="#/new">新闻</a></li>
				<li><a href="#/product">产品</a></li>
				<li><a href="#/about">关于我们</a></li>
			</ul>

		</nav>
	</div>

	<div id="content">

	</div>
	<script>
	var content = document.querySelector('#content')
	var r = Router({
		'/':function(){
			content.innerHTML = "首页"
		},
		'/new':function(){
			content.innerHTML = "新闻"
		},
		'/product':function(){
			content.innerHTML = "产品"
		},
		'/about':function(){
			content.innerHTML = "关于我们"
		},
	})
	</script>
</body>
</html>

js:

function _route(option){
	this.option = option || {};
}
_route.prototype.on = function(ev,callback){
	var evs = ev.split(" ");
	for(var i=0;i<evs.length;i++){
		window.addEventListener(evs[i],callback)
	}
}

_route.prototype.init = function(){
	this.on("load hashchange",this.refresh.bind(this))
}

_route.prototype.refresh = function(){
	this.currentUrl = location.hash.slice(1)||"/";
	this.option[this.currentUrl]();
}

_route.prototype.rt = function(path,callback){
	this.option[path] = callback;
	return this;
}

function Router(option){
	var r = new _route(option);
	r.init();
	return r;
}

# HTML5 history API路由实现

html5 增加了两个方法,分别是pushStatereplaceState

pushStatereplaceState是用来手动插入历史记录,然后执行AJAX请求,而popstate就是当浏览器前进后退的时候获得相应的state再执行相应操作。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>History Roter</title>
</head>
<body>
	<div class="container">
		<nav>
			<ul>
				<li><a href="#/">首页</a></li>
				<li><a href="#/new">新闻</a></li>
				<li><a href="#/product">产品</a></li>
				<li><a href="#/about">关于我们</a></li>
			</ul>
		</nav>
	</div>

	<div id="content"></div>
    <script>
	class Router {
	     constructor(routes = {}) {
	          this.routes = routes;
	          this.curUrl = "";
	      }
	      notify(state){
	        this.curUrl = state.path || '/';
	        if(this.routes[this.curUrl]){
	          this.routes[this.curUrl]();
	        }else{
	          console.log('路由没有注册');
	        }
	      }
	      route(path,callback){
	        this.routes[path] = callback || function(){

	        }
	      }
	      init(){
	        let that = this;
	        //浏览器点击前进后退时触发的事件
	         window.addEventListener('popstate',function(event){
	            that.notify(event.state || {})
	         }, false);

	         //监控页面A标签,跳转做处理
	         document.querySelector('body').addEventListener('click', function(event){
	          if(event.target.tagName === 'A'){
	            let link = event.target.getAttribute('href');
	            if(!/^http/.test(link)){
	              event.preventDefault();
	              let path = link.slice(1) || '/';
	              that.notify({ 'path' : path} || {})
	              if(event.target.getAttribute('type') == 'replace'){
	                history.replaceState({ 'path' : path},'',event.target.href);
	              }else{
	                history.pushState({ 'path' : path},'',event.target.href);
	              }             
	            }       
	          }   
	         }, false)

	         //首次进入页面进行路由
	        let path = location.hash.slice(1) || '/';
	        that.notify({ 'path' : path} || {})
	      }
	  }

	  function changeContent(text){
	    document.getElementById('content').innerHTML = text;
	  }
	  let router = new Router({
	    '/':function(){
	    	changeContent('首页');
			},
			'/new':function(){
	    	changeContent('新闻');
			},
			'/product':function(){
	      changeContent('产品');
			}
	  })

	  router.route('/about',function(){
	    changeContent('关于我们');
	  })

	  router.init();
    </script>
</body>

</html>

# 附录