Vue Router的手写实现

为何须要前端路由

在先后端分离的如今,大部分应用的展现方式都变成了 SPA(单页面应用 Single Page Application)的模式。为何会选择 SPA 呢?缘由在于:html

  • 用户的全部操做都在同一个页面下进行,不进行页面的跳转。用户体验好。
  • 对比多页面,单页面不须要屡次向服务器请求加载页面(只请求一次.html文件),只须要向服务器请求数据(多亏了 ajax)。所以,浏览器不须要渲染整个页面。用户体验好。

归根结底,仍是由于 SPA 可以提供更好的用户体验。前端

为了更好地实现 SPA,前端路由是必不可少的。假设一个场景:用户在 SPA 页面的某个状态下,点击了强制刷新按钮。若是没有前端路由记住当前状态,那么用户点击该按钮以后,就会返回到最开始的页面状态。这不是用户想要的。vue

固然,须要前端路由另外一个点在于:咱们能够更好地进行 SPA 页面的管理。经过将组件与路由发生配对关联,依据路由的层级关系,可为 SPA 内部的组件划分与管理提供一个依据参考。ajax

Hash 路由模式 与 History 路由模式

这是两种常见的前端路由模式。vue-router

Hash 路由模式

Hash 模式使用了浏览器 URL 后缀中的#xxx部分来实现前端路由。默认状况下,URL后缀中的#xxx hash 部分是用来作网页的锚点功能的,如今前端路由看上了这个点,并对其加以利用。后端

好比这个 URL:http://www.abc.com/#/hello,hash 的值为 #/helloapi

为何会看上浏览器URL后缀中的 hash 部分呢?缘由也简单:浏览器

  • 浏览器URL后缀中的 hash 改变了,不会触发请求,对服务器彻底没有影响。它的改变不会从新加载浏览器页面。
  • 更关键的一点是,由于hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退均可以用了,页面的状态与浏览器的URL就发生了挂钩。

hash模式背后的原理是onhashchange事件,能够在window对象上监听这个事件。服务器

History 路由模式

随着 HTML5 中 history api 的到来,前端路由开始进化了。hashchange 只能改变 # 后面的代码片断,history api (pushState、replaceState、go、back、forward) 则给了前端彻底的自由。简单讲,它的功能更为强大了:分为两大部分,切换和修改。app

路由切换

参考MDN,切换历史状态包括 back、forward、go 三个方法,对应浏览器的前进,后退,跳转操做。

history.go(-2);//后退两次
history.go(2);//前进两次
history.back(); //后退
hsitory.forward(); //前进
复制代码

路由修改

修改历史状态包括了pushState,replaceState两个方法:

/** ** 参数含义 ** state: 须要保存的数据,这个数据在触发popstate事件时,能够在event.state里获取 ** title:标题,基本没用,通常传 null ** url:设定新的历史记录的 url */	
window.history.pushState(state, title, url) 

//假设当前的url是:https://www.abc.com/a/
//例子1
history.pushState(null, null, './cc/') //此时的url为https://www.abc.com/a/cc/
//例子2
history.pushState(null, null, '/bb/') //此时的url为https://www.abc.com/bb/
复制代码

一样的,history 模式能够监听到对应的事件:

window.addEventListener("popstate", function() {
    // 监听浏览器前进后退事件,pushState 与 replaceState 方法不会触发 
});
复制代码

History 模式的注意点

和 Hash 模式相比,History 模式存在着更多的选择。可是也有一些自身的注意点:在用户点击强制刷新的时候,History 模式会向服务器发送请求。

为了解决这个问题,须要服务器作对应的处理。服务器能够针对不一样的URL进行处理,固然,也能够简单处理:只要是未匹配到的URL请求,一概返回同一个 index.html 页面。

Vue Router 作了什么?

Vue Router 做为 Vue 生态系统中很是重要的一个成员,它实现了 Vue 应用的路由管理。能够说,Vue Router 是专门为 Vue 量身定制的路由管理器,功能点很是多。它的内部实现是与 Vue 自身是有强耦合关系的(Vue Router 内部利用了 Vue 的数据响应式)。

咱们来看一个典型的 Vue Router 配置:

import Vue from "vue";
import App from "./vue/App.vue";
import VueRouter  from 'vue-router';

//以插件的形式,使用VueRouter
Vue.use(VueRouter);

//路由配置信息,能够从外部文件引入,在此直接写是为了方便演示
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const routes = [
  { path: '/', component: Foo },
  { path: '/bar', component: Bar }
]

//初始化并与 Vue 实例关联
const router = new VueRouter({routes});
new Vue({
  router,
  render: h => h(App),
}).$mount("#root");
复制代码

可看出,VueRouter 是做为插件的形式引入到 Vue 系统内部的。而将具体的 router 信息嵌入到每一个 Vue 实例中,则是做为 Vue 的构造函数参数传入。

同时来看看如何使用它:

//routerExample.vue
<template>
    <div>
        <h1 @click="goBack">App Test</h1>
        <router-link to="/">foo</router-link>
        <router-link to="/bar">bar</router-link>

        <router-view></router-view>
    </div>
</template>

<script>
export default {
  methods: {
    goBack() {
      console.log(this.$router);  
      window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/')
    }
  }
}
</script>

<style lang="less" scoped>

</style>
复制代码

上面的代码中,咱们能够直接使用router-linkrouter-view这两个组件。它们是随着 Vue Router 一块儿引入的,做为全局组件使用。

这就是一个最简单的 Vue Router 的使用方式。咱们下面就来看看,该如何本身实现上面的简单功能,作一个本身的 Vue Router。

一个简单的 Vue Router 实现

看了上面的这个过程,最简单的 Vue Router 应该包括如下实现步骤:

  • 实现 Vue 规定的插件的写法,将咱们本身的Vue Router 做为插件引入 Vue 系统中。
    • router功能一:解析传入的routes选项,以备调用
    • router功能二:监控URL变化(两种路由方式:history、hash)
  • 实现两个全局组件:router-linkrouter-view

看看自定义的 Vue Router 的实现:

//FVueRouter.js

 let Vue; //保存 Vue 构造函数的引用,与 Vue 深度绑定

 class FVueRouter {
    constructor(options){
        this.$options = options;
      	//保存路由的路径与路由组件的对应关系
        this.routerMap = {};

        //当前的URL必须是响应式的,使用一个新的 Vue 实例来实现响应式功能
        this.app = new Vue({
            data: {current : "/"}
        })
    }

    init(){
        //监听路由事件
        this.bindEvents();
        //解析传入的routes
        this.createRouterMap();
        //全局组件的声明
        this.initComponent();
    }

    bindEvents(){
        window.addEventListener('hashchange', this.onHashChange.bind(this));
    }

    onHashChange(){
        this.app.current = window.location.hash.slice(1) || '/';
    }

    createRouterMap(){
        this.$options.routes.forEach(route => {
            this.routerMap[route.path] = route;
        })
    }

    initComponent() {
        // 形式:<router-link to="/"> 转换目标=> <a href="#/">xxx</a>
        Vue.component("router-link", {
          props: {
            to: String,
          },
          render(h) {
            // h(tag, data, children)
            return h('a', {
                attrs: {href: '#' + this.to}
            }, [this.$slots.default])
          },
        });
        // 获取path对应的Component将它渲染出来
        Vue.component("router-view", {
            render: (h) => {
                //此处的this 可以正确指向 FVouter内部,是由于箭头函数
                const Component = this.routerMap[this.app.current].component;
                return h(Component)
            }
        })
      }
 }

 // 全部的插件都须要实现install 方法,传入参数是Vue的构造函数
 FVueRouter.install = function(_Vue){
    //将Vue的构造函数保存起来
    Vue = _Vue;

    //实现一个混入操做的缘由,插件的install阶段很是早,此时并无Vue实例
    //所以,使用mixin,延迟对应操做到Vue实例构建的过程当中来执行。
    Vue.mixin({
        beforeCreate(){
            //获取到Router的实例,并将其挂载在原型上
            if(this.$options.router){
                //根组件beforeCreate时只执行一次
                Vue.prototype.$router = this.$options.router;

                this.init();
            }
        }
    })
 }

export default FVueRouter;
复制代码

这里是最为简单的一种实现。有几个值得注意的点:

  • 如上代码,将最基本的一个Vue Router 的代码架子搭建起来了,可以运行。但细微处依然须要酌情考虑。
  • 关于插件的写法:自定义插件内部必须实现一个 install 方法,传入参数是Vue的构造函数。
  • 使用了一个新的Vue 实例,将 URL 的 hash 变量进行数据响应化处理。
  • 关于渲染函数 render 的参数 h,它其实是 createElement 函数。具体用法值得深究。代码中使用的是最为简单的处理方式。

结尾

在本文中,咱们讲解了 前端路由常见的两种模式:Hash 模式与 History 模式。同时,咱们尝试本身实现了一个最为简单的 Vue Router。更多相关的 Vue Router 的细节,能够参考其官网。但愿本文对你有用。

相关文章
相关标签/搜索