实现一个简易的vue-router

363 阅读2分钟

前言

最近在研究vue的源码,打算自己着手实现一些简单的功能,加深一下对框架的了解,于是准备实现一个简单的vue-router。

准备工作

先用vue-cli创建一个包含vue-router的demo,然后把项目引入的vue-router注释掉,改成自己写的js。

hash模式和history模式的区别

  • 表现形式的区别
    • hash模式 http:localhost/#/about
    • history模式 http:localhost/about
  • 原理的区别
    • hash模式 Vue Router默认使用的模式是hash模式,通过onhashchange事件监听url的变化。不需要后台人员进行配置
    • history模式 使用的是H5的 History API,通过popstate事件监听url的变化。需要后台人员处理刷新页面的情况(找不到当前页面,在服务端应该除了静态资源外都返回单页应用的 index.html
    history.pushState({}, "", "bar.html") // 往浏览器添加历史记录
    history.replaceState({}, "", "bar.html") // 往浏览器修改当前的历史纪录
    history.go() // 跳转指定页面
    

实现思路

  • 创建一个VueRouter类,有一个静态方法install,判断此插件是否加载过,只需加载一次。当Vue加载时,把传入的router对象挂载到Vue实例上(只执行一次)。
  • 在构造函数中,初始化,把传入的参数options放到自己的this上;创建一个routerMap对象,用来记录路由和组件的映射关系;利用Vue提供的 observable 方法创建一个响应式数据,记录当前的路由。
  • 创建initEvent事件,利用popstate事件监听history模式下路由的变化,利用hashchange事件监听hash模式下的变化。
  • 创建initRouterMap事件,循环传入的routes路由配置信息,把路由和组件的映射关系记录到routerMap对象中。
  • 创建initComponent事件,创建router-link和router-view两个组件。
  • 当路径改变时,通过routerMap找到对应的组件,渲染到router-view中。
  • 插件初始化时,执行initEvent、initRouterMap、initComponent事件。

代码演示

// my-Router.js

let _Vue = null;

class VueRouter {
  static install(Vue) {
    // 判断此插件是否安装过
    if(VueRouter.prototype.installed) {
      return
    }
    VueRouter.prototype.installed = true
    
    _Vue = Vue;

    // 把当前路由对象挂载到vue实例上
    _Vue.mixin({
      beforeCreate() { // 为什么在这里挂载,因为这里的this才是vue实例
        if(this.$options.router) {
          _Vue.prototype.$router = this.$options.router
        }
      }
    })
  }

  constructor(options) {
    // 传入的对象
    this.options = options;
    // 所有路由
    this.routerMap = {};
    // 当前路由
    this.data = _Vue.observable({
      current: "/",
    });
    this.init();
  }

  init() {
    //监听浏览器地址变化
    this.initEvent()
    // 把所有的路由添加到routerMap中
    this.initRouterMap();
    // 初始化全局的路由组件
    this.initComponent(_Vue);
  }

  initRouterMap() {
    for (let item of this.options.routes) {
      this.routerMap[item.path] = item.component;
    }
  }

  initEvent() {
    if(this.options.mode == 'history') {
      window.addEventListener('popstate', ()=>{
        this.data.current = window.location.pathname
      })
    }else {
      window.addEventListener('hashchange', ()=>{
        this.data.current = window.location.hash.substr(1)
      })
      window.addEventListener("load", () => {
        if (!window.location.hash) {
          window.location.hash = "#/";
        }
      });
    }
  }

  initComponent(Vue) {
    let that = this;

    Vue.component("router-link", {
      props: {
        to: String,
      },
      render(h) {
        return h(
          "a",
          {
            attrs: {
              href: this.to,
            },
            on: {
              click: this.clickhander,
            },
          },
          [this.$slots.default]
        );
      },
      methods: {
        clickhander(e) {
          e.preventDefault();
          
          if(that.options.mode == 'history') {
            history.pushState({}, "", this.to);
          }else{
            history.pushState({}, "", '#' + this.to);
          }
          that.data.current = this.to;
        },
      },
    });

    Vue.component("router-view", {
      render(h) {
        let app = that.routerMap[that.data.current];
        return h(app);
      },
    });
  }
}

export default VueRouter;