Vue Router 模拟 - 嵌套路由

vue-router 模拟嵌套路由

vue-router 源码

src 目录结构

  • components
    • link.js router-link组件
    • view.js router-view组件
  • history 历史管理,包含3个模式
    • hash.js - hash模式
    • html5.js - history模式
    • abstract.js - 服务端渲染时使用的抽象模式
    • base.js - 上面3个模式的父类,提供一些公共的内容
    • errors.js - abstract.js使用的方法
  • util 存放一些公共方法
  • create-matcher.js 导出 createMatcher 方法
    • createMatcher - 创建并返回一个匹配器 matcher,匹配器包含 match 方法和 addRoutes 方法
      • match - 根据路由地址匹配相应的路由规则对象
      • addRoutes - 动态添加路由
  • create-route-map.js 导出 createRouteMap 方法
    • createRouteMap - 把所有的路由规则解析成路由表
      • pathList - 一个存储所有路由地址的数组
      • pathMap - 路由表
        • key - 路由地址
        • value - record(路由记录),一个记录路由各种信息的对象
  • index.js - 创建VueRouter实例
  • install.js - 插件注册的方法

模拟实现

约定

约定下划线 ‘_’ 开头的成员是不希望被外部调用的。

搭建基本结构

  • my-vue-router
    • components
      • link.js
      • view.js
    • history
      • base.js
      • html5.js
      • hash.js
    • util
      • route.js
    • create-matcher.js
    • create-route-map.js
    • index.js
    • install.js

编写基本代码

// index.js
import install from './install'
export default class VueRouter {
  constructor(options) {
    this._routes = options.routes || []
  }

  // 注册路由变化的事件
  init(app) {}
}

// 挂载install方法
VueRouter.install = install
// install.js
export let _Vue = null
export default function install (Vue) {
  _Vue = Vue

  // 混入钩子函数
  _Vue.mixin({
    beforeCreate () {
      // router -> VueRouter实例
      // 将router注入到 Vue实例 及其 所有组件
      // Vue组件的创建顺序:
      // 1. 父组件 beforeCreate create ->
      // 2. 子组件 beforeCreate create ->
      // 3. 子组件 mounted ->
      // 4. 父组件 mounted
      if (this.$options.router) {
        // 此时this是Vue根实例(最先执行)
        this._routerRoot = this
        this._router = this.$options.router

        this._router.init(this)
      } else {
        // 此时this是组件实例(父组件先于子组件执行)
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    }
  })
}

简单实现router-link、router-view组件

简单实现两个组件用于启动测试,之后边测试边扩展。

link.js和view.js导出一个包含组件选项的对象。

当前使用运行时版本的Vue,直接写render(不用template)。

// link,js
export default {
  props: {
    to: {
      type: String,
      required: true
    }
  },
  render(h) {
    return h(
      'a',
      {
        domProps: {
          href: '#' + this.to
        }
      },
      [this.$slots.default]
    )
  }
}
// view.js
export default {
  render(h) {
    return h('div', '--router-view')
  }
}
// install.js
import View from './components/view'
import Link from './components/link'

export let _Vue = null
export default function install(Vue) {
  _Vue = Vue

  // 混入钩子函数
  _Vue.mixin({
    beforeCreate() {
      // ...
    }
  })

  // 注册插件
  _Vue.component('RouterView', View)
  _Vue.component('RouterLink', Link)
}

配置路由规则

在官方示例的基础上增加一些路由及嵌套路由(暂时使用静态路由)。

import Vue from 'vue'
// import VueRouter from "vue-router";
import VueRouter from '../my-vue-router'
import Home from '../views/Home.vue'
import Music from '../views/music/Index.vue'
import Pop from '../views/music/Pop.vue'
import Rock from '../views/music/Rock.vue'

Vue.use(VueRouter)

// 路由规则
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () =>
      import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  // 新增路由
  {
    path: '/music',
    name: 'Music',
    component: Music,
    children: [
      {
        path: 'pop',
        name: 'pop',
        component: Pop
      },
      {
        path: 'rock',
        name: 'rock',
        component: Rock
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes
})

export default router

// music/Index.vue
<template>
  <div class="music">
    <div id="nav">
      <router-link to="/music/pop">Pop</router-link> |
      <router-link to="/music/rock">Rock</router-link>
    </div>
    <router-view></router-view>
  </div>
</template>

解析路由表

将路由规则解析到一个路由表(routeMap)中。

createMatcher 创建路由匹配器

create-matcher.js 导出的方法。

创建并返回一个匹配器 matcher,匹配器包含 match 方法和 addRoutes 方法

  • match - 根据路由地址匹配相应的路由规则对象
  • addRoutes - 动态添加路由

这个匹配器会被定义在VueRouter实例的 matcher 属性上。

// create-matcher.js
import createRouteMap from './create-route-map'

export default function createMatcher(routes) {
  // 把所有的路由规则,解析到一个路由表中
  // pathList --> ['/', '/music', ...]
  // pathMap --> { path: { component ....} }
  const { pathList, pathMap } = createRouteMap(routes)

  // 打印查看结果
  console.log(pathList, pathMap)

  function match() {}

  // addRoutes({ path, component })
  function addRoutes(routes) {
    // createRouteMap(添加的路由, 加入到的列表, 加入到的映射表)
    createRouteMap(routes, pathList, pathMap)
  }

  return {
    match,
    addRoutes
  }
}

createRouteMap 解析路由规则

create-route-map.js 导出 createRouteMap 方法

把所有的路由规则解析成路由表

  • pathList - 一个存储所有路由地址的数组
  • pathMap - 路由表
    • key - 路由地址
    • value - record(路由记录),一个记录路由各种信息的对象

record 是一个路径对应的记录。

// create-route-map.js
export default function createRouteMap(routes, oldPathList, oldPathMap) {
  const pathList = oldPathList || []
  const pathMap = oldPathMap || {}

  // 递归遍历所有的路由规则,解析到路由表中
  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap)
  })

  return {
    pathList,
    pathMap
  }
}

// 解析 route,把解析好的规则放入 pathList pathMap 中
function addRouteRecord(route, pathList, pathMap, parentRecord) {
  const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path

  const record = {
    path,
    component: route.component,

    // 如果是子路由的话,记录子路由对应的父 record
    parent: parentRecord
  }

  // 如果已经有了path,相同的path直接跳过
  if (!pathMap[path]) {
    pathList.push(path)
    pathMap[path] = record
  }

  // 判断 route 中是否有子路由
  if (route.children) {
    // 遍历子路由,把子路由也添加到路由表中
    route.children.forEach(childRoute => {
      addRouteRecord(childRoute, pathList, pathMap, record)
    })
  }
}

match

createMatcher 返回的方法。

根据路由地址,匹配路由记录。

如果是子路由,需要匹配和这个子路由地址相关的组件(父组件),即多个路由记录。

// create-matcher.js
//...
export default function createMatcher(routes) {
  // ...
  function match(path) {
    const record = pathMap[path]
    if (record) {
      // 返回子路由匹配搭配到的所有相关组件
      return createRoute(record, path)
    }
    // 返回匹配空数组的结果
    return createRoute(null, path)
  }
}
createRoute

根据路由地址,创建route路由规则对象。

获取path匹配到的所有相关路由。

返回对象:

  • path:用于匹配的路径
  • matched:匹配到的record

如果是子路由,找到它的所有父路由对应的record,插入到数组的第一项中。

因为渲染(create)组件时,先渲染父组件。

// util/createRoute.js
// 根据path匹配到的所有的record,放到matched数组中
export default function createRoute(record, path) {
  // 如果path是子路由,record会有parent属性
  // Vue渲染(create)组件时,先渲染父组件
  // 此时应该把 parent(parentRecord)放在 record(childRecord)前面
  // [ParentRecord, childRecord]
  const matched = []
  while (record) {
    matched.unshift(record)
    record = record.parent
  }
  return {
    path,
    matched
  }
}

History 历史管理

将3种(当前只实现hash 和 html5)模式抽象成单个类。

将它们中相同的部分,抽象到一个父类中(History)。

base.js - History 父类
  • router - VueRouter实例
  • current - 记录当前路径对应的路由规则对象
    • createRoute创建返回的对象:{path, matched}
  • transitionTo()
    • 跳转到指定的路径,根据当前路径获取匹配的路由规则对象 route,然后更新视图
// history/base.js
import createRoute from '../util/route'
export default class History {
  constructor(router) {
    this.router = router
    // 初始化时为首次加载页面的路径对应('/')的路由
    this.current = createRoute(null, '/')
  }

  // 跳转到其他位置,最终会渲染路由对应的组件
  transitionTo(path, onComplete) {
    // 重新改变 current
    this.current = this.router.matcher.match(path)
    // 调试
    console.log(this.current)

    // 首次跳转完成后,调用onComplete监听hash变化
    onComplete && onComplete()
  }
}
hash.js - HashHistory 类
  • 继承 History
  • 确保首次访问地址为 #/
  • getCurrentLocation() 获取当前的路由地址(# 后面的部分)
  • setupListeners() 监听路由地址改变的事件
// history/hash.js
import History from './base'
export default class HashHistory extends History {
  constructor(router) {
    super(router)
    // 确保 首次 访问的路径是 #/
    ensureSlash()
  }

  // 获取当前的路由地址
  getCurrentLocation() {
    return window.location.hash.substr(1)
  }
  // 监听 hashchange 监听路由地址的变化
  setupListeners() {
    window.addEventListener('hashchange', () => {
      this.transitionTo(this.getCurrentLocation())
    })
  }
}

function ensureSlash() {
  // 判断当前是否有 hash
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}
更新 index.js
// index.js
import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
import HTML5History from './history/html5'

export default class VueRouter {
  constructor(options) {
    this._routes = options.routes || []

    // 匹配器 createMatcher(routes) => {match, addRoutes}
    this.matcher = createMatcher(this._routes)

    // 根据模式设置不同的类
    const mode = options || 'hash'
    this.mode = mode

    switch (mode) {
      case 'hash':
        this.history = new HashHistory(this)
        break
      case 'history':
        this.history = new HTML5History(this)
        break
      default:
        throw new Error('mode error')
    }
  }

  // 注册路由变化的事件
  init() {
    const history = this.history
    history.transitionTo(history.getCurrentLocation(), () => {
      // 包裹一层是为了使 setupListeners 的调用者是 history
      // 即确保this指向history
      history.setupListeners()
    })
  }
}

// 挂载install方法
VueRouter.install = install

渲染 router-view

当前路由(current)发生变化时,重新渲染对应的组件。

给 router 对象设置响应式的 _route 属性

方案对比:

  1. Vue.observable(Object)
    1. 创建返回一个响应式的对象,不是向Vue实例设置一个响应式的属性
  2. Vue.set()
    1. 无法向Vue实例,或data(根数据对象)添加响应式属性
  3. Vue.util.defineReactive(Object, key, value)
    1. 向对象中设置/添加一个响应式的属性,即初始值。
    2. 类似 defineProperty
    3. Vue内部方法
// install.js
import View from './components/view'
import Link from './components/link'

export let _Vue = null
export default function install(Vue) {
  _Vue = Vue

  // 混入钩子函数
  _Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this
        this._router = this.$options.router

        this._router.init(this)

        // 定义一个响应式的属性 _route
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    }
  })

  // 注册插件
  _Vue.component('RouterView', View)
  _Vue.component('RouterLink', Link)
}

实现 current改变时重新对 _route 赋值:

创建一个回调变量 cb,用于对 _route 重新赋值。

transitionTo 内部执行这个回调。

创建一个方法 listen,用于设置这个回调。

// history/base.js
import createRoute from '../util/route'
export default class History {
  constructor(router) {
    this.router = router
    this.current = createRoute(null, '/')

    // 这个回调函数,在 hashhistory 中赋值,作用是更改 vue实例上的 _route
    this.cb = null
  }
  listen(cb) {
    this.cb = cb
  }

  transitionTo(path, onComplete) {
    this.current = this.router.matcher.match(path)

    // 调用 cb
    this.cb && this.cb(this.current)

    onComplete && onComplete()
  }
}

// index.js
// import ...

export default class VueRouter {
  constructor(options) {
    // ..
  }

  init(app) {
    const history = this.history
    history.transitionTo(history.getCurrentLocation(), () => {
      history.setupListeners()
    })

    // 添加回调,更新_route
    history.listen(route => {
      app._route = route
    })
  }
}

VueRouter.install = install

定义 $route / $router

定义 $route / $router 的目的是让所有的Vue实例和组件都能访问的到。

  • $route 路由规则对象
  • $router 路由对象(VueRouter实例)

Vue Router 源码直接使用Object.defineProperty向Vue原型添加这两个属性。

注意:它们是只读的(仅设置了get)。

// install.js
import View from './components/view'
import Link from './components/link'

export let _Vue = null
export default function install(Vue) {
  _Vue = Vue

  _Vue.mixin({
    // ...
  })

  // 定义 $router / $route
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })

  // ...
}

router-view

  • 获取当前组件的 $route 路由规则对象
  • 找到里面的 matched 匹配的 record (获取里面的component)
  • 如果没有子路由(/music),matched 匹配到一个,直接渲染对应的组件
  • 如果有子路由(/music/pop),matched 匹配到两个 record(第一个是父组件,第二个是子组件)

Vue渲染组件的顺序是:父组件 -> 子组件。

所以render会先创建父路由的compnent。

渲染的模板中有 router-view,再继续渲染子路由的component

matched返回的顺序也是,父组件在前。

通过 this.$parent 的层级获取 matched 中对应的路由记录(record)。

// component/view.js
export default {
  render(h) {
    // 当前匹配到的路由对象
    const route = this.$route

    // 记录深度
    let depth = 0

    // 记录当前组件为 router-view(其他组件不会被匹配到 matched)
    this.routerView = true

    let parent = this.$parent
    // 遍历父组件,获取深度,作为从 matched 获取 record 的依据
    while (parent) {
      // 如果嵌套了 router-view 组件 深度+1
      if (parent.routerView) {
        depth++
      }
      parent = parent.$parent
    }

    const record = route.matched[depth]

    if (!record) {
      // h函数不传参,默认创建一个注释节点:<!---->
      return h()
    }

    return h(record.component)
  }
}

到此模拟结束。

Vue.util.defineReactive()

简化 defineReactive 源码,解析 Vue 向对象设置响应式的方法

export function defineReactive(obj: Object, key: string, val: any) {
  // 获取属性原描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果属性不可配置,直接返回
  if (property && property.configurable === false) {
    return
  }

  // 获取属性预定义的 getter setter
  const getter = property && property.get
  const setter = property && property.set
  // 获取预定义的初始值(!getter || setter 没理解)
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 如果预定义了 getter,直接使用
      const value = getter ? getter.call(obj) : val
      return value
    },
    set(newVal) {
      // 获取旧值
      const value = getter ? getter.call(obj) : val
      // 判断值是否变化 !== 判断的是 NaN
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // 如果属性本身是响应式的,判断是否设置了 setter
      // 如果没有设置,则什么也不做
      if (getter && !setter) return
      // 如果设置了,则调用预定义的 setter
      if (setter) {
        setter.call(obj, newVal)
      } else {
        // 如果不是响应式的属性,则直接设置
        val = newVal
      }
    }
  })
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值