前言
在上一篇文章中,我简单的带大家了解了一下前端路由的原理,这次,我将带大家手写一个属于自己的VueRouter。
创建项目
我们首先利用Vue-cli创建一个Vue项目,记得勾选安装Router选项。
删除一些没用的文件后项目结构如下图所示:
具体文件的代码如下: App.vue:
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
route/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
Home.vue
<template>
<div class="home">
<h1>this is home page</h1>
</div>
</template>
About.vue
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
至此,一切准备工作完成,现在我们将VueRouter的引入改成我们自己的myVueRouter.js
import VueRouter from './myVueRouter.js'
分析
通过观察原始项目,我们可以发现VueRouter({...})就是一个构造函数,因为他通过new创建了一个实例router,所以,我们可以很容易得出结论:VueRouter的本质是一个类
所以我们可以写出如下代码:
class VueRouter{}
我们还用使用了Vue.use()方法,使每个组件都可以拥有store的实例,而use方法的原则就是执行对象的install方法。
所以,补充代码如下:
class VueRouter {}
VueRouter.install = function(){}
最后将VueRouter导出即可
class VueRouter {}
VueRouter.install = function(){}
export default VueRouter
对于install方法,我们需要保证install方法被执行的时候第一个参数是Vue,其余参数是注册插件时传入的参数,所以我们将Vue保存起来。
// myVueRouter.js
let Vue = null;
class VueRouter{}
VueRouter.install = function(v) {
Vue = v
}
export default VueRouter
我们知道,在使用VueRouter的过程中,有两个组件经常用得到,他们是router-link和router-view,我们该怎么实现他们呢,Vue中为我们提供了一个创建组件的方法Vue.component(),添加到install方法中。
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
对install方法进行补充:
Vue.mixin({ // 将mixin的内容混入到Vue的初始参数options中
beforeCreate() { // 需要在$options初始化完成之前准备
if (this.$options && this.$options.router) { // 是根组件
this._root = this // 将_root挂载到根组件的实例上
this._router = this.$options.router
} else { // 是子组件
// 将_root根组件复制一份到子组件上
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get() {
return this._root._router
}
})
}
})
上述代码主要是为了让除了根组件以外的其他组件也能拥有router,最后通过defineProperty对this进行劫持,虽然我们获取的是$router,但其实返回的是根组件的_root._router。
然后我们对VueRouter传入的参数进行处理,第一个参数表示当前的路由模式,第二个参数是一个数组格式的路由表,但我们直接处理路由表很不方便,对其进行处理,转化成key:value的格式,代码如下:
constructor(options) {
this.mode = options.mode || 'hash';
this.routes = options.routes
this.routesMap = this.createMap(this.routes)
}
createMap(routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component // '/':Home, '/about':About
return pre
}, {})
}
路由中需要存放当前的路径,为了表示当前的路径状态,我们可以用对象来管理
constructor() {
this.current = null;
}
同时VueRouter中还需要添加一句
this.history = new HistoryRoute()
但目前的current也就是当前的路径还是null,所以我们需要进行初始化,同时还要先判断路由的模式,然后将路径保存在current中,实现原理可以参考上一篇文章。
init() {
if (this.mode === '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
})
}
}
我们已经可以获取当前的路径,可以开始实现$route了
Object.defineProperty(this, '$route', {
get() {
return this._root._router.history.current
}
})
跟router类似,也是通过数据劫持然后返回当前的路径。
现在我们保存了当前的路径,可以根据路径从路由表中获取对应的组件进行渲染,由此完善router-view组件
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current // 当前路由地址 '/about'
let routerMap = this._self._root._router.routesMap // 所有路由对象
return h(routerMap[current])
}
})
render函数里的this指向一个Proxy代理对象,代理Vue组件,而我们前面说每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例,所以我们可以从router实例上获取路由表,也可以获得当前的路径,然后再把获得的组件放到h()里进行渲染。
但有一个问题,当我们改变路径的时候,视图是没有重新渲染的,所以,需要将_router.history进行响应化。
Vue.util.defineReactive(this, 'xxx', this._router.history)
我们可以利用Vue的一个api:defineReactive,使得this._router.history对象得到监听
下面对Vue-router进行完善
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上的路径的切换,从而实现视图的重新渲染。
至此,我们已经完成全部的代码。完整代码如下:
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()
}
createMap(routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component // '/':Home, '/about':About
return pre
}, {})
}
init() {
if (this.mode === '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
})
}
}
}
VueRouter.install = function(v) { // 确保install方法只能被构造函数调用,实例对象无法使用
Vue = v
// console.log(Vue);
Vue.mixin({ // 将mixin的内容混入到Vue的初始参数options中
beforeCreate() { // 需要在$options初始化完成之前准备
if (this.$options && this.$options.router) { // 是根组件
this._root = this // 将_root挂载到根组件的实例上
this._router = this.$options.router
Vue.util.defineReactive(this, 'xxx', this._router.history)
} else { // 是子组件
// 将_root根组件复制一份到子组件上
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 // 当前路由地址 '/about'
let routerMap = this._self._root._router.routesMap // 所有路由对象
return h(routerMap[current])
}
})
}
export default VueRouter;
实现效果如图:
以上就是手写一个VueRouter的全部过程了,欢迎大家指正。