vue-router模拟实现

361 阅读5分钟

路由的概念

路由是一个比较广义和抽象的概念,路由的本质就是对应关系;

在开发中,路由分为前端路由后端路由

后端路由

  • 概念:根据不同用户的URl请求,返回不同的内容;
  • 本质:URL请求地址和服务器资源之间的对应关系

前端路由

  • 概念: 根据不同的用户时间,显示不同的页面内容
  • 本质:用户事件与时间处理函数之间的对应关系

Vue-Router是前端路由,当路径切换时,在浏览器判断当前路径,加载对应的组件。

Hash模式和History模式

前端路由中,不管是什么实现方式,都是客户端的一种实现方式,也就是当路径发生变化的时候,是不会向服务器发送请求的。

如果需要向服务器发送请求,就要用到ajax方式

两种模式的区别

表现形式的差异

Hash模式

  • 带有#,#后内容左右路由地址,可以通过?携带参数;
  • 这种模式相对来说比较丑,路径中带有与数据无关的符号,例如#和?

History模式

  • 正常的路径模式,需要服务端的相应支持(后面会解释原因)

原理上的差别

Hash模式

  • 基于锚点,以及onhashchange事件
  • 通过锚点的值作用路径地址,当地址发生变化后触发onhashchange事件

History

  • 基于HTML5中的History API
  • history.pushState() // IE10以后才支持
  • history.repalceState()

History模式为何需要服务器的支持???

因为在单页面中,只有一个页面,也就是index.html页面。服务端不存在www.test.com/lohin 这样的地址。也就是说如果刷新浏览器,请求服务器,是找不到login这个页面的。所以就会出现404的错误。 所以说在服务端应该除了静态资源外都返回单页面SPA应用的index.html

vue-router的使用

是官方的路由管理器,可以非常方便的用于SPA应用程序的开发

基本使用步骤

  1. 引入相关的库文件(vue、vue-router,使window挂在Vue和VueRouter这两个构造函数)
  2. 添加路由链接
// router-link是vue中提供的标签,默认会被渲染为a标签;
// to属性默认会被渲染为href属性,其值会被渲染为#开头的hash地址
<router-link to="/user">User</router-link>
  1. 添加路由填充位/路由占位符
// 将来通过路由规则匹配到的组件,将会被渲染到router-view所在的位置
// to属性默认会被渲染为href属性,其值会被渲染为#开头的hash地址
<router-view></router-view>
  1. 定义路由组建
const UserComp = {
    template: '<div>这里是用户主页</div?'
}
  1. 配置路由规则并创建路由实例
const router = new VueRouter({
    routes: [
        { path: '/user', component: UserComp },
    ]
})
  1. 把路由挂载到Vue根实例中
new Vue({
    el: "#app",
    router,
})

vueRouter模拟实现

回顾使用VueRouter的核心代码

// 注册组件Vue.use支持传入函数和对象,如果传入的是对象,会调用对象中的install方法
Vue.use(VueRouter)
// 创建路由对象 VueRouter 是个类 里面应该有一个静态的install方法
const router = new VueRouter({
    routes: [
        { name: 'home', path: '/', component: homeComponent }
    ]
})
// main.js
// 创建vue实例,注册router兑现
new Vue({
    router,
    render: h => h(App)
}).$mount('#app')

画一个类图

三个属性用处
options记录构造函数中传入的对象
datacurrent记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法
routeMap记录路由地址和组件的对应关系
6个方法解释
Constructor(Options)VueRouter 构造函数
install(Vue):void用来实现vue的插件机制 静态方法
init()void 注册popstate
initEvent(): void处理前进后退
createRouteMap():void初始化routeMap属性,转成map形式
initCOmponents(Vue): void创建route-link 和route-view这两个组件的

创建VueRouter的静态函数注册install方法

先看install需要做哪些事情:

  1. 判断当前插件是否已经被安装,如果已经安装,不需要重复安装
  2. 把Vue构造函数记录到全局变量中,静态方法install,这个方法接收了一个参数-vue的构造函数,而将来我们在vue实例中的一些方法中,还要使用vue的构造函数,比如创建组件时,需要使用Vue.component()
  3. 把创建Vue实例时传入的router对象注入到Vue实例上
export default class VueRouter {
    static install(Vue) {
        // 1. 判断当前插件是否已经被安装
        if (VueRouter.install.installed) {
            return
        }
        VueRouter.install.installed = true
        // 2. vue的构造函数记录到全局变量中
        _Vue = vue
        // 3. 把创建Vue实例时传入的router对象注入到Vue实例上
        // _Vue.prototype.$router = this.$options.router(x)
        // 因为只有在new Vue()的时候,才会有$options这个属性
        // 混入
        _Vue.mixin({
            beforeCreate() {
                if (this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                }
            }
        })
    }

}

实现构造哦函数

在介绍VueRouter的类图时,我们说过,Cinstructor是一个构造函数,该猴枣函数方法中会初始化options,data,routeMap这三个属性

export default class VueRouter {
    constructor(options) {
        this.options = options;
        this.routeMap = {}
        this.data = _Vue.observable({
            currnet: "/", // current记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法
        })
    }
}

实现cerareRouteMap方法

cerareRouteMap方法,会把构造中传入进来的options中的路由规则,转换成键值对的形式存储到routeMap中。键就是路由地址,值就是对应的组建

export default class VueRouter {
    cerareRouteMap() {
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        })
    }
}

实现initComponents方法

initComponents方法,主要作用是用来创建router-link和router-view这两个组件的。

 <router-link to="/user">用户主页</route-link>

我们知道router-link这个组件最终会被渲染成a标签,同时to作为一个属性,其值会作为a标签中的href属性的值。同时还要获取route-link这个组件中的文本,作为最终超链接的文本;

export default class VueRouter {
    initComponents() {
        Vue.component('route-link', {
            props: {
                to: String,
            },
            template: '<a :href="to"><slot></slot></a>'
        })
    }
}

现在我们已经通过Vue.component来创建了router-link这个组建; 现在我们开始对上述代码进行测试,要进行测试先将cerareRouteMainitComponents进行调用,那什么时候调用呢? 我们应该在VueRouter对象创建成功之后,并且将VueRouter对象注册到Vue实例上的时候,也就是在beforeCreate这个钩子函数中

export default class VueRouter {
    static install(Vue) {
        // 1. 判断当前插件是否已经被安装
        if (VueRouter.install.installed) {
            return
        }
        VueRouter.install.installed = true
        // 2. vue的构造函数记录到全局变量中
        _Vue = vue
        // 3. 把创建Vue实例时传入的router对象注入到Vue实例上
        // _Vue.prototype.$router = this.$options.router(x)
        // 因为只有在new Vue()的时候,才会有$options这个属性
        // 混入
        _Vue.mixin({
            beforeCreate() {
                if (this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                }
                // *** 在这里调用
                this.$options.router.init()
            }
        })
    },
    constructor(options) {
        this.options = options;
        this.routeMap = {}
        this.data = _Vue.observable({
            currnet: "/", // current记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法
        })
    },

    init() {
        this.cerareRouteMap()
        this.initComponents(_Vue)
    }

    cerareRouteMap() {
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        })
    },

    initComponents(_Vue) {
        _Vue.component('route-link', {
            props: {
                to: String,
            },
            template: '<a :href="to"><slot></slot></a>'
        })
    }
}

this.$options.router.init() 含义

this表示的就是Vue实例,$options表示的就是创建Vue实例的时候传递进来的选项,如下所示:

const vm = new Vue({
    el: "#app",
    router,
})

看$options中有el属性,也有router属性,router是什么呢,是const router = new VueRouter() ,init就是这个类上的实例的方法; 在我们引用我们自己创建的VueRouter后,

import VueRouter from './vurRouter'
// 注册组件Vue.use支持传入函数和对象,如果传入的是对象,会调用对象中的install方法
Vue.use(VueRouter)
// 创建路由对象 VueRouter 是个类 里面应该有一个静态的install方法
const router = new VueRouter({
    routes: [
        { name: 'home', path: '/', component: homeComponent }
    ]
})
// main.js
// 创建vue实例,注册router兑现
new Vue({
    router,
    render: h => h(App)
}).$mount('#app')

我们得到这样一个错误: You are using the runtime-only build of Vue where the template compiler is not available ……

该错误的含义是,我妈使用的是运行时版本的Vue,编译模板不可用;

你可以使用预编译模板把模板编译成render函数,或者是哟哦弄个包含编译版本的Vue;

以上错误说明了Vue的构建版本有两个,分别是运行时版完整版

运行时版: 不支持template模版,需要打包的时候提前编译;

完整版:包含运行时和编译器,体积比运行时大10k左右,程序运行的时候把模板转换成render函数。性能低于运行时版本;

使用vue-cli创建的项目默认使用运行时版,而我们创建的VueRouter中有template模板,所以才会出现那个错误

解决的方案:在项目的根目录创建vue.config.js文件,在该文件中添加runtimeCompiler配置项,该配置项表示的是,是否使用包含运行时编译器的Vue构建

moudle.exports = {
    runtimeCompiler: true,
}

render函数

如果不想做上述的修改,我们可以将template改成render函数

export default class VueRouter {
    initComponents() {
        Vue.component('route-link', {
            props: {
                to: String,
            },
            render(h) {
                return h(
                    "a",
                    { 
                        attrs: {
                            href: this..to
                        },
                    },
                    [this.$slots.default]
                )
            }
        })
    }
}

在测试之前一定要将根目录下的vue.config.js文件删除掉,这样当前的环境为“运行时”环境

创建router-view组件

router-view组件就是一个占位符。当根据路由规则找到组件后,会渲染到router-view的位置

export default class VueRouter {
    initComponents() {
        Vue.component('route-link', {
            props: {
                to: String,
            },
            render(h) {
                return h(
                    "a",
                    { 
                        attrs: {
                            href: this..to
                        },
                    },
                    [this.$slots.default]
                )
            }
        }),
        const self = this
        Vue.component('route-view', {
            render(h) {
                const component = self.routeMap[self.data.current]
                return h(component)
        })
    }
}

可以测试一下效果

当我们单击链接的时候,发现了浏览器进行了刷新操作。表明向服务器发送了请求,而我们单页面应用中是不希望向服务器发送请求。

修改后的initComponents方法如下:

export default class VueRouter {
    initComponents() {
        Vue.component('route-link', {
            props: {
                to: String,
            },
            render(h) {
                return h(
                    "a",
                    { 
                        attrs: {
                            href: this..to
                        },
                        on: {
                            click: this.clickHandler()
                        }
                    },
                    [this.$slots.default]
                )
            }
        }),
        methods: {
            clickHandler(e) {
                history.pushState({},"", this.to);

                this.$router.data.current = this.to
                // 阻止向服务器发送
                e.preventDefault()
            }
        },
        const self = this
        Vue.component('route-view', {
            render(h) {
                const component = self.routeMap[self.data.current]
                return h(component)
        })
    }
}

给a标签添加了单击事件

initEvent方法的实现

现在有一个问题就是,当点击浏览器中的前进和后退按钮的时候,地址栏中的地址放生了变化,但是对应的组建没有发生变化

这时候就要解决这个问题,就需要用到popstate事件;

popstate事件,可以发现浏览器历史操作的变化,记录改变后的地址,单击前进后者后退按钮的时候触发该事件

initEvetn() {
    window.addEventListener('popstate', () => {
        this.data.current = window.localtion.pathname
    })
}
// 针对initEvent方法的调用如下
init() {
    this.createRouteMap();
    this.initComponents(_vue)
    this.initEvent()
}

源码