一、外围原理
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.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。
- 装置 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.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
解释下代码:
- 参数 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.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
当初测试下胜利没
|