乐趣区

关于vue.js:手把手写一个Vuerouter无惧面试官的vueRoute题目

一、外围原理

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.js
import 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.js
class 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.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {Vue = v;};

export default VueRouter

而后再通过传进来的 Vue 创立两个组件 router-link 和 router-view

//myVueRouter.js
let 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 = false

new Vue({
  router,
  render: function (h) {return h(App) }
}).$mount('#app')

咱们能够发现这里只是将 router,也就是./router 导出的 store 实例,作为 Vue 参数的一部分。

然而这里就是有一个问题咯,这里的 Vue 是根组件啊。也就是说目前只有根组件有这个 router 值,而其余组件是还没有的,所以咱们须要让其余组件也领有这个 router。

因而,install 办法咱们能够这样欠缺

//myVueRouter.js
let 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.js
import 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.js
let 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.js
let 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.js
let 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.js
let 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

当初测试下胜利没

|

退出移动版