Vue框架源码:Vue基础回顾、Vue-Router实现-笔记

250 阅读4分钟

这个模块主要是对Vue.js进行深度的原理剖析,本任务包含以下几点:

  • 快速回顾Vue基础用法
  • Vue-Router原理分析及实践
  • 介绍虚拟DOM、分析Snabbdom源码
  • 实现最小版本Vue,主要进行响应式原理分析及实现
  • Vue源码分析,初始化过程、首次渲染过程、响应式过程、编译模板过程

Vue-Router浅析

本次回顾的Vue路由相关内容有:

  1. 动态路由,类似:id的路由形式,可以拿参数。
  2. 嵌套路由,路由配置中的children属性。
  3. 编程式导航,一般路由跳转是使用router-link组件,编程式是使用$router的push、replace、go等等方法,实现路由跳转。
  4. Hash和HIstory模式区别,它们都是客户端路由的实现方式,路径发生变化都不会刷新浏览器,是由JS来监控路由变化,渲染不同的组件。想要用好HIstory模式,还需要服务端配置支持。
    1. hash模式基于锚点,锚点作为路由地址,路由变化触发onhashchange事件,根据路径决定页面显示内容。
    2. history模式基于HTML5中的History API,主要使用pushState和replaceState。
    3. pushState和push方法有区别,push方法会使路径发生变化,向服务器发送请求;而pushState方法只会改变浏览器地址栏地址,并记录到路由栈历史记录里。
    4. pushState是IE 10以后才支持,IE 10之前还是得使用hash模式

History模式

这里有个新概念,History模式需要服务器支持,为什么呢?

  1. 比如在单页应用中,只有index.html,而没有login.html。刷新浏览器请求login页面应该会返回404。
  2. 而Vue-Cli帮我们做好了配置处理,所以刷新不会报错。

在NodeJS环境的History模式

在nodeJs中可以使用connect-history-api-fallback这个中间件来处理history模式,其原理是:当浏览器发送一个页面请求时,服务器如果没有这个页面,就会将index.html页面返回,然后浏览器再根据url去渲染对应的组件出来。

具体原理可以查查百度,参考链接

在Nginx下的History模式

首先自己配置一个Nginx服务器,将示例代码放在html目录下。如果未进行history配置,他依然会进行404报错,所以我们就需要修改Nginx配置文件了。

在Nginx的nginx.conf文件下配置根路径,增加try_files字段:

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

$uri就是当前请求的路径,它会去找路径对应文件,找到了则返回,没找到则接着找默认首页文件,也就是index.html。如果还没找到则直接返回首页,也是index.html。

当我们访问单页面应用的特定路径时,nginx服务器则会返回index.html,然后在客户端根据路由地址去解析对应的组件。

模拟实现Vue-Router

通过模拟了解内部原理,主要实现History模式,这里有些Vue相关概念的前置知识:

  • 插件
  • 混入
  • Vue.observable()
  • 插槽
  • render函数
  • 运行时和完整版的Vue

这里简单讨论下Vue下的两种路由模式:

Hash模式

  • URL中#后面的内容作为路径地址。
    • #后面的地址改动,不会触发浏览器的重新刷新。
  • 监听hashchange事件。
  • 根据当前路由地址找到对应组件重新渲染。

History模式

  • 通过history.pushState方法改变地址栏,将当前地址记录到浏览器访问历史中,并不会真正跳转指定路径,不会向服务器发送请求。
  • 监听popState事件。
    • 要注意pushState、replaceState不会触发该事件,点击浏览器的前进后退,或调用go、forward方法会触发。
  • 根据当前路由地址找到对应的组件重新渲染。

原理分析

使用Vue-Router的核心代码:

Vue-Router的类图,它描述了Vue-Router包含的属性和方法:

我们就根据这张类图来进行实现。先来介绍一下属性:

  • options:记录构造函数中传入的对象。
  • data:它是一个对象,它有一个属性current,记录当前路由地址。它是一个响应式对象。
  • routeMap:记录路由地址和组件对应关系,它是一个对象。

然后是其方法:

  • 带+号的是对外公开的方法,带_的是静态方法,例如_Install方法,它用来实现Vue插件机制。
  • init方法是用来调用下面的三个方法。将不同功能代码分割到不同方法中实现。
    • initEvent:注册popState事件。
    • createRouteMap:初始化routeMap属性,将在构造函数中,传入的路由规则转换成键值对形式。
    • initComponents:创建router-linkrouter-view组件的。

在使用自己写的router-link组件时,报了如下错误:

意思是正在使用运行时版本的Vue,不支持template,要么将模板预编译为render函数,要么使用带编译器的构建。这里说说Vue的构建版本,分为运行时版和完整版:

  • 运行时版: 不支持Template模板,需要打包的时候提前编译,将其编译为render函数,然后使用render函数创建虚拟DOM,渲染到视图。
  • 完整版: 包含运行时和编译器,因为多了编译器,所以体积比运行时版大了10K左右,它的作用是在程序运行时把模板转换为render函数,所以性能不如前者。

Vue-Cli创建的项目默认使用运行时版Vue,那么我们如何来解决呢?这里有两种方式:

  • 切换为完整版本的Vue官方文档。我们创建一个vue.config.js文件,然后配置runtimeCompiler属性为true
  • 运行时版本,则使用 render函数替换Template:
    • 对于单文件组件,它也是使用Template,它能正常使用是因为在打包时,将其编译成了render函数,也就是预编译。
    • 使用render函数的h函数对Template进行编译。h函数接收三个参数:元素选择器、设置属性、子元素。

完整Vue-Router实现代码

// vue-router.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 1 判断当前插件是否被安装
    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    // 2 把Vue的构造函数记录在全局中,将来要在Vue-Router实例方法中,使用到这个Vue的构造函数,比如创建组件时需使用Vue.Component
    _Vue = Vue
    // 3 把创建Vue的实例时传入的router对象注入到所有Vue实例,让所有实例共享一个成员
    // 在创建Vue实例时,Vue会将options中自定义的属性和Vue构造函数中定义的属性合并为vm.$options
    _Vue.mixin({
      beforeCreate() {
        // 此处只执行一次,如果是组件则不执行,是Vue实例则执行
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()
        }
      }
    })
  }

  constructor(options) {
    // 初始化options、routeMap、data属性
    this.options = options

    // 在router-view组件中,会根据路由地址去routeMap中找对应组件
    this.routeMap = {}
    // data是一个响应式对象,因为它存储着当前路由地址,地址变化时要加载对应组件
    this.data = _Vue.observable({
      current: location.pathname || '/' // 默认为 /
    })

    this.init()
  }

  init() {
    this.createRouteMap()
    this.initComponent(_Vue)
    this.initEvent()
  }

  // 将构造函数传入的routes(路由配置表),转换成键值对形式,传入routeMap对象中
  // 健就是路由地址,值就是所对应的组件
  createRouteMap() {
    // 遍历所有的路由规则,把路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponent(Vue) {
    // 通过Vue.component注册组件
    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)
          // window.location = this.to
          this.$router.data.current = this.to
          e.preventDefault()
        }
      }
      // template: "<a :href='to' @click={{this.clickhander}}><slot></slot><>"
    })

    const self = this
    Vue.component('router-view', {
      render(h) {
        // 需找到当前路由地址,根据路由地址到routerMap中找到对应的组件
        // 使用h函数将组件转换为虚拟DOM
        const cm = self.routeMap[self.data.current] || self.routeMap['*']
        return h(cm)
      }
    })
  }

  initEvent() {
    window.addEventListener('popstate', () => {
      this.data.current = window.location.pathname
    })
  }
}