miniVueRouter

11/14/2020 vue-routerminivue

# VueRouter 原理

  • 利用 Vue 的响应式属性,保存当前路由

  • 利用浏览器监听 hash 或者 url 变化,更新响应式的路由信息

  • 利用 mixIn 给组件添加生命周期初始化路由

  • 将用户配置的路由信息生成路由表并进行保存

  • 注册 router-link router-view 组件,在内部通过当前路由信息匹配路由表从而进行渲染

  • 嵌套路由,这个稍微比较麻烦,在源码中,路由表是嵌套递归的,每次匹配时会生成一个 match 数组,对应索引就是层级,在 route-view 中根据对应层级,展示对应组件

# mini 实现效果

# mini 实现代码

实现较为简单,并且只做了 hash 模式,未实现嵌套路由

index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <!-- <script src="https://unpkg.com/vue-router@2.0.0/dist/vue-router.js"></script> -->
  <title>miniVueRouter</title>
</head>

<body>
  <div id="app">
    <h2>VueRouterDemo</h2>
    <router-link to='/'>home</router-link>
    <router-link to='/about'>about</router-link>
    <hr>
    <router-view></router-view>
  </div>
</body>
<script src="./router-link.js"></script>
<script src="./router-view.js"></script>
<script src="./miniVueRouter.js"></script>
<script src="./component.js"></script>
<script src="./app.js"></script>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

  • mini 版本 引用了 vue,作为 router 原理简单讲解
  • 文件进行了拆分,组件单独拆到对应的文件,分模块查看更加清晰

app.js 与正常使用一致,将 vuerouter 替换为官方的 VueRouter 可以正常运行

app.js
// 正常使用即可
Vue.use(VueRouter)
const router = new VueRouter({
  mode: "hash",
  routes: [
    { path: "/", component: Home },
    { path: "/about", component: About },
  ]
})


const app = new Vue({ el: "#app", router })
1
2
3
4
5
6
7
8
9
10
11
component.js
const Home = {
  template: "<div>{{title}}</div>",
  data() { return { title: '这是Home' } }
}

const About = {
  template: "<div>{{title}}</div>",
  data() { return { title: '这是About' } }
}
1
2
3
4
5
6
7
8
9

# 重要部分

miniVueRouter.js

let _Vue
class VueRouter {
  constructor(options) {
    this.$options = options
    this.init()
  }
  // 在源码里比这个复杂得多,通过 createMatcher 创建一个闭包, 返回一个 {match,addRoutes}
  // 其他地方用的都是 这个叫做matcher的对象,所以 才有了element-admin中动态路由的hach方式
  // 直接生成一个新的VueRouter 使用其matcher对正在使用的matcher进行一个替换 就做到了更新
  createMatcher(routes = []) {
    console.log(routes)
    this.pathObj = {}
    routes.forEach(({ path, component }) => {
      this.pathObj[path] = _Vue.extend(component)
    })
  }
  match(path) {
    path = path || this.current
    return this.pathObj[path]
  }
  initEvent() {
    const hashChange = e => {
      this.current = location.hash.slice(1) || '/'
    }
    window.addEventListener('hashchange', hashChange)
    hashChange()
  }
  init() {
    this.createMatcher(this.$options.routes)
    this.initEvent()
    Vue.util.defineReactive(this, 'current', '/')
  }
}


VueRouter.install = function (Vue, options) {
  _Vue = Vue
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
        this.$router.init(this)
      }
    }
  })
  Vue.component('router-link', RouterLink)
  Vue.component('router-view', RouterView)
}

if (window && window.Vue) {
  Vue.use(VueRouter)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

  • 在源码里比这个复杂得多,通过 createMatcher 创建一个闭包, 返回一个 {match,addRoutes}

  • 其他地方用的都是 这个叫做 matcher 的对象,所以 才有了 element-admin 中动态路由的 hach 方式

  • 直接生成一个新的 VueRouter 使用其 matcher 对正在使用的 matcher 进行一个替换 就做到了更新

router-link.js
const RouterLink = {
  functional: true,
  props: {
    to: { type: [String, Object], default: "" }
  },
  render(h, context) {
    const handleClick = (e) => {
      e.preventDefault()
      location.hash = context.props.to
      Vue.prototype.$router.current = context.props.to
    }
    return h('a', { attrs: { href: context.props.to }, on: { click: handleClick } }, context.children)
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
router-view.js
const RouterView = {
  functional: true,
  render(h, context) {
    const com = Vue.prototype.$router.match()
    return h(com)
  }
}
1
2
3
4
5
6

# 部分源码

源码中处理嵌套部分
// router-view 的render  部分代码
  let depth = 0
    let inactive = false
    // 只要父组件不是根组件就继续遍历
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      // 只要发现routerView  就把深度加一。。。。以匹配嵌套路由
      if (vnodeData.routerView) {
        depth++
      }


    ...
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    ...

    return h(component, data, children)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
createMatcher 源码
export function createMatcher(routes: Array<RouteConfig>, router: VueRouter): Matcher {
  // 根据传递的routes  递归加入生成一个  path列表,并把*放在最后
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  // addRoutes就是继续加路由,,,
  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function match(raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route {
    // 调用match的时候是新建一个route对象,该对象有一个matched    matched是通过recoed  由下至上parent 组成的数组  嵌套路由比较有用
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      //如果解析到了名字属性,直接通过名字查找    record
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys.filter((key) => !key.optional).map((key) => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }

      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // 内部通过 record的正则表达式进行匹配 判断是不是目标路由  如果是  就通过record 生成新的$route
        // $route 不是  record   扩展了很多东西  比如  matched
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }

  function redirect(record: RouteRecord, location: Location): Route {
    const originalRedirect = record.redirect
    let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect

    if (typeof redirect === 'string') {
      redirect = { path: redirect }
    }

    if (!redirect || typeof redirect !== 'object') {
      if (process.env.NODE_ENV !== 'production') {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
      }
      return _createRoute(null, location)
    }

    const re: Object = redirect
    const { name, path } = re
    let { query, hash, params } = location
    query = re.hasOwnProperty('query') ? re.query : query
    hash = re.hasOwnProperty('hash') ? re.hash : hash
    params = re.hasOwnProperty('params') ? re.params : params

    if (name) {
      // resolved named direct
      const targetRecord = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        assert(targetRecord, `redirect failed: named route "${name}" not found.`)
      }
      return match(
        {
          _normalized: true,
          name,
          query,
          hash,
          params,
        },
        undefined,
        location
      )
    } else if (path) {
      // 1. resolve relative redirect
      const rawPath = resolveRecordPath(path, record)
      // 2. resolve params
      const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
      // 3. rematch with existing query and hash
      return match(
        {
          _normalized: true,
          path: resolvedPath,
          query,
          hash,
        },
        undefined,
        location
      )
    } else {
      if (process.env.NODE_ENV !== 'production') {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
      }
      return _createRoute(null, location)
    }
  }

  function alias(record: RouteRecord, location: Location, matchAs: string): Route {
    const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
    const aliasedMatch = match({
      _normalized: true,
      path: aliasedPath,
    })
    if (aliasedMatch) {
      const matched = aliasedMatch.matched
      const aliasedRecord = matched[matched.length - 1]
      location.params = aliasedMatch.params
      return _createRoute(aliasedRecord, location)
    }
    return _createRoute(null, location)
  }

  function _createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: Location): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoutes,
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

  • 可以看到 接收的是 用户配置的 routes,以及 router 实例
  • 最终返回的是 match, addRoutes,
  • 在 router 中使用的匹配相关都是使用的此对象
  • 所以动态生成路由原理就是替换为新的 matcher
Last Updated: 12/11/2020, 11:27:59 AM