Vue-Router 原理及实现

1,074 阅读4分钟

一、Vue Router 使用步骤

  1. 创建路由对象
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'

// 1. 注册路由插件
Vue.use(VueRouter)

// 路由规则
const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  },
  {
    path: '/blog',
    name: 'Blog',
    component: () => import('../views/Blog.vue')
  },
  {
    path: '/photo',
    name: 'Photo',
    component: () => import('../views/Photo.vue')
  }
]
// 2. 创建 router 对象
const router = new VueRouter({
  routes
})

export default router

  1. 注册 router 对象
// main.js
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')

// 在创建 Vue 实例的时候,注入了 router 选项,在生成的 Vue 实例中就会有 $route、$router 两个属性
// $route 存放了路由规则,$router 就是路由对象,提供了很多路由方法
  1. 在页面或组件中使用,创建占位符、链接
<template>
  <div id="app">
    <div>
      <img src="@/assets/logo.png" alt="">
    </div>
    <div id="nav">
      <!-- 5. 创建链接 -->
      <router-link to="/">Index</router-link> |
      <router-link to="/blog">Blog</router-link> |
      <router-link to="/photo">Photo</router-link>
    </div>
    <!-- 4. 创建路由组建的占位 -->
    <router-view/>
  </div>
</template>

二、动态路由使用

动态路由指的是带参数的路由,路径参数用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来,让组件来接收路由的参数;同时也可以开启路由的 props ,会把 URL 中的参数传递给组件,组件可以通过 props 来接收 URL 参数

// 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
  },
  {
    path: '/detail/:id', // :id 是一个占位符
    name: 'Detail',
    props: true, // 开启 props,会把 URL 中的参数传递给组件,组件可以通过 props 来接收 URL 参数
    component: () => import('../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'] // 接收参数 id
}
</script>

<style>

</style>

三、嵌套路由使用

因为首页和详情页都有相同的头部和尾部,这个时候就可以使用嵌套路由,头部和尾部都提取到了 Layout 组件中,嵌套路由会把外部 path 和 children 中的 path 进行合并,然后会先加载外部 Layout 组件,最后加载首页组件或详情页组件

// 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

Layout 中会使用 router-view 占位符,来展示首页或者详情页

// components/Layout.vue
<template>
  <div>
    <div>
      <img width="25%" src="@/assets/logo.png">
    </div>
    <div>
      <router-view></router-view>
    </div>
    <div>
      Footer
    </div>
  </div>
</template>

<script>
export default {
  name: 'layout'
}
</script>

<style scoped>
</style>

四、编程式导航

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现

  1. this.$router.push

这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL,该方法的参数可以是一个字符串路径,或者一个描述地址的对象

// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
  1. this.router.replace这个方法作用类似于 this.router.replace 这个方法作用类似于 this.router.push,唯一不同的是,它在导航时不会向 history 添加新记录

  2. this.$router.go 这个方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步

五、Hash 模式和 History 模式的区别

Hash 模式和 History 模式都是客户端路由实现方式,路径发生变化的时候,不会向服务端发送请求,而是用 javaScript 监视路径的变化,根据不同的地址渲染不同的内容,如果需要服务端内容的话,需要发送 ajax 请求来获取

  1. 表现形式的区别
  1. 原理的区别
  • Hash 模式是基于锚点,以及 onhashchange 事件,通过锚点的值作为路由地址,当地址变化后出触发 onhashchange 事件,根据路径决定页面上呈现的内容。Vue Router 默认使用的是 hash 模式,使用 hash 来模拟一个完整的 URL,通过 onhashchange 监听路径的变化

  • History 模式是基于 HTML5 中的 History API ,也就是 history.pushState()和 history.replaceState()这两个方法,pushState 和 push 方法的区别是,当我们调用 history.push 方法的时候,路径会发生变化,会向服务器发送请求,而 history.pushState 不会向服务器发送请求,只会改变浏览器地址栏的地址,并且把地址记录到历史记录当中,所以通过 history.pushState 可以实现客户端路由,都是 history.pushState 是 ie10 以后才支持的,所以有浏览器兼容问题

Version:0.9 StartHTML:0000000105 EndHTML:0000000475 StartFragment:0000000141 EndFragment:0000000435

六、HTML5 History 模式的使用

首先需要将返回的 router 对象设置为 History 模式

const router = new VueRouter({
  mode: 'history',
  routes
})
  • 首先要明白的是,在 history 模式中页面的跳转是不会向服务器发送请求,只会改变浏览器地址栏的地址,但是当浏览器刷新的时候,浏览器就会自动向服务器发送请求,所以说,History 模式是需要服务器的支持 !!!

  • 所以在服务端应该除了静态资源外都返回单页应用的 index.html,在 node 服务端中可以使用 history 插件来实现

const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')

const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))

// 开启服务器,端口是 3000
app.listen(3000, () => {
  console.log('服务器开启,端口:3000')
})

七、VueRouter 模拟实现( history 模式为例)

先看 Vue Router 的核心代码

// 1. 注册路由插件
Vue.use(VueRouter) // 内部调用传入对象的 install 方法

// 路由规则
const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  }
]
// 2. 创建 router 对象
const router = new VueRouter({
  routes
})

export default router

Vue.use 这个方法可以传入函数或者对象,如果传入函数的话,Vue.use 会直接调用这个函数,如果传入对象的话,Vue.use 会直接调用这个对象的 install 方法,因为这里传入的是一个对象,所以要去实现 VueRouter 的 install 方法,因为后续要使用 new 关键字来创建实例对象,所以可以得知 VueRouter 是一个类,并且这个类有一个静态的 install 方法

7-1、VueRouter install 实现

  • Vue.use 调用 install 方法的时候会传递两个参数,一个是 Vue 构造函数,第二个是可选的选项对象
  • install 方法的的三个作用
  1. 判断当前插件是否被安装
  2. 把 Vue 的构造函数记录在全局
  3. 把创建 Vue 的实例传入的 router 对象注入到所有的 Vue 实例上
let _Vue = null
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
                }
            }
        })
    }
}

7-2、VueRouter 构造函数实现

let _Vue = null
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) {
        this.options = options 
        
        // options 中的路由规则解析后都存储在 routeMap 中
        // routeMap 的键是路由地址,值是路由组件
        this.routeMap = {} 
        
        // data 是一个响应式的对象,里面有一个属性 current 来记录当前路由地址
        this.data = _Vue.observable({
            current: "/"
        })
    }
}

7-3、VueRouter createRouteMap 方法实现

createRouteMap 方法的作用是将构造函数传过来的 options 中的路由规则转化成键值对的形式存储到 routeMap 对象中

let _Vue = null
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) {
        this.options = options 
        
        // options 中的路由规则解析后都存储在 routeMap 中
        // routeMap 的键是路由地址,值是路由组件
        this.routeMap = {} 
        
        // data 是一个响应式的对象,里面有一个属性 current 来记录当前路由地址
        this.data = _Vue.observable({
            current: "/"
        })
        this.init()
    }
    init() {
        this.createRouteMap()
    }
    createRouteMap() {
        // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到 routeMap 中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        });
    }
}

7-4、VueRouter router-link 实现

initComponent 方法的作用是创建两个组件,分别是 router-link 和 router-view,router-link 这个组件最终要渲染成超链接

let _Vue = null
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) {
        this.options = options 
        
        // options 中的路由规则解析后都存储在 routeMap 中
        // routeMap 的键是路由地址,值是路由组件
        this.routeMap = {} 
        
        // data 是一个响应式的对象,里面有一个属性 current 来记录当前路由地址
        this.data = _Vue.observable({
            current: "/"
        })
        this.init()
    }
    init() {
        this.createRouteMap()
        this.initComponent(_Vue)
    }
    createRouteMap() {
        // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到 routeMap 中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        });
    }
    initComponent(Vue) {
        Vue.component("router-link", {
            props: {
                to: String
            },
            render(h) {
                // h 函数的作用是创建虚拟dom
                // 第一参数是标签,
                // 第二个参数是属性和事件
                // 第三个参数是元素的内容
                return h("a", {
                    attrs: {
                        href: this.to
                    },
                    on: {
                        click: this.clickhander
                    }
                }, [this.$slots.default])
            }
        })

    }
}

7-5、VueRouter router-view 实现

router-view 组件相当于一个占位符,在 router-view 组件内部要获取到当前路由地址,获取到对应的组件并且渲染到 router-view 的位置中

let _Vue = null
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) {
        this.options = options 
        
        // options 中的路由规则解析后都存储在 routeMap 中
        // routeMap 的键是路由地址,值是路由组件
        this.routeMap = {} 
        
        // data 是一个响应式的对象,里面有一个属性 current 来记录当前路由地址
        this.data = _Vue.observable({
            current: "/"
        })
        this.init()
    }
    init() {
        this.createRouteMap()
        this.initComponent(_Vue)
    }
    createRouteMap() {
        // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到 routeMap 中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        });
    }
    initComponent(Vue) {
        Vue.component("router-link", {
            props: {
                to: String
            },
            render(h) {
                // h 函数的作用是创建虚拟dom
                // 第一参数是标签,
                // 第二个参数是属性和事件
                // 第三个参数是元素的内容
                return h("a", {
                    attrs: {
                        href: this.to
                    },
                    on: {
                        click: this.clickhander
                    }
                }, [this.$slots.default])
            },
            methods: {
                clickhander(e) {
                    // history.pushState 仅仅是修改浏览器地址,不向服务器发送请求
                    history.pushState({}, "", this.to)
                    // 通过 data.current 记录当前地址
                    this.$router.data.current = this.to
                    // 阻止默认行为执行
                    e.preventDefault()
                }
            }
        })
        // 因为在 render 方法中,this 指向已经不是 VueRouter 实例了,所以要保存 this
        const self = this 
        Vue.component("router-view", {
            render(h) {
                const cm = self.routeMap[self.data.current]
                return h(cm)
            }
        })

    }
}

7-6 VueRouter initEvent 实现

let _Vue = null
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) {
        this.options = options 
        
        // options 中的路由规则解析后都存储在 routeMap 中
        // routeMap 的键是路由地址,值是路由组件
        this.routeMap = {} 
        
        // data 是一个响应式的对象,里面有一个属性 current 来记录当前路由地址
        this.data = _Vue.observable({
            current: "/"
        })
        this.init()
    }
    init() {
        this.createRouteMap()
        this.initComponent(_Vue)
    }
    createRouteMap() {
        // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到 routeMap 中
        this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component
        });
    }
    initComponent(Vue) {
        Vue.component("router-link", {
            props: {
                to: String
            },
            render(h) {
                // h 函数的作用是创建虚拟dom
                // 第一参数是标签,
                // 第二个参数是属性和事件
                // 第三个参数是元素的内容
                return h("a", {
                    attrs: {
                        href: this.to
                    },
                    on: {
                        click: this.clickhander
                    }
                }, [this.$slots.default])
            },
            methods: {
                clickhander(e) {
                    // history.pushState 仅仅是修改浏览器地址,不向服务器发送请求
                    history.pushState({}, "", this.to)
                    // 通过 data.current 记录当前地址
                    this.$router.data.current = this.to
                    // 阻止默认行为执行
                    e.preventDefault()
                }
            }
        })
        // 因为在 render 方法中,this 指向已经不是 VueRouter 实例了,所以要保存 this
        const self = this 
        Vue.component("router-view", {
            render(h) {
                const cm = self.routeMap[self.data.current]
                return h(cm)
            }
        })

    }
    initEvent() {
        // 监听浏览器的历史的变化
        window.addEventListener("popstate", () => {
            this.data.current = window.location.pathname
        })
    }
}