Vue.js 框架源码与进阶 - Vue-Router 原理实现

一、Vue-Router 使用步骤

1.1 项目结构

在这里插入图片描述

1.2 创建 router 对象,router/index.js

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

// 1. 注册路由插件
// Vue.use 是用来注册插件,它会调用传入对象的 install 方法
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

1.3 注册 router 对象 main.js

当我们创建 Vue 实例配置上 router 选项时,此时会给 Vue 实例分别注入 $route 和 $router 这两个属性

import Vue from 'vue'
import App from './App.vue'
// 导入 router 对象
import router from './router'

Vue.config.productionTip = false

new Vue({
  // 3. 注册 router 对象
  router,
  render: h => h(App)
}).$mount('#app')

1.4 创建路由组件的占位 App.vue

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

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>

三、嵌套路由

在这里插入图片描述
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.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>

APP.vue

<template>
  <div id="app">
    <div id="nav">
      <router-view></router-view>
    </div>
  </div>
</template>

<style>
</style>

四、编程式导航

  • push 方法会记录本次历史(可以在浏览器上面后退)
 this.$router.push({ path: "/detail", name: 'Detail', params: { id: 1 } })
// 使用 path 会自动忽略 params
 this.$router.push({ name: 'Detail', query: { id: 1 } })
  • replace 方法不会记录本次历史 会替换历史记录
this.$router.replace('/login')
  • go 方法会以 0 为基准跳转到相对页面
this.$router.go(-2)

五、Hash 和 History 模式区别

两种方式均为客户端路由的实现方式:当路径法发生变化,不会向服务器发送请求,使用 js 监视路径的变化根据不同的地址渲染不同的内容,如果需要服务器端的内容会发送 Ajax 请求来获取

5.1 表现形式的区别

Hash 模式(带 # 号)

  • https://music.163.com/#/playlist?id=3102961863

History 模式

  • https://music.163.com/playlist/3102961863

5.2 原理的区别

Hash 模式

  • Hash 模式是基于锚点,以及 onhashchange 事件
  • Vue Router 默认使用的是 hash 模式,使用 hash 来模拟一个完整的 URL,通过 onhashchange 监听路径的变化

History 模式

  • History 模式是基于 HTML5 中的 History API
    • history.pushState() IE10 以后才支持,它不会向服务器发送请求,只会改变浏览器地址栏地址,并且把这个地址记录到历史记录里
    • history.replaceState()
    • 开启 History 模式
const router = new VueRouter({ 
  // mode: 'hash', 
  mode: 'history', 
  routes 
})

5.3 History 模式

  • History 需要服务器的支持
  • 单页应用中,服务端不存在 http://www.testurl.com/login 这样的地址会返回找不到该页面
  • 在服务端应该除了静态资源外都返回单页应用的 index.html

在这里插入图片描述
App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/video">Video</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
</style>

views/404.vue

<template>
  <div>
    您要查看的页面不存在
  </div>
</template>

<script>
export default {

}
</script>
<style>

</style>

router/index.js

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

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // 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: "about" */ '../views/About.vue')
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: "404" */ '../views/404.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

当我们刷新 /video 地址,会向服务器发送请求,请求/video页面,服务器如果不存在这个页面应该返回 404 页面,但是 vue-cli 自带的服务器已经配置好了

5.4 History 模式 - Node.js 服务器配置

当我们开启服务器支持,再次刷新浏览器时,会向服务器发送请求,服务器接收到这个请求,因为我们开启了对 history 模式的支持,服务器会判断,当前请求的页面服务器上没有,因此会将单页应用默认的首页(index.html)返回给浏览器,浏览器接收到此页面再去判断路由地址并加载对应组件内容并渲染到浏览器

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");
});

5.5 History 模式 - nginx 服务器配置

当 nginx 服务器未处理 vue-router 的 history 模式时,当我们刷新浏览器再去请求地址时,服务器不存在请求的路径中对应的文件,所以服务器会返回 404 页面

  • 从官网下载 nginx 的压缩包
  • 把压缩包解压到 c 盘根目录(不能有中文),c:\nginx-1.18.0 文件夹
  • 修改 conf\nginx.conf 文件
location / { 
  root html; 
  index index.html index.htm; 
  #新添加内容 
  #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页 
  #如果都获取不到返回根目录中的 index.html 
  try_files $uri $uri/ /index.html; 
}
  • 打开命令行,切换到目录 c:\nginx-1.18.0
  • nginx 启动、重启和停止
# 启动 
start nginx 
# 重启 
nginx -s reload 
# 停止 
nginx -s stop

六、Vue Router 模拟实现 ( History模式 )

前置的知识:插件、slot 插槽、混入、render 函数、运行时和完整版的 Vue

Hash 模式

  • URL 中 # 后面的内容作为路径地址,可以直接通过 location 对象操作 URL 来切换浏览器地址,如果只改变路径 # 号后面的内容,浏览器不会向服务器请求地址,但会把这个地址记录到浏览器的访问历史中
  • 当 hash 改变后,监听 hashchange 事件,在 hashchange 事件中记录当前的路由地址,并找到该路径对应的组件,并重新渲染
  • 根据当前路由地址找到对应组件重新渲染

History 模式

  • 通过 history.pushState() 方法改变浏览器地址栏,仅仅是改变地址栏,并把地址记录到浏览器的访问历史中,不会真正的跳转到指定路径(浏览器不会向服务器发送请求,如果点击刷新,就会重新向后端服务器发送请求)
  • 监听 popstate 事件,记录改变后的地址,在调用 pushState、replaceState 时,不会触发该事件,在点击浏览器前进后退按钮或调用 History 的 back 和 forward 方法时,事件才会被触发
  • 根据当前路由地址找到对应组件重新渲染

回顾 Vue Router 的核心代码

router/index.js

// 注册插件 
// Vue.use() 内部调用传入对象的 install 方法
// Vue.use() 如果传入一个函数,会调用这个函数
Vue.use(VueRouter);
// 创建路由对象
const router = new VueRouter({
  routes: [{ name: "home", path: "/", component: homeComponent }],
});

main.js

// 创建 Vue 实例,注册 router 对象
new Vue({ router, render: (h) => h(App) }).$mount("#app");
  • 类图

在这里插入图片描述

  1. options 属性 :记录构造函数中传入的对象
  2. data 是一个对象:其中有一个属性 current 记录当前路由地址,该对象是响应式的(调用vue.observe 方法)
  3. routeMap 属性:记录路由地址和组件的对应关系
  4. Coustructor(Options) 方法:构造函数,初始化属性
  5. install(Vue) 方法:静态方法,实现 vue 的插件机制
  6. init() 方法:调用下面三个方法,将不同代码分隔到不同方法实现
  7. initEvent() 方法:注册 popstate 事件,监听浏览器历史的变化
  8. creatRouteMap() 方法:初始化 RouterMap 属性,把构造函数中传入的路由规则转换成键值对的形式存储到 RouterMap 对象(路由地址:对应组件)
  9. initComponents(Vue) 方法:创建 router-link 和 router-vue 两个组件

6.1 实现思路

  • 创建 VueRouter 插件,静态方法 install
    • 判断插件是否已经被加载
    • 当 Vue 加载的时候把传入的 router 对象挂载到 Vue 实例上(注意:只执行一次)
  • 创建 VueRouter 类
    • 初始化,options、routeMap、app(简化操作,创建 Vue 实例作为响应式数据记录当前路径)
    • creatRouteMap() 遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
    • 注册 popstate 事件,当路由地址发生变化,重新记录当前的路径
    • 创建 router-link 和 router-view 组件
    • 当路径改变的时候通过当前路径在 routerMap 对象中找到对应的组件,渲染 router-view

6.2 代码实现

let _Vue = null;
export default class VueRouter {
  // 静态方法,实现 vue 的插件机制
  // 方法或函数也是一个对象
  static install(Vue) {
    // 1. 判断当前插件是否已经被安装
    // 如果插件已经安装直接返回
    if (VueRouter.install.installed) {
      return;
    }
    VueRouter.install.installed = true;
    // 2. 把 Vue 构造函数记录到全局变量
    _Vue = Vue;
    // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
    // 混入
    _Vue.mixin({
      beforeCreate() {
        // 判断 router 对象是否已经挂载了 Vue 实例上
        if (this.$options.router) {
          // 这里的 this 就是 Vue 的实例
          _Vue.prototype.$router = this.$options.router;
          // 初始化插件的时候,调用 init
          this.$options.router.init();
        }
      },
    });
  }

// 构造函数,初始化属性
  constructor(options) {
  // options 属性:记录构造函数中传入的对象
    this.options = options;
    // 记录路径和对应的组件(记录路由地址和组件的对应关系)
    this.routeMap = {};
    // data 是一个对象:其中有一个属性 current 记录当前路由地址,该对象是响应式的(调用vue.observe 方法)
    this.data = _vue.observable({
      // 当前的默认路径
      current: "/",
    });
  }

  init() {
    this.createRouteMap();
    this.initComponents(_Vue);
    this.initEvent();
  }

// 初始化 RouterMap 属性,把构造函数中传入的路由规则转换成键值对的形式存储到 RouterMap 对象(路由地址:对应组件)
  createRouteMap() {
    //遍历所有的路由规则,把路由规则解析成键值对的形式存储到 routeMap 中
    this.options.routes.forEach((route) => {
      // 记录路径和组件的映射关系
      this.routeMap[route.path] = route.component;
    });
  }

// 创建 router-link 和 router-vue 两个组件
  initComponents(Vue) {
    // 创建组件
    Vue.component("router-link", {
      // 接收外部传入的参数 to
      props: {
        to: String,
      },
      // 使用运行时版本的 Vue.js
      // 此时没有编译器,直接写一个 render 函数
      // 参数 h 创建虚拟DOM, render 函数中调用 h 函数并将结果返回
      render(h) {
        // h 函数接收三个参数
        return h(
          "a", // 1. 创建的元素对应的选择器
          {
            // 2. 给标签设置属性 attes 指明 DOM 对象属性
            attrs: {
              href: this.to,
            },
            // 给 a 标签注册点击事件
            on: {
              click: this.clickhander,
            },
          },
          [this.$slots.default] // 3. 生成 a 标签之间的内容
        );
      },
      methods: {
        clickhander(e) {
          // 改变浏览器地址栏 pushState 不向服务器发送请求
          history.pushState({}, "", this.to); // data title url
          // this - router-link 组件
          this.$router.data.current = this.to; // 响应式对象 data
          e.preventDefault(); // 阻止事件默认行为
        },
      },
      // template:"<a :href='to'><slot></slot></a>"
      // Vue 的构建版本版本包含:运行时版和完整版
      // 运行时版:不支持 template 模版,需要打包的时候提前编译

      // 完整版:包含运行时和编译器,体积比运行时版本大 10k 左右,程序运行的时候把模版转换成 render 函数
      // vue-cli 创建的项目默认使用的是运行时版本的 Vue.js
      /* 如果想切换成带编译器版本的 Vue.js,需要修改 vue-cli 配置
         项目根目录创建 vue.config.js 文件,添加 runtimeCompiler
         module.exports = { runtimeCompiler: true } */
    });
    const self = this;
    Vue.component("router-view", {
      render(h) {
        const component = self.routeMap[self.data.current];
        return h(component);
      },
    });
  }

// 注册 popstate 事件,监听浏览器历史的变化
  initEvent() {
    // 点击浏览器前进后退
    // 当路径变化之后,重新获取当前路径并记录到 current
    window.addEventListener("popstate", () => {
      // this - VueRouter 对象
      this.data.current = window.location.pathname;
    });
  }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天内卷一点点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值