Vue Router 实现原理

181 阅读3分钟

概述

  • Vue Router 基础回顾
  • Hash模式和History模式
  • 模拟实现自己的Vue Router

Vue Router 基础回顾

基本使用

"vue-router": "^3.1.6"

目录结构

router/index.js

  1. 注册路由插件: Vue.use 用来注册插件,传入函数的话就直接调用,传入对象就会调用对象的 install 方法
  2. 传入路由规则创建 router 对象并导出
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',
    // 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')
  }
]
// 2. 创建 router 对象
const router = new VueRouter({
  routes
})

export default router

main.js

  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')

在创建Vue实例时配置了router选项,此时会给Vue实例注入两个属性,$route(路由规则),$router(Vue Router 提供路由信息、方法等)。

App.vue

  1. 创建路由组件的占位
  2. 创建链接
<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>

效果

查看

动态路由传参

目录

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',
    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

获取路由参数的方式:

  1. 通过当前路由规则,获取数据(组件依赖路由)
  2. 路由规则中开启 props 传参,会把 URL 中的参数传递给组件,在组件中通过 props 来接收 URL 参数(推荐)

Detail.vue

<template>
  <div>
    <!-- 方式1: 通过当前路由规则,获取数据 -->
    通过当前路由规则获取:{{ $route.params.id }}

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

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

效果

查看

嵌套路由

当多个路由组件有相同的内容,可以把这些相同的内容提取到一个公共组件当中。

目录

其中Layout.vue包含了首页和详情页相同的header和footer,通过router-view切换首页和详情页的内容。

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>

router.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: '',
        // path: '/',
        component: Index
      },
      {
        name: 'detail',
        path: 'detail/:id',
        // path: '/detail/:id',
        props: true,
        component: () => import('@/views/Detail.vue')
      }
    ]
  }
]

const router = new VueRouter({
  routes
})

export default router

编程式导航

之前路由跳转是在页面通过router-link生成超链接,很多情况下需要在代码里加入跳转逻辑。

编程式导航中常用的方法:

  • $router.push() 传参方式
  1. 路径 $router.push('/')

  2. 组件名称 $router.push({ name: 'Detail', params: { id: 1 } })

  • $router.replace() 传参与push方法相同

replace方法不会记录本次历史

  • $router.go()

跳转到历史某一次

$router.go(-1)$router.back()效果一致

Hash 模式和 History 模式的区别

不管哪种模式都是客户端路由的实现方式,也就是当路径发生变化之后不会向服务器发送请求,是由JS监视路由的变化,然后根据不同的地址渲染不同的内容。如果需要服务器端内容的话会发起Ajax请求来获取。

表现形式的区别
  • Hash 模式 https://music.163.com/#/playlist?id=3102961863

# 号,# 后面的内容作为路由地址,可以通过 ? 携带url参数,这些路径很丑,路径中带着一些和数据无关的符号 # ?

  • History 模式 https://music.163.com/playlist/3102961863

正常的url,History 模式还需要服务端配置支持。

原理的区别
  • Hash 模式是基于锚点,以及 onhashchange 事件 通过锚点的值作为地址,当地址发生变化后触发 onhashchange 事件,根据路径呈现页面上不同的内容。
  • History 模式是基于 HTML5中的 History API
  1. history.pushState() IE10 以后才支持 与 history.push() 区别是:调用 history.push() 方法时,路径会发生变化,这时要向服务器发送请求;而调用history.pushState() 不会向服务器发送请求只会改变地址,并且将地址记录到历史记录,所以通过history.pushState()可以实现客户端路由。因为 IE10 以后才支持history.pushState(),所以想要兼容IE9以下的版本只能选择Hash模式。

  2. history.replaceState()

History 模式

使用
  • History 模式需要服务器的支持 因为单页面应用(只有一个页面 index.html)中,服务器不存在 http://www.testurl.com/login这样的地址(这样的页面)会返回找不到该页面,所以应该在服务端配置除了静态资源外都返回单页应用的index.html
Node.js 服务器配置

目录结构

前端目录结构

打包后将打包后的dist文件夹放到web目录下(相当于网站的根目录),这就把前端项目部署好了。

后端目录结构

app.js

const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express 基于 node 的 web开发框架
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')
})

这是基于node的express开发的web服务器,运行node app.js,当在浏览器中的输入localhost:3000 就可以访问该服务器看到该网站的内容。

Nginx 服务器配置

安装

$ brew search nginx
$ brew install nginx

启动(会在后台启动,不会阻塞命令行的运行)

$ nginx

然后在浏览器中输入:http://localhost:8080/ 查看 Nginx 首页。

其他操作

$ nginx -s reload:重新加载配置
$ nginx -s stop:停止
$ nginx -s reopen:重启
$ nginx -s quit:退出

把打包好的项目放到nginx的/usr/local/var/www(网站的根目录)

这时候需要修改nignx配置(/usr/local/etc/nginx/nginx.conf)来支持history模式。

location / {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

Vue Router 实现原理

基本原理

Hash 模式

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

模拟实现分析

Vue Router 的核心代码

Vue Router 类图

实现

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

              }

          }
      })
  }
}
构造函数
constructor(options){
    this.options = options
    this.routeMap = {}
    // observable
    this.data = _Vue.observable({
        current:"/"
    })
    this.init()

}
createRouteMap
createRouteMap(){
    //遍历所有的路由规则 把路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
        this.routeMap[route.path] = route.component
    });
}
router-link

Vue 的构建版本

  • 运行时版: 不支持template模板,需要打包的时候提前编译
  • 完整版:包含运行时和编译器,体积比运行时版本大10K左右,程序运行的时候把模板转换成render函数。
initComponent(Vue){
  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){
              // 改变浏览器的地址栏
              history.pushState({},"",this.to)
              this.$router.data.current=this.to
              e.preventDefault()
          }
      }
      // template:"<a :href='to'><slot></slot><>"
  })
}
router-view
initComponent(Vue){
  const self = this
  Vue.component("router-view",{
    render(h){
        // self.data.current
        const cm=self.routeMap[self.data.current]
        return h(cm)
    }
  })
}
initEvent
initEvent(){
    //
    window.addEventListener("popstate",()=>{
        this.data.current = window.location.pathname
    })
}