VueRouter 原理

161 阅读2分钟

Vue-Router基本使用,可以去浏览我之前的文章

实现router核心

  1. URL的变化不引起页面刷新
  2. 检测URL的变化

改变 URL 的方式只有这几种:

  1. 通过浏览器前进后退改变 URL
  2. 通过<a>标签改变 URL
  3. 通过window.location改变URL

原生js实现方法

hash 方式实现

  • hash 会在会在路径前加一个 #,改变URL中的hash部分不会引起页面刷新
  • 通过 hashchange 事件监听URL的变化
<body>
  <a href="#/">home</a>
  <a href="#/about">about</a>
  <div class="routerview"></div>
  <script>
    let routerView = [
      { path: '/', component: "<div>home</div>"},
      { path: '/about', component: "<div>about</div>"},
    ]
​
    window.addEventListener("hashchange", () => {
      if (!location.hash) {
        location.hash = "/#"
      }
​
      RouterRender(location.hash)
    })
    
    // 初始化
    if (!location.hash) {
      location.hash = "#/"
    }
    RouterRender(location.hash)
​
    function RouterRender(url) {
      var resContent
      routerView.forEach((item, i) => {
        if (url == ('#' + item.path)) {
          resContent = item.component
        }
        let divView = document.querySelector(".routerview")
        divView.innerHTML = resContent
      })
    }
  </script>
</body>

history 实现

  • pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

popstate 事件有些不同:

  1. 通过浏览器前进后退改变 URL 时会触发 popstate 事件
  2. 通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。
  3. 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化
  4. 通过js 调用history的back,go,forward方法课触发该事件

所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

<body>
  <a href="/">home</a>
  <a href="/about">about</a>
  <div class="routerview"></div>
  <script>
    let routerView = [
      { path: '/', component: "<div>home</div>"},
      { path: '/about', component: "<div>about</div>"},
    ]
​
    window.addEventListener("DOMContentLoaded", () => {
      // 获取所有的a标签
      var linkList = document.querySelectorAll('a[href]')
      linkList.forEach(item => {
        item.addEventListener('click', (e) => {
          // 取消默认点击跳转事件
          e.preventDefault()
          // 该方法在历史堆栈添加一个状态,并不会发生页面跳转
          let href = item.getAttribute('href')
          history.pushState(null, "", href)
          RouterRender(href)
        })
      })
    })
    
    // 初始化
    history.pushState(null, "", "/")
    RouterRender("/")
​
    function RouterRender(url) {
      var resContent
      routerView.forEach((item, i) => {
        if (url == item.path) {
          resContent = item.component
        }
        let divView = document.querySelector(".routerview")
        divView.innerHTML = resContent
      })
    }
​
    window.addEventListener("popstate", () => {
​
      RouterRender(location.pathname)
    })
  </script>
</body>

VueRouter 实现

创建Vue项目,安装Router

在src的router文件夹下的index.js文件可以看到如图;

  • 使用了Vue.use,所以Router里面拥有一个install函数
  • VueRouter是一个类,需要一些参数。

image-20211220154652340.png

install函数

  • (给每个vue实例添加东西的)—— Vue作为install的第一个参数
  • 注意:相同的插件不会重复下载 (插件会放在installedPlugins里面)

因此,我们需要将常量和组件放在install函数中,进行导入加载。

创建小刘Router: XlRouter

  • 解析index.js文件中新建实例传的参数
  • 建立路由和组件的映射
  • 根据路由模式,初始化URL监听事件
  • 导入路由组件,添加route route \ router对象 Vue.mixin里面的$option 是Vue初始化实例中的参数(main.js文件中)
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

Vue双向绑定原理传送门

import RouterLink from "./RouterLink.vue";
import RouterView from "./RouterView.vue";
class XlRouter {
  constructor(options) {
    // 判断是否设置了模式
    this.mode = options.mode || "hash";
    // 传入数组的路由配置
    this.routes = options.routes || [];
    // / 根据配置创建路由的匹配映射
    this.routesMap = this.createMap(this.routes);
    // 路由信息
    this.history = {
      current: null,
    };
    // 根据模式来匹配路由
    this.init();
  }
​
  init() {
    if (this.mode === "hash") {
      // 判断hash是否存在
      location.hash || (location.hash = "/");
      window.addEventListener("DOMContentLoaded", () => {
        this.history.current = location.hash.slice(1);
      });
      window.addEventListener("hashchange", () => {
        this.history.current = location.hash.slice(1);
      });
    } 
​
    if (this.mode === "history") {
      window.addEventListener("DOMContentLoaded", () => {
        this.history.current = location.pathname
      });
      window.addEventListener("popstate", () => {
        this.history.current = location.pathname
      });
    }
​
  }
​
  createMap(routes) {
    // 遍历route信息对象,建立路径和组件的映射
    return routes.reduce((pre, current) => {
      pre[current.path] = current.component;
      return pre;
    }, {});
  }
}
​
// 使用 install方法,会传入Vue对象作为参数
XlRouter.install = function (Vue) {
  Vue.mixin({
    // 给vue原型链添加 $route $router
    beforeCreate() {
      // 如果是根组件
      if (this.$options && this.$options.router) {
        this._root = this;
        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", RouterLink);
  Vue.component("router-view", RouterView);
};
​
export default XlRouter;

路由组件

Router-Link

  • a标签根据路由模式进行不同的URL绑定
  • 插槽用于存放写在组件内的内容
<template>
  <a :href="mode === 'hash' ? '#' + to : to">
    <slot></slot>
  </a>
</template><script>
export default {
  props: {
    // route-link to绑定跳转路由信息
    to: String
  },
  data () {
    return {
      mode: this._self._root._router.mode
    }
  }
}
</script>

Router-View

  • 利用Vue内置组件 component 进行组件切换
  • 利用data数据双向绑定,路由变化更改history对象,达到更换组件
<template>
  <div>
    <component :is="component"></component>
  </div>
</template><script>
export default {
  data() {
    // 当前路由
    let current = this._self._root._router.history.current;
    // 路由映射
    let routesMap = this._self._root._router.routesMap;
    return {
      routesMap,
      history: this._self._root._router.history,
      component: routesMap[current],
    };
  },
  mounted() {
    let mode = this._self._root._router.mode;
    if (mode === "hash") {
      window.addEventListener("hashchange", () => {
        this.component = this.routesMap[this.history.current];
      });
    } 
    if (mode === "history") {
      window.addEventListener("DOMContentLoaded", () => {
        this.component = this.routesMap[this.history.current];
      });
​
      window.addEventListener("popstate", () => {
        this.component = this.routesMap[this.history.current];
      });
    }
  },
};
</script><style></style>

使用

可以将XlRouter和两个路由组件放在router文件夹下,进行使用:

image-20211220170704459.png

修改index.js文件如下

import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from "./XlRouter"
import Home from '../views/Home.vue'