vue-router原理实现

231 阅读2分钟

vue-router基础回顾

使用步骤

  1. 引入vue-router,注册路由插件
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
  1. 定义路由规则,创建router对象
// 路由规则
const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  },
  {
    path: '/blog',
    name: 'Blog',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue')
  },
  {
    path: '/photo',
    name: 'Photo',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "photo" */ '../views/Photo.vue')
  }
]
// 创建 router 对象
const router = new VueRouter({
  routes
})

export default router
  1. 注册 router 对象
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  // 3. 注册 router 对象
  router,
  render: h => h(App)
}).$mount('#app')
  1. 创建路由组件的占位
<template>
  <div id="app">
    <div>
      <img src="@/assets/logo.png" alt="">
    </div>
    <div id="nav">
      <router-link to="/">Index</router-link> |
      <router-link to="/blog">Blog</router-link> |
      <router-link to="/photo">Photo</router-link>
    </div>
    <!-- 创建路由组建的占位 -->
    <router-view/>
  </div>
</template>
  1. 创建链接
<template>
  <div id="app">
    <div>
      <img src="@/assets/logo.png" alt="">
    </div>
    <div id="nav">
      <!-- 创建链接 -->
      <router-link to="/">Index</router-link> |
      <router-link to="/blog">Blog</router-link> |
      <router-link to="/photo">Photo</router-link>
    </div>
    <router-view/>
  </div>
</template>

动态路由

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  },
  {
    // 动态路由
    // :id就是一个占位符,匹配变化的位置
    path: '/detail/:id',
    name: 'Detail',
    // 开启 props,会把 URL 中的参数传递给组件
    // 在组件中通过 props 来接收 URL 参数
    props: true,
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router
// Detail.vue
<template>
  <div>
    <!-- 方式1: 通过当前路由规则,获取数据 -->
    <!-- 这种方法不太好,因为当前组件这么写会强依赖于路由,也就是使用这个组件的时候必须有路由给传递对应的参数 -->
    通过当前路由规则获取:{{ $route.params.id }}

    <br>
    <!-- 方式2:路由规则中开启 props 传参 【推荐】 -->
    通过开启 props 获取:{{ id }}
  </div>
</template>

<script>
export default {
  name: 'Detail',
  props: ['id']
}
</script>

<style>
</style>
// App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Index</router-link> |
      <router-link to="/detail/11">Detail</router-link>
    </div>
    <router-view/>
  </div>
</template>

嵌套路由

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 加载组件
import Layout from '@/components/Layout.vue'
import Index from '@/views/Index.vue'
import Login from '@/views/Login.vue'

Vue.use(VueRouter)

const routes = [
  {
    name: 'login',
    path: '/login',
    component: Login
  },
  // 嵌套路由
  {
    path: '/',
    component: Layout,
    children: [
      {
        name: 'index',
        path: '',
        component: Index
      },
      {
        name: 'detail',
        path: 'detail/:id',
        props: true,
        component: () => import('@/views/Detail.vue')
      }
    ]
  }
]

const router = new VueRouter({
  routes
})

export default router

编程式导航

// 跳转到的某个路由
// 参数可以接收:字符串 或 对象
this.$router.push('/')
this.$router.push({ name: 'Detail', params: { id: 1 } })

// 跳转到的某个路由,不会记录当前历史,直接改变当前地址
// 参数可以接收:字符串 或 对象
this.$router.replace('/login')
this.$router.replace({ name: 'Detail', params: { id: 1 } })

// 跳转到某个路由
// 参数:正值--前进   负值--后退
this.$router.go(-2)

Hash和History模式区别

Hash模式:

  • URL中#后面的内容最为路径地址
  • 监听hashchange事件
  • 根据当前路由地址找到对应组件重新渲染 History模式:
  • 通过history.pushState()方法改变地址栏
  • 监听popstate事件
  • 根据当前路由地址找到对应组件重新渲染

History:music.163.com/playlist/31…

  • 原理 Hash模式基于锚点以及onhashchange事件

History模式是基于HTML5中的History API

    • history.pushState() 不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,并把地址记录到历史记录中,不会向服务器发送请求,可以实现无刷新的更新。 【IE10以后才支持】
    • history.replaceState() 将当前的历史记录给替换掉,并把地址记录到历史记录中,不会向服务器发送请求

Vue Router实现原理

let _Vue = null;
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.mixin({
      beforeCreate() {
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router;
        }
      },
    });
  }
  // 构造函数
  constructor(options) {
    // 记录构造函数中传入的options
    this.options = options;
    // routeMap存储路由规则
    this.routeMap = {};
    // 响应式对象
    this.data = _Vue.observable({
      current: "/", // 记录当前路由地址
    });
    this.init();
  }
  init() {
    this.createRouteMap();
    this.initComponent(_Vue);
    this.initEvent();
  }
  createRouteMap() {
    //遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中【键是路由地址,值是路由组件】
    this.options.routes.forEach((route) => {
      this.routeMap[route.path] = route.component;
    });
  }
  initComponent(Vue) {
    const _this = this;
    Vue.component("router-link", {
      // 接收传入的参数to
      props: {
        to: String,
      },
      render(h) {
        return h(
          "a",
          {
            attrs: {
              href: _this.mode === "hash" ? "#" + this.to : this.to,
            },
            on: {
              click: this.clickhander,
            },
          },
          [this.$slots.default]
        );
      },
      methods: {
        clickhander(e) {
          // hash 模式下,a 标签改变地址栏,但不会触发页面加载请求
          // 只有 history 模式,才需阻止 a 标签的默认行为,并通过 history.pushState 修改地址栏,然后触发 popstate
          if (self.mode !== "hash") {
            e.preventDefault(); // 阻止 a 标签的默认行为
            history.pushState({}, "", this.to);
          }
          // 修改当前组件的路径标识
          this.$router.data.current = this.to;
        },
      },
      // 运行时版:不支持template模板,需要打包的时候提前编译
      // 完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模板转换成render函数
      // 可以通过在vue.config.js中设置runtimeCompiler:true; 将Vue的构建版本设置为完整版
      // template:"<a :href='to'><slot></slot></a>"
    });
    Vue.component("router-view", {
      render(h) {
        const cm = _this.routeMap[_this.data.current];
        return h(cm);
      },
    });
  }
  initEvent() {
    // 为 hash、history 模式分别设置监听地址栏变化事件,从而加重目标组件
    if (this.mode !== "hash") {
      window.addEventListener("popstate", () => {
        this.data.current = window.location.pathname;
      });
    } else {
      window.addEventListener("load", () => {
        this.data.current = window.location.hash.slice(1);
        if (!this.data.current) {
          this.data.current = "/";
          window.location.hash = "#/";
        }
      });
      window.addEventListener("hashchange", () => {
        this.data.current = window.location.hash.slice(1);
      });
    }
  }
}