vue-路由原理及实现

1,095 阅读6分钟

路由概念

  • 你了解什么是路由么?

了解,路由的概念最先来源于服务端,主要是用来描述URL和处理函数之间的映射。目前,路由主要分为两种,前端路由和后端路由。

  • 后端路由
http://www.xx.com/login

后端路由又称作服务器端路由,当服务器接收到客户端的请求时,就会根据所请求的URL,来找到相应的映射函数,然后执行函数,把函数的返回值送给客户端。

优点:安全性好,有利于SEO

缺点:服务器压力大,不利于用户体验,代码不好维护。

  • 前端路由

前端路由主要描述的是URL和UI之间的映射关系,前端路由的映射函数通常是处理一些dom的显示和隐藏,访问不同的路径显示不同的页面。

优点:访问页面不会重新刷新加载,没有网络延迟,用户体验好.

缺点:使用浏览器的后退前进会重新发送请求,没有合理的利用缓存,还有一点,不利于SEO。

  • 如何实现前端路由?

前端路由实现很简单,就是匹配不同的URL路径,进行解析,然后动态的渲染出页面,在这过程中,首先需要监测URL的变化,一旦改变,进行渲染,同时还要保证,只进行UI更改,不引起页面刷新。

  • 前端路由实现方案有几种?

主要有两种实现方式:

  • hash
  • history api

hash路由实现

hash模式的路由,在URL后面会带一个#号,而hash指的是#及后面的那部分内容。

  • URL中的hash只是客户端的一种状态,常用作锚点在页面里进行导航,当页面向服务完全发送请求的时候,hash部分并不会被发送,所以改变hash也不会引起页面刷新。

  • 我们可以通过hashchange事件来监听hash的改变,从而动态的渲染出页面。

  • hash值的改变方式有几种:

    • 通过浏览器前进后退改变URL
    • 改变location.href和loca.hash的值
    • 触发带锚点的链接

实现一个简单的hash路由:

1.在html中,指定一个钩子元素routerview,在这里实现不同页面的渲染。同时绑定html标签需要跳转链接的点击事件。

2.实现一个Router,主要作用是,监听hashchange事件,当hash变化时,触发回调函数handler进行处理。

3.在handler函数,处理不同页面的渲染。

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <meta name="viewport">
  <title>router</title>
  <script src='js/jquery.min.js'></script>
</head>
<body>
	<div>
      <a href="#/">主页</a>
      <a href="#/login">login</a>
      <a href="#/register">register</a>
      <span id="luyou4">userInfo用户中心</span>
	</div>
	<div id="routerView"></div>
	<script type="text/javascript">
	   var luyou4 = document.getElementById('luyou4')
	   luyou4.addEventListener('click',function(){
	  		route.push('/userInfo')
	  	})
	  function Router(routerList){
	  	// 路由列表
	  	this.$routerList = routerList // 路由列表
	  	this.$courrent = this.getState()// 当前路由
	  	this.$routerView = document.getElementById('routerView')//要渲染的节点
	  	this.handler = this.handler.bind(this)
	  	this.handler();// 初次渲染
	  	window.addEventListener('hashchange',this.handler,false)//监听
	  }
	  Router.prototype = {
	  	// 处理回调
	  	handler(){
	  		// console.log('hander')
	  		this.$courrent = this.getState()
	  		this.render(this.$courrent)
	  	},
	  	// 重新渲染页面
	  	render(path){
	  		let compentent = this.$routerList.filter((val)=>{
	  			return val.path === path
	  		})
	  		this.$routerView.innerHTML = compentent[0].compentent
	  	},
	  	getState(){
	  		// #号及后面的值,比如 localhost:1234/#/login
	  		// hash = #/login
	  		const hash = window.location.hash
	  		return hash ? hash.slice(1) : '/'
	  	},
	  	push(path){
	  		window.location.hash = '#' + path
	  	},
	  	// 获取 默认页 url
	  	getUrl(path) {
	  		const href = window.location.href;
	  		const i = href.indexOf('#');
	  		const base = i >= 0 ? href.slice(0, i) : href;
	  		return base +'#'+ path;
	  	},
        // 替换页面
        replace(path) {
  	       window.location.replace(this.getUrl(path));
        },
	  }
	  let route = new Router([
	  {
	  	path:'/',
	  	compentent:'index'
	  },
	  {
	  	path:'/login',
	  	compentent:'login'
	  },
	  {
	  	path:'/register',
	  	compentent:'register'
	  },
	  {
	  	path:'/userInfo',
	  	compentent:'userInfo'
	  },
	  ])
	</script>
</body>
</html>

history路由实现

history路由模式实现是基于HTML5,HTML5中新增了两个API,pushState,replaceState。

  • pushState,replaceState改变URL的路径部分,不会引起页面刷新。

  • 我们可以通过popstate事件来检测URL的变化。但是pushState,replaceState事件不会触发popstate事件,需要手动触发。

  • history路由模式没有#号,是真实的URL,刷新页面,这种模式会被服务器识别,服务器会对URL的文件路径进行资源查找,找不到就会报404问题,所以,在我们使用history路由模式时,需要与后端进行沟通。

404问题的处理?

配置webpack(开发环境)

historyApiFallback:{
    index:'/index.html'//index.html为当前目录创建的template.html
}

配置nginx(生产环境):如果匹配不到静态资源的路径,就将重定向到 index 页面。

location /{
    root   /data/nginx/html;
    index  index.html index.htm;
    error_page 404 /index.html;
}

window.history及api

  • history.back()后退
  • history.forward()前进
  • history.go()接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back(),go(0)相当于刷新当前页面
  • history.pushState()
  • history.replaceState()

history.pushState(state,title,url)

此方法向历史记录中写入数据,并不跳转,该方法接受三个参数。

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

history.replaceState(state,title,url)

该方法参数与pushstate是相同的,区别是,它修改浏览器历史记录,同样不会触发跳转。

popstate事件

当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

  • 仅仅调用pushState方法或replaceState方法 ,并不会触发该事件
  • 用户点击浏览器倒退按钮和前进按钮触发
  • 使用 JavaScript 调用back、forward、go方法时会触发。

如何监听 pushState 和 replaceState 的变化呢?

1.重写方法,使用window.dispatchEvent()自定义触发事件,然后使用window.addEventListener()全局监听所定义的触发事件。

2.在每次调用的时候,执行handler处理函数。

实现一个简单的history模式路由

1.在html中,指定一个钩子元素routerview,在这里实现不同页面的渲染。同时绑定html标签需要跳转链接的点击事件。

2.实现一个Router,主要作用是,监听popState事件,当页面path变化时,触发回调函数handler进行处理。因为单独调用pushState,replaceState不会主动触发popState事件,所以我们这里,在调用之后,手动触发handler事件。或者重写pushState,replaceState函数。

3.在handler函数,处理不同页面的渲染。

把html文档丢到nginx,开启服务,才能看到效果,单独运行html会报错,无法正确获取当前的URL。

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <meta name="viewport">
  <title>router</title>
  <script src='js/jquery.min.js'></script>
</head>
<body>
	<div>
      <a class="route" href="/">主页</a>
      <a class="route" href="/login">login</a>
      <a class="route" href="/register">register</a>
      <span class="route" data-href="/userInfo" id="luyou4">userInfo用户中心</span>
	</div>
	<div id="routerView">

	</div>
	<script type="text/javascript">

	  var _wr = function(type){
	  	var orig = history[type]
	  	return function(){
	  		var rv = orig.apply(this,arguments)
	  		var e = new Event(type)
	  		e.arguments = arguments
	  		//window.dispatchEvent(),是个自定义触发事件。每次调用pushState时,会触发,同时又会触发window的监听pushState事件。
	  		window.dispatchEvent(e)
	  		return rv
	  	}
	  }
	  // 重写事件,给它添加监听
	  history.pushState = _wr('pushState')
	  history.replaceState  = _wr('replaceState ')

	  function linkAddEvent(){
	  		document.querySelectorAll('.route').forEach(item=>{
	  			item.addEventListener('click',e=>{
	  				let path;
	  				if(!!item['attributes']['href']){
	  				  e.preventDefault();
	  				  path = item['attributes']['href'].value
	  				} else {
	  					path = item['attributes']['data-href'].value
	  				}
	  				console.log(path)
	  				if(!!(window.history && window.history.pushState)){
	  					route.push(path)
	  				}
	  			})
	  		})
	  	}
	  	linkAddEvent();
	  function Router(routerList){
	  	// 路由列表
	  	this.$routerList = routerList // 路由列表
	  	this.$courrent = this.getState()// 当前路由
	  	this.$routerView = document.getElementById('routerView')//要渲染的节点
	  	this.handler = this.handler.bind(this)
	  	this.handler();// 初次渲染
	  	window.addEventListener('popstate',this.handler,false)//监听
	  	window.addEventListener('replaceState',this.handler,false)//监听
	  	window.addEventListener('pushState',this.handler,false)//监听
	  }
	  Router.prototype = {
	  	// 处理回调
	  	handler(){
	  		// console.log('hander')
	  		this.$courrent = this.getState()
	  		this.render(this.$courrent)
	  	},
	  	// 重新渲染页面
	  	render(path){
	  		console.log('render',path)
	  		let compentent = this.$routerList.filter((val)=>{
	  			return val.path === path
	  		})
	  		this.$routerView.innerHTML = compentent[0].compentent
	  	},
	  	getState(){
	  		const path = window.location.pathname
	  		return path ? path : '/'
	  	},
	  	push(path){
	  		history.pushState(null,null,path)
	  		// this.handler()
	  	},
	  	// replace 页面
	  	replace(path) {
	  		history.replaceState(null, null, path);
	  		// this.handler();
	  	},
	  	// 前进 or 后退浏览历史
	  	go(n) {
	  		window.history.go(n);
	  	},
	  }
	  let route = new Router([
	  {
	  	path:'/',
	  	compentent:'index'
	  },
	  {
	  	path:'/login',
	  	compentent:'login'
	  },
	  {
	  	path:'/register',
	  	compentent:'register'
	  },
	  {
	  	path:'/userInfo',
	  	compentent:'userInfo'
	  },
	  ])
	</script>
</body>
</html>

路由传参

  • query

通过?query=query1形式表现在URL上。

使用path和name都可以。

  • params

不能使用path,只能用name。

刷新数据就会消失。可以用localstorage存储。

用于动态路由。

路由守卫

守卫的作用:主要用来通过跳转或取消的方式守卫导航。

有点绕口,简单来说,导航守卫就是路由跳转过程中的一些钩子函数,路由跳转是一个大的过程,类似于生命周期,而其中有许多函数,可以做一些其他的事。

路由守卫有三种:全局守卫,路由独享守卫,组件守卫。

路由的生命周期

-> 导航被触发

-> beforeRouteLeave()当前导航的组件守卫

-> 进入另一个路由

----> beforeCreate ----vue 生命周期

-> beforeEach() 全局前置守卫

-> beforeEnter()路由独享守卫

-> beforeRouteEnter()组件内守卫

-> beforeResolve()全局解析守卫

-> afterEach()全局后置守卫

----> mounted ----vue 生命周期

路由生命周期

回调参数(to,form,next)介绍

先来解说回调参数。

to:即将要进入的目标路由对象。

from:即将要离开的路由对象。

next:涉及到next参数的钩子,必须要调用next()这个方法来来resolve这个钩子,否则路由中断,不再继续往下执行。

  • next(): 进入下一个钩子。
  • next(false):中断当前导航。
  • next(path):跳转到另一个地址。
  • next(error):如果传入的是一个Error实例,导航终止,并将错误传递给回调函数。

全局守卫

全局守卫是在路由实例上直接操作的钩子函数,所有路由配置的组件都会触发。它有三个钩子函数:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

router.afterEach((to, from) => {
  // ...
})

  • beforeEach(to,from,next):全局前置守卫,主要用于验证登录。

  • beforeResolve(to,from,next):全局解析守卫,在路由被确认之前,同时所有组件内守卫和异步路由被解析之后调用。

  • afterEach(to,form):全局后置守卫,在路由跳转完成之后触发,不会接受next也不会改变导航本身。

路由独享守卫

路由独享守卫是配置在单个路由时设置的钩子。

let routes = [
  {
      path:'/user',
      component:User,
      beforeEnter:(to,form,next)=>{
          //do something……
      }
  }
]
  • beforeEnter(to,form,next):和beforeEach一致,在beforeEach之后执行。

组件守卫

组件守卫是在组件内执行的钩子函数。

export default{
  data(){
    //...
  },
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}
  • beforeRouteEnter(to,form,next):该守卫内访问不到组件实例,this为undefined,因为它在组件生命周期beforeCreate阶段触发。

  • beforeRouteUpdate(to,form,next):在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。

  • beforeRouteLeave(to,form,next):导航离开该组件的对应路由时调用,可以访问组件实例this。这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过next( false )来取消。

参考

Vue - 路由守卫(路由的生命周期)