一、外围原理
1.什么是前端路由?
在 Web 前端单页利用 SPA(Single Page Application)中,路由形容的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变动引起 UI 更新(无需刷新页面)。
2.如何实现前端路由?
要实现前端路由,须要解决两个外围:
- 如何扭转 URL 却不引起页面刷新?
- 如何检测 URL 变动了?
上面别离应用 hash 和 history 两种实现形式答复下面的两个外围问题。
hash 实现
hash 是 URL 中 hash (#) 及前面的那局部,罕用作锚点在页面内进行导航,扭转 URL 中的 hash 局部不会引起页面刷新
通过 hashchange 事件监听 URL 的变动,扭转 URL 的形式只有这几种:
- 通过浏览器后退后退扭转 URL
- 通过
<a>
标签扭转 URL - 通过window.location扭转URL
history 实现
history 提供了 pushState 和 replaceState 两个办法,这两个办法扭转 URL 的 path 局部不会引起页面刷新
history 提供相似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:
- 通过浏览器后退后退扭转 URL 时会触发 popstate 事件
- 通过pushState/replaceState或
<a>
标签扭转 URL 不会触发 popstate 事件。 - 好在咱们能够拦挡 pushState/replaceState的调用和
<a>
标签的点击事件来检测 URL 变动 - 通过js 调用history的back,go,forward办法课触发该事件
所以监听 URL 变动能够实现,只是没有 hashchange 那么不便。
vue实战视频解说:进入学习
二、原生js实现前端路由
1.基于 hash 实现
html
<!DOCTYPE html><html lang="en"><body><ul> <ul> <!-- 定义路由 --> <li><a href="#/home">home</a></li> <li><a href="#/about">about</a></li> <!-- 渲染路由对应的 UI --> <div id="routeView"></div> </ul></ul></body><script> let routerView = routeView window.addEventListener('hashchange', ()=>{ let hash = location.hash; routerView.innerHTML = hash }) window.addEventListener('DOMContentLoaded', ()=>{ if(!location.hash){//如果不存在hash值,那么重定向到#/ location.hash="/" }else{//如果存在hash值,那就渲染对应UI let hash = location.hash; routerView.innerHTML = hash } })</script></html>
解释下下面代码,其实很简略:
- 咱们通过a标签的href属性来扭转URL的hash值(当然,你触发浏览器的后退后退按钮也能够,或者在控制台输出window.location赋值来扭转hash)
- 咱们监听hashchange事件。一旦事件触发,就扭转routerView的内容,若是在vue中,这扭转的该当是router-view这个组件的内容
- 为何又监听了load事件?这时应为页面第一次加载完不会触发 hashchange,因此用load事件来监听hash值,再将视图渲染成对应的内容。
2.基于 history 实现
<!DOCTYPE html><html lang="en"><body><ul> <ul> <li><a href='/home'>home</a></li> <li><a href='/about'>about</a></li> <div id="routeView"></div> </ul></ul></body><script> let routerView = routeView window.addEventListener('DOMContentLoaded', onLoad) window.addEventListener('popstate', ()=>{ routerView.innerHTML = location.pathname }) function onLoad () { routerView.innerHTML = location.pathname var linkList = document.querySelectorAll('a[href]') linkList.forEach(el => el.addEventListener('click', function (e) { e.preventDefault() history.pushState(null, '', el.getAttribute('href')) routerView.innerHTML = location.pathname })) }</script></html>
解释下下面代码,其实也差不多:
- 咱们通过a标签的href属性来扭转URL的path值(当然,你触发浏览器的后退后退按钮也能够,或者在控制台输出history.go,back,forward赋值来触发popState事件)。这里须要留神的就是,当扭转path值时,默认会触发页面的跳转,所以须要拦挡
<a>
标签点击事件默认行为, 点击时应用 pushState 批改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的成果。 - 咱们监听popState事件。一旦事件触发,就扭转routerView的内容。
- load事件则是一样的
有个问题:hash模式,也能够用history.go,back,forward
来触发hashchange事件吗?
A:也是能够的。因为不论什么模式,浏览器为保留记录都会有一个栈。
三、基于Vue实现VueRouter
咱们先利用vue-cli建一个我的项目
代码如下:
App.vue
<template> <div id="app"> <div id="nav"> <router-link to="/home">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view/> </div></template>
router/index.js
import Vue from 'vue'import VueRouter from 'vue-router'import Home from '../views/Home.vue'import About from "../views/About.vue"Vue.use(VueRouter) const routes = [ { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }]const router = new VueRouter({ mode:"history", routes})export default router
Home.vue
<template> <div class="home"> <h1>这是Home组件</h1> </div></template>
About.vue
<template> <div class="about"> <h1>这是about组件</h1> </div></template>
当初咱们启动一下我的项目。看看我的项目初始化有没有胜利。
ok,没故障,初始化胜利。
当初咱们决定创立本人的VueRouter,于是创立myVueRouter.js文件
目前目录如下
再将VueRouter引入 改成咱们的myVueRouter.js
//router/index.jsimport Vue from 'vue'import VueRouter from './myVueRouter' //批改代码import Home from '../views/Home.vue'import About from "../views/About.vue"Vue.use(VueRouter) const routes = [ { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }];const router = new VueRouter({ mode:"history", routes})export default router
四、分析VueRouter实质
先抛出个问题,Vue我的项目中是怎么引入VueRouter。
- 装置VueRouter,再通过
import VueRouter from 'vue-router'
引入 - 先
const router = new VueRouter({...})
,再把router作为参数的一个属性值,new Vue({router})
- 通过Vue.use(VueRouter) 使得每个组件都能够领有store实例
从这个引入过程咱们能够发现什么?
- 咱们是通过new VueRouter({...})取得一个router实例,也就是说,咱们引入的VueRouter其实是一个类。
所以咱们能够初步假如
class VueRouter{}
- 咱们还应用了Vue.use(),而Vue.use的一个准则就是执行对象的install这个办法
所以,咱们能够再一步 假如VueRouter有有install这个办法。
class VueRouter{}VueRouter.install = function () {}
到这里,你能大略地将VueRouter写进去吗?
很简略,就是将下面的VueRouter导出,如下就是myVueRouter.js
//myVueRouter.jsclass VueRouter{}VueRouter.install = function () {}export default VueRouter
五、剖析Vue.use
Vue.use(plugin);
(1)参数
{ Object | Function } plugin
(2)用法
装置Vue.js插件。如果插件是一个对象,必须提供install办法。如果插件是一个函数,它会被作为install办法。调用install办法时,会将Vue作为参数传入。install办法被同一个插件屡次调用时,插件也只会被装置一次。
对于如何上开发Vue插件,请看这篇文章,非常简单,不必两分钟就看完:如何开发 Vue 插件?
(3)作用
注册插件,此时只须要调用install办法并将Vue作为参数传入即可。但在细节上有两局部逻辑要解决:
1、插件的类型,能够是install办法,也能够是一个蕴含install办法的对象。
2、插件只能被装置一次,保障插件列表中不能有反复的插件。
(4)实现
Vue.use = function(plugin){ const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])); if(installedPlugins.indexOf(plugin)>-1){ return this; } <!-- 其余参数 --> const args = toArray(arguments,1); args.unshift(this); if(typeof plugin.install === 'function'){ plugin.install.apply(plugin,args); }else if(typeof plugin === 'function'){ plugin.apply(null,plugin,args); } installedPlugins.push(plugin); return this;}
1、在Vue.js上新增了use办法,并接管一个参数plugin。
2、首先判断插件是不是曾经别注册过,如果被注册过,则间接终止办法执行,此时只须要应用indexOf办法即可。
3、toArray办法咱们在就是将类数组转成真正的数组。应用toArray办法失去arguments。除了第一个参数之外,残余的所有参数将失去的列表赋值给args,而后将Vue增加到args列表的最后面。这样做的目标是保障install办法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
4、因为plugin参数反对对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户应用哪种形式祖册的插件,而后执行用户编写的插件并将args作为参数传入。
5、最初,将插件增加到installedPlugins中,保障雷同的插件不会重复被注册。(~~让我想起了已经面试官问我为什么插件不会被从新加载!!!哭唧唧,当初总算明确了)
第三点讲到,咱们把Vue作为install的第一个参数,所以咱们能够把Vue保存起来
//myVueRouter.jslet Vue = null;class VueRouter{}VueRouter.install = function (v) { Vue = v;};export default VueRouter
而后再通过传进来的Vue创立两个组件router-link和router-view
//myVueRouter.jslet Vue = null;class VueRouter{}VueRouter.install = function (v) { Vue = v; console.log(v); //新增代码 Vue.component('router-link',{ render(h){ return h('a',{},'首页') } }) Vue.component('router-view',{ render(h){ return h('h1',{},'首页视图') } })};export default VueRouter
咱们执行下我的项目,如果没报错,阐明咱们的假如没故障。
天啊,没报错。没故障!
六、欠缺install办法
install 个别是给每个vue实例增加货色的
在这里就是给每个组件增加$route
和$router
。
$route
和$router
有什么区别?
A:$router
是VueRouter的实例对象,$route
是以后路由对象,也就是说$route
是$router
的一个属性
留神每个组件增加的$route
是是同一个,$router
也是同一个,所有组件共享的。
这是什么意思呢???
来看mian.js
import Vue from 'vue'import App from './App.vue'import router from './router'Vue.config.productionTip = falsenew Vue({ router, render: function (h) { return h(App) }}).$mount('#app')
咱们能够发现这里只是将router ,也就是./router导出的store实例,作为Vue 参数的一部分。
然而这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其余组件是还没有的,所以咱们须要让其余组件也领有这个router。
因而,install办法咱们能够这样欠缺
//myVueRouter.jslet Vue = null;class VueRouter{}VueRouter.install = function (v) { Vue = v; // 新增代码 Vue.mixin({ beforeCreate(){ if (this.$options && this.$options.router){ // 如果是根组件 this._root = this; //把以后实例挂载到_root上 this._router = this.$options.router; }else { //如果是子组件 this._root= this.$parent && this.$parent._root } Object.defineProperty(this,'$router',{ get(){ return this._root._router } }) } }) Vue.component('router-link',{ render(h){ return h('a',{},'首页') } }) Vue.component('router-view',{ render(h){ return h('h1',{},'首页视图') } })};export default VueRouter
解释下代码:
- 参数Vue,咱们在第四大节剖析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。
- mixin的作用是将mixin的内容混合到Vue的初始参数options中。置信应用vue的同学应该应用过mixin了。
- 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options曾经初始化好了。
- 如果判断以后组件是根组件的话,就将咱们传入的router和_root挂在到根组件实例上。
- 如果判断以后组件是子组件的话,就将咱们_root根组件挂载到子组件。留神是援用的复制,因而每个组件都领有了同一个_root根组件挂载在它身上。
这里有个问题,为什么判断以后组件是子组件,就能够间接从父组件拿到_root根组件呢?这让我想起了已经一个面试官问我的问题:父组件和子组件的执行程序?
A:父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted
能够失去,在执行子组件的beforeCreate的时候,父组件曾经执行完beforeCreate了,那天经地义父组件曾经有_root了。
而后咱们通过
Object.defineProperty(this,'$router',{ get(){ return this._root._router }})
将$router
挂载到组件实例上。
其实这种思维也是一种代理的思维,咱们获取组件的$router
,其实返回的是根组件的_root._router
到这里还install还没写完,可能你也发现了,$route
还没实现,当初还实现不了,没有欠缺VueRouter的话,没方法取得以后门路
七、欠缺VueRouter类
咱们先看看咱们new VueRouter类时传进了什么东东
//router/index.jsimport Vue from 'vue'import VueRouter from './myVueRouter'import Home from '../views/Home.vue'import About from "../views/About.vue"Vue.use(VueRouter) const routes = [ { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }];const router = new VueRouter({ mode:"history", routes})export default router
可见,传入了一个为数组的路由表routes,还有一个代表 以后是什么模式的mode。因而咱们能够先这样实现VueRouter
class VueRouter{ constructor(options) { this.mode = options.mode || "hash" this.routes = options.routes || [] //你传递的这个路由是一个数组表 }}
先接管了这两个参数。
然而咱们间接解决routes是非常不不便的,所以咱们先要转换成key:value
的格局
//myVueRouter.jslet Vue = null;class VueRouter{ constructor(options) { this.mode = options.mode || "hash" this.routes = options.routes || [] //你传递的这个路由是一个数组表 this.routesMap = this.createMap(this.routes) console.log(this.routesMap); } createMap(routes){ return routes.reduce((pre,current)=>{ pre[current.path] = current.component return pre; },{}) }}
通过createMap咱们将
const routes = [ { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }
转换成
路由中须要寄存以后的门路,来示意以后的门路状态
为了方便管理,能够用一个对象来示意
//myVueRouter.jslet Vue = null;新增代码class HistoryRoute { constructor(){ this.current = null }}class VueRouter{ constructor(options) { this.mode = options.mode || "hash" this.routes = options.routes || [] //你传递的这个路由是一个数组表 this.routesMap = this.createMap(this.routes) 新增代码 this.history = new HistoryRoute(); } createMap(routes){ return routes.reduce((pre,current)=>{ pre[current.path] = current.component return pre; },{}) }}
然而咱们当初发现这个current也就是 以后门路还是null,所以咱们须要进行初始化。
初始化的时候判断是是hash模式还是 history模式。,而后将以后门路的值保留到current里
//myVueRouter.jslet Vue = null;class HistoryRoute { constructor(){ this.current = null }}class VueRouter{ constructor(options) { this.mode = options.mode || "hash" this.routes = options.routes || [] //你传递的这个路由是一个数组表 this.routesMap = this.createMap(this.routes) this.history = new HistoryRoute(); 新增代码 this.init() } 新增代码 init(){ if (this.mode === "hash"){ // 先判断用户关上时有没有hash值,没有的话跳转到#/ location.hash? '':location.hash = "/"; window.addEventListener("load",()=>{ this.history.current = location.hash.slice(1) }) window.addEventListener("hashchange",()=>{ this.history.current = location.hash.slice(1) }) } else{ location.pathname? '':location.pathname = "/"; window.addEventListener('load',()=>{ this.history.current = location.pathname }) window.addEventListener("popstate",()=>{ this.history.current = location.pathname }) } } createMap(routes){ return routes.reduce((pre,current)=>{ pre[current.path] = current.component return pre; },{}) }}
监听事件跟下面原生js实现的时候统一。
八、欠缺$route
后面那咱们讲到,要先实现VueRouter的history.current的时候,能力取得以后的门路,而当初曾经实现了,那么就能够着手实现$route
了。
很简略,跟实现$router
一样
VueRouter.install = function (v) { Vue = v; Vue.mixin({ beforeCreate(){ if (this.$options && this.$options.router){ // 如果是根组件 this._root = this; //把以后实例挂载到_root上 this._router = this.$options.router; }else { //如果是子组件 this._root= this.$parent && this.$parent._root } Object.defineProperty(this,'$router',{ get(){ return this._root._router } }); 新增代码 Object.defineProperty(this,'$route',{ get(){ return this._root._router.history.current } }) } }) Vue.component('router-link',{ render(h){ return h('a',{},'首页') } }) Vue.component('router-view',{ render(h){ return h('h1',{},'首页视图') } })};
九、欠缺router-view组件
当初咱们曾经保留了以后门路,也就是说当初咱们能够取得以后门路,而后再依据以后门路从路由表中获取对应的组件进行渲染
Vue.component('router-view',{ render(h){ let current = this._self._root._router.history.current let routeMap = this._self._root._router.routesMap; return h(routeMap[current]) }})
解释一下:
render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而咱们后面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。
所以咱们能够从router实例上取得路由表,也能够取得以后门路。
而后再把取得的组件放到h()里进行渲染。
当初曾经实现了router-view组件的渲染,然而有一个问题,就是你扭转门路,视图是没有从新渲染的,所以须要将_router.history进行响应式化。
Vue.mixin({ beforeCreate(){ if (this.$options && this.$options.router){ // 如果是根组件 this._root = this; //把以后实例挂载到_root上 this._router = this.$options.router; 新增代码 Vue.util.defineReactive(this,"xxx",this._router.history) }else { //如果是子组件 this._root= this.$parent && this.$parent._root } Object.defineProperty(this,'$router',{ get(){ return this._root._router } }); Object.defineProperty(this,'$route',{ get(){ return this._root._router.history.current } }) }})
咱们利用了Vue提供的API:defineReactive,使得this._router.history对象失去监听。
因而当咱们第一次渲染router-view这个组件的时候,会获取到this._router.history
这个对象,从而就会被监听到获取this._router.history
。就会把router-view组件的依赖wacther收集到this._router.history
对应的收集器dep中,因而this._router.history
每次扭转的时候。this._router.history
对应的收集器dep就会告诉router-view的组件依赖的wacther执行update(),从而使得router-view
从新渲染(其实这就是vue响应式的外部原理)
好了,当初咱们来测试一下,通过扭转url上的值,能不能触发router-view的从新渲染
path改成home
可见胜利实现了以后门路的监听。。
十、欠缺router-link组件
咱们先看下router-link是怎么应用的。
<router-link to="/home">Home</router-link> <router-link to="/about">About</router-link>
也就是说父组件间to这个门路传进去,子组件接管就好
因而咱们能够这样实现
Vue.component('router-link',{ props:{ to:String }, render(h){ let mode = this._self._root._router.mode; let to = mode === "hash"?"#"+this.to:this.to return h('a',{attrs:{href:to}},this.$slots.default) }})
咱们把router-link渲染成a标签,当然这时最简略的做法。
通过点击a标签就能够实现url上门路的切换。从而实现视图的从新渲染
ok,到这里实现此次的我的项目了。
看下VueRouter的残缺代码吧
//myVueRouter.jslet Vue = null;class HistoryRoute { constructor(){ this.current = null }}class VueRouter{ constructor(options) { this.mode = options.mode || "hash" this.routes = options.routes || [] //你传递的这个路由是一个数组表 this.routesMap = this.createMap(this.routes) this.history = new HistoryRoute(); this.init() } init(){ if (this.mode === "hash"){ // 先判断用户关上时有没有hash值,没有的话跳转到#/ location.hash? '':location.hash = "/"; window.addEventListener("load",()=>{ this.history.current = location.hash.slice(1) }) window.addEventListener("hashchange",()=>{ this.history.current = location.hash.slice(1) }) } else{ location.pathname? '':location.pathname = "/"; window.addEventListener('load',()=>{ this.history.current = location.pathname }) window.addEventListener("popstate",()=>{ this.history.current = location.pathname }) } } createMap(routes){ return routes.reduce((pre,current)=>{ pre[current.path] = current.component return pre; },{}) }}VueRouter.install = function (v) { Vue = v; Vue.mixin({ beforeCreate(){ if (this.$options && this.$options.router){ // 如果是根组件 this._root = this; //把以后实例挂载到_root上 this._router = this.$options.router; Vue.util.defineReactive(this,"xxx",this._router.history) }else { //如果是子组件 this._root= this.$parent && this.$parent._root } Object.defineProperty(this,'$router',{ get(){ return this._root._router } }); Object.defineProperty(this,'$route',{ get(){ return this._root._router.history.current } }) } }) Vue.component('router-link',{ props:{ to:String }, render(h){ let mode = this._self._root._router.mode; let to = mode === "hash"?"#"+this.to:this.to return h('a',{attrs:{href:to}},this.$slots.default) } }) Vue.component('router-view',{ render(h){ let current = this._self._root._router.history.current let routeMap = this._self._root._router.routesMap; return h(routeMap[current]) } })};export default VueRouter
当初测试下胜利没
|