一、外围原理

1.什么是前端路由?

在 Web 前端单页利用 SPA(Single Page Application)中,路由形容的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变动引起 UI 更新(无需刷新页面)。

2.如何实现前端路由?

要实现前端路由,须要解决两个外围:

  1. 如何扭转 URL 却不引起页面刷新?
  2. 如何检测 URL 变动了?

上面别离应用 hash 和 history 两种实现形式答复下面的两个外围问题。

hash 实现

hash 是 URL 中 hash (#) 及前面的那局部,罕用作锚点在页面内进行导航,扭转 URL 中的 hash 局部不会引起页面刷新

通过 hashchange 事件监听 URL 的变动,扭转 URL 的形式只有这几种:

  1. 通过浏览器后退后退扭转 URL
  2. 通过<a>标签扭转 URL
  3. 通过window.location扭转URL
history 实现

history 提供了 pushState 和 replaceState 两个办法,这两个办法扭转 URL 的 path 局部不会引起页面刷新

history 提供相似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

  1. 通过浏览器后退后退扭转 URL 时会触发 popstate 事件
  2. 通过pushState/replaceState或<a>标签扭转 URL 不会触发 popstate 事件。
  3. 好在咱们能够拦挡 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变动
  4. 通过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>

解释下下面代码,其实很简略:

  1. 咱们通过a标签的href属性来扭转URL的hash值(当然,你触发浏览器的后退后退按钮也能够,或者在控制台输出window.location赋值来扭转hash)
  2. 咱们监听hashchange事件。一旦事件触发,就扭转routerView的内容,若是在vue中,这扭转的该当是router-view这个组件的内容
  3. 为何又监听了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>

解释下下面代码,其实也差不多:

  1. 咱们通过a标签的href属性来扭转URL的path值(当然,你触发浏览器的后退后退按钮也能够,或者在控制台输出history.go,back,forward赋值来触发popState事件)。这里须要留神的就是,当扭转path值时,默认会触发页面的跳转,所以须要拦挡 <a> 标签点击事件默认行为, 点击时应用 pushState 批改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的成果。
  2. 咱们监听popState事件。一旦事件触发,就扭转routerView的内容。
  3. 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。

  1. 装置VueRouter,再通过import VueRouter from 'vue-router'引入
  2. const router = new VueRouter({...}),再把router作为参数的一个属性值,new Vue({router})
  3. 通过Vue.use(VueRouter) 使得每个组件都能够领有store实例

从这个引入过程咱们能够发现什么?

  1. 咱们是通过new VueRouter({...})取得一个router实例,也就是说,咱们引入的VueRouter其实是一个类。

所以咱们能够初步假如

class VueRouter{}
  1. 咱们还应用了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

解释下代码:

  1. 参数Vue,咱们在第四大节剖析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。
  2. mixin的作用是将mixin的内容混合到Vue的初始参数options中。置信应用vue的同学应该应用过mixin了。
  3. 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options曾经初始化好了。
  4. 如果判断以后组件是根组件的话,就将咱们传入的router和_root挂在到根组件实例上。
  5. 如果判断以后组件是子组件的话,就将咱们_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

当初测试下胜利没

|