Vue Router 实现原理

Vue Router 实现原理

Vue Router使用步骤

基础路由

接下来我会通过代码的形式为大家展现路由的基本用法

  1. 创建router对象,router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 路由组件
import index from '@/views/index'
// 组成插件
Vue.use(VueRouter)
// 路由规则
const routes = [
    {
        name: 'index',
        path: '/',
        component: index
    }
]
// 路由对象
const router = new VueRouter({
    routes
})
export default router
  1. 注册注册router对象,在main.js文件内操作
import router from './router'
new Vue({
render: h => h(App),
router
}).$mount('#app')
  1. 创建路由占位, 在需要用到路由的文件中,例如App.vue文件

<router-view></router-view>

  1. 最后一步创建链接
<router-link to="./">首页</router-link>
<router-link :to="{name: 'index'}">首页</router-link>

动态路由

<template>
    <div>
        // 方式一: 通过路由规则获取的数据
        通过当前路由规则匹配获取: {{$route.params.id}}

        // 方式二: 不仅可以通过路由,还有通过父组件传递(推荐)
        通过开启 props 获取: {{id}}
    </div>
</template>
<script>
export default {
    name: 'Detail',
    props: ['id']
}
</script>
const routes = [
    {
        name: 'Detail',
        path: '/detail/:id', // :id是占位符,传入对应的id,
        // 开启props会把url中的参数传递给组件,在组件内通过props来接收url参数
        props: true,
        // 路由懒加载,当需要访问该路由时加载组件
        // webpackChunkName 这个参数在我 前端模块打包工具Webpack中有介绍(魔法注释,自定义打包名)
        component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
    }
]

嵌套路由

const routes = [
    {
        name: 'Login',
        path: '/login',
        component: () => import('@/views/Login.vue')
    },
    // 嵌套路由
    {
        path: '/',
        component: () => import('@/components/Layout.vue'), // 公共组件
        children: [
            {
                name: 'index',
                path: '', // 如果为空则访问父路径('/')时会默认将父组件合并加载该组件
                component: () => import('@/views/Index.vue'), // 跟上边公共组件合并
            },
            {
                name: 'detail',
                path: 'detail/:id', // 相对路径
                props: true,
                component: () => import('@/view/Detail.vue'), // 跟上边公共组件合并
            }
        ]
    }
]

编程式导航

  • 跳转路由但不记录历史
    this.$router.replace('/login')

  • 会记录历史跳转
    this.$router.push('/')

  • 会记录历史跳转切传递参数id为1
    this.$router.push({name: 'Detail', params: { id: 1 }})

  • 后退到上上一次页面,后退2次
    this.$router.go(-2)

Hash模式和History模式

表现形式区别

  • Hash模式
    https://xuanhe.com/#/detail?age=18

  • History模式
    https://xuanhe.com/detail/18

原理的区别

  • Hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,当地址发生变化后,发生onhashchange事件
  • History模式是基于HTML5中的History API
    • history.pushState() 不会向服务端发送请求,只会改变浏览器地址,并且将地址记录在历史记录中
    • history.replaceState() 同上,但是不会保存记录
    • 监听popstate事件监听浏览器的变化,但是调用history.pushState()和history.replaceState()不会触发该事件,只有在点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)才会出发

关于popstate的用法详情 popstate

注意: History模式需要服务器的支持

  • 单页应用中,服务端不存在的地址会返回找不到该页面(刷新页面的操作会向服务器发送请求)

  • 在服务器端要配置除了静态资源外都返回单页应用的index.html

  • node中使用一个插件处理history模式

connect-history-api-fallback

  • nginx中配置

    server {
        listen       80;  # 端口号
        server_name  localhost; # 域名(一般是线上地址)
    
        #charset koi8-r;
    
        #access_log  logs/host.access.log  main;
    
        location / {
            root   html; # 根目录
            index  index.html index.htm; # 默认首页(从根目录里找)
            try_files $uri $uri/ /index.html; # $uri就是请求的路径,会去找($url这个文件, 或者是$uri目录下的index配置的页面,或者直接访问根文件的index.html,可以根据实际情况进行配置)
        }
    }    

模拟实现Vue Router

回顾核心代码

// router/index.js
// 注册插件
// 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')

install 方法的创建

  1. 判断插件是否已经安装
  2. 把vue的构造函数记录的全局变量中
  3. 把创建vue实例时候传入的router对象注入的vue实例上
    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实例都会有
    _vue.mixin({
      beforeCreate() {
        // 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行
        if (this.$options.router) {
          _vue.prototype.$router = this.$optins.router;
        }
      },
    });
    }
    }

构造函数实现

构造函数主要初始化三个参数.

  1. options
  2. data (响应式对象)
  3. routeMap (键值对形式存储,键是路由地址,值是对应的组件)
 constructor(options) {
    this.options = options;
    this.data = _vue.observable({
        current: '/'
    });
    // 解析routes解析路由规则,键值对形式
    this.routeMap = {};
  }

createRouteMap方法实现

  • 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  // 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  createRouteMap () {
    // 遍历routes把path和component存到routerMap对象中;
    this.options.routes.forEach((route) => {
      this.routeMap[route.path] = route.component
    })
  }

initComponents的router-link组件

  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      template: '<a :href="to"><slot></slot></a>'
    })
  }

初步router插件已经完成,但是这样有一个问题,就是运行时版本不支持tempalte选项,必须使用render函数来创建组件,如果直接使用会报错

Vue 构建版本

  • 运行时版: 不支持template模板,需要打包的时候提前编译
  • 完整版: 包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数

要开启完整版只需要在vue.config.js文件中设置runtimeCompiler: true

module.exports = {
    runtimeCompiler: true
}

运行时版不带编译器,不支持template选项

单文件组件中的template在打包时编译成了render函数,所以单文件中的template在运行时版可以生效

  • 不使用编译器来解决组件template的方法

将tempalte替换为render函数,initComponents方法

initComponents的router-view组件和router-link组件的修改

  • router-view相当于一个占位符,在该组件内部根据当前路由地址获取到对应的路由组件,渲染到router-view中
  • 其他细节均在代码注释中体现
  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        // 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
        return h('a', {
          // 添加dom对象的属性写在attrs中
          attrs: {
            href: this.to
          },
          // 注册dom对象的事件
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default]) // 获取子元素默认插槽
      },
      methods: {
        // 事件处理函数
        clickHandler(e) {
          // 1.调用pushState方法改变浏览器的地址
          // pushState接收三个参数,1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
          history.pushState({}, '', this.to)
          // 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例),因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
          this.$router.data.current = this.to // 下面方式使用self也行,这个也行
          // 阻止默认行为
          e.preventDefault() 
        }
      }
      // 运行时版本不支持template,只支持render函数
      // template: '<a :href="to"><slot></slot></a>'
    })
    // 创建router-view组件
    // 组件内部的this是vue实例,这里需要用到router实例
    const self = this
    _vue.component('router-view',{
      // 利用h参数创建虚拟dom
      render(h) {
        // 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以接收一个组件转换成一个虚拟dom
        return h(component)
      }
    })
  }

initEvent解决浏览器前进后退

现在基本上已经完成了一个基础的router了,但是会有一个小问题,在点击浏览器的前进后退按钮时地址栏发生变化,但是组件没有发生变化,没有重新加载地址对应的组件

  • 解决浏览器前进后退渲染不更新的情况
 // 解决浏览器前进后退不重新加载组件的方法
  initEvent(){
    window.addEventListener('popstate', () => {
        // 将响应式的保存路径的值设置成 浏览器地址中路径部分的值即可
        this.data.current = window.location.pathname
    })
  }

init方法封装

  • 主要就是封装方法的调用
    // 封装初始化的一些方法
  init() {
    // 调用保存地址和组件映射的方法
    this.createRouteMap()
    // 调用创建router-link和router-view组件的方法
    this.initComponents()
    // 调用解决浏览器前进后退不重新加载组件的方法
    this.initEvent()
  }

简版的完整代码

那么现在,我们就实现了一个简单的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实例都会有
    _vue.mixin({
      beforeCreate () {
        // 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行
        if (this.$options.router) {
          _vue.prototype.$router = this.$options.router
        }
      }
    })
  }

  constructor (options) {
    this.options = options
    this.data = _vue.observable({
      current: '/'
    })
    // 解析routes解析路由规则,键值对形式
    this.routeMap = {}
    this.init()
  }

  // 封装初始化的一些方法
  init () {
    // 调用保存地址和组件映射的方法
    this.createRouteMap()
    // 调用创建router-link和router-view组件的方法
    this.initComponents()
    // 调用解决浏览器前进后退不重新加载组件的方法
    this.initEvent()
  }

  // 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
  createRouteMap () {
    // 遍历routes把path和component存到routerMap对象中;
    this.options.routes.forEach((route) => {
      this.routeMap[route.path] = route.component
    })
  }

  // 创建组件
  initComponents () {
    // 创建router-link组件, 两个参数,1组件名, 2组件参数
    _vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        // 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
        return h('a', {
          // 添加dom对象的属性写在attrs中
          attrs: {
            href: this.to
          },
          // 注册dom对象的事件
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default]) // 获取子元素默认插槽
      },
      methods: {
        // 事件处理函数
        clickHandler (e) {
          // 1.调用pushState方法改变浏览器的地址,pushState接收三个参数
          // 1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
          history.pushState({}, '', this.to)
          // 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例)
          // 因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
          this.$router.data.current = this.to
          e.preventDefault()
        }
      }
      // 运行时版本不支持template,只支持render函数
      // template: '<a :href="to"><slot></slot></a>'
    })
    // 创建router-view组件
    const self = this
    _vue.component('router-view', {
      // 利用h参数创建虚拟dom
      render (h) {
        // 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以接收一个组件转换成一个虚拟dom
        return h(component)
      }
    })
  }

  // 解决浏览器前进后退不重新加载组件的方法
  initEvent () {
    window.addEventListener('popstate', () => {
      // 将响应式的保存路径的值设置成浏览器地址中路径部分的值即可
      this.data.current = window.location.pathname
    })
  }
}

希望大家对Router的理解更加深刻,开发中更加效率!

讨论数量: 0

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!