手写一个简易版的 vue-router(支持 history 和 hash)

简介:vue-router 相信大家都用过,内部到底是如何实现的呢,我根据源码以及一些参考资料,手写了一个简易版的 vue-router,供大家参考。

本文github 源码地址

下面是实现步骤

1.项目依赖:(先安装,步骤略)
package.json

1
{
2
  "name": "vue-my-router",
3
  "version": "0.1.0",
4
  "private": true,
5
  "scripts": {
6
    "serve": "vue-cli-service serve",
7
    "build": "vue-cli-service build",
8
    "lint": "vue-cli-service lint"
9
  },
10
  "dependencies": {
11
    "core-js": "^3.6.4",
12
    "register-service-worker": "^1.7.1",
13
    "vue": "^2.6.11",
14
    "vue-router": "^3.1.6",
15
    "vuex": "^3.1.3"
16
  },
17
  "devDependencies": {
18
    "@vue/cli-plugin-babel": "~4.3.0",
19
    "@vue/cli-plugin-eslint": "~4.3.0",
20
    "@vue/cli-plugin-pwa": "~4.3.0",
21
    "@vue/cli-plugin-router": "~4.3.0",
22
    "@vue/cli-plugin-vuex": "~4.3.0",
23
    "@vue/cli-service": "~4.3.0",
24
    "@vue/eslint-config-prettier": "^6.0.0",
25
    "babel-eslint": "^10.1.0",
26
    "eslint": "^6.7.2",
27
    "eslint-plugin-prettier": "^3.1.1",
28
    "eslint-plugin-vue": "^6.2.2",
29
    "less": "^3.0.4",
30
    "less-loader": "^5.0.0",
31
    "lint-staged": "^9.5.0",
32
    "prettier": "^1.19.1",
33
    "vue-template-compiler": "^2.6.11"
34
  }
35
}

2.项目目录结构

在这里插入图片描述
3.核心代码
/src/router/router.js

1
export let Vue
2
3
class MyRouter {
4
  constructor(options) {
5
    this.$options = options
6
7
    // 路由数组映射成键值对
8
    this.routeMap = {}
9
10
    // 借助vue使其是响应式的,原因是为了更新router-view组件
11
    this.app = new Vue({
12
      data() {
13
        return { current: '/' }
14
      }
15
    })
16
  }
17
18
  // vue 插件必须实现的 install 方法
19
  static install(_Vue) {
20
    Vue = _Vue
21
22
    // 在混入的beforeCreate生命周期中为Vue.prototype添加  $router
23
    Vue.mixin({
24
      beforeCreate() {
25
        // 这里的this指向的是vue 的实例
26
        if (this.$options.router) {
27
          Vue.prototype.$router = this.$options.router
28
          Vue.prototype.$router.push = this.$options.router.push
29
          this.$options.router.init()
30
        }
31
      }
32
    })
33
  }
34
35
  // 路由的push方法
36
  push(path) {
37
    const { mode } = this.$options
38
    const _path = path || '/'
39
    if (mode === 'history') {
40
      window.history.pushState(_path, null, _path)
41
      this.app.current = _path
42
    } else {
43
      window.location.hash = '#' + _path
44
    }
45
  }
46
47
  // 初始化
48
  init() {
49
    this.createRouteMap()
50
    this.bindEvent()
51
    this.registerComponents()
52
  }
53
54
  // 创建路由映射 这里是一层遍历,实际是需要递归操作的,vue-router支持子路由
55
  createRouteMap() {
56
    this.$options.routes.forEach(item => {
57
      this.routeMap[item.path] = item.component
58
    })
59
  }
60
61
  // 绑定事件
62
  bindEvent() {
63
    const { mode } = this.$options
64
    // 绑定this的原因是this.onLocationChange函数里的this指向的是window,遵循谁调用指向谁原则
65
    // 然而我们希望this指向的是MyRouter 的实例
66
    window.addEventListener('load', this.onLocationChange.bind(this))
67
    const event = mode === 'history' ? 'popstate' : 'hashchange'
68
    // popstate 方法并不会监听到 window.history.pushState 和 window.history.replaceState
69
    // this.onLocationChange  不会执行
70
    window.addEventListener(event, this.onLocationChange.bind(this))
71
  }
72
73
  // 修改路由
74
  onLocationChange() {
75
    const { mode } = this.$options
76
    const isHistory = mode === 'history'
77
    let path = '/'
78
    if (isHistory) {
79
      path = window.location.pathname
80
    } else {
81
      if (window.location.hash) {
82
        path = window.location.hash.slice(1)
83
      } else {
84
        window.location.href = '#/'
85
      }
86
    }
87
    this.app.current = path || '/'
88
  }
89
90
  // 注册全局组件
91
  registerComponents() {
92
    const _this = this
93
94
    // 全局注册router-link
95
    Vue.component('router-link', {
96
      props: {
97
        to: {
98
          type: String,
99
          required: true,
100
          default: ''
101
        }
102
      },
103
      render(h) {
104
        const { mode } = _this.$options
105
        const isHistory = mode === 'history'
106
        const attrs = isHistory ? { href: '' } : { href: '#' + this.to }
107
        const clickHandler = e => {
108
          e.preventDefault()
109
          const _path = this.to || '/'
110
          window.history.pushState(_path, null, _path)
111
          _this.app.current = this.to
112
        }
113
        const on = isHistory
114
          ? {
115
              click: clickHandler
116
            }
117
          : {}
118
119
        // h即createElement,用法参考官方链接
120
        // https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0
121
        return h(
122
          'a',
123
          {
124
            class: {
125
              'router-link': true
126
            },
127
            attrs,
128
            on
129
          },
130
          // this.$slots.default default 属性包括了所有没有被包含在具名插槽中的节点
131
          // https://cn.vuejs.org/v2/api/#vm-slots
132
          [this.$slots.default]
133
        )
134
      }
135
    })
136
137
    // 全局注册router-view
138
    Vue.component('router-view', {
139
      // eslint-disable-next-line no-unused-vars
140
      render(h) {
141
        const Component = _this.routeMap[_this.app.current]
142
        // 下面使用的是jsx语法,也可以用h(Component)
143
        return <Component />
144
      }
145
    })
146
  }
147
}
148
149
export default MyRouter

/src/App.vue

1
<template>
2
  <div id="app">
3
    <div id="nav">
4
      <h1>便签导航</h1>
5
      <router-link to="/">Home</router-link> |
6
      <router-link to="/about">About</router-link>
7
8
      <h1>编程式导航</h1>
9
      <button @click="toHome">Home</button>
10
      <button @click="toAbout">About</button>
11
    </div>
12
    <router-view />
13
  </div>
14
</template>
15
16
<script>
17
  export default {
18
    created() {
19
      console.log(this.$router)
20
    },
21
    methods: {
22
      toHome() {
23
        this.$router.push('/')
24
      },
25
      toAbout() {
26
        this.$router.push('/about')
27
      }
28
    }
29
  }
30
</script>
31
32
<style lang="less">
33
  #app {
34
    font-family: Avenir, Helvetica, Arial, sans-serif;
35
    -webkit-font-smoothing: antialiased;
36
    -moz-osx-font-smoothing: grayscale;
37
    text-align: center;
38
    color: #2c3e50;
39
  }
40
41
  #nav {
42
    padding: 30px;
43
44
    a {
45
      font-weight: bold;
46
      color: #2c3e50;
47
48
      &.router-link-exact-active {
49
        color: #42b983;
50
      }
51
    }
52
  }
53
</style>

4.实现效果
history
在这里插入图片描述
hash
在这里插入图片描述

参考链接

1.https://cn.vuejs.org/v2/guide/render-function.html

2.https://github.com/57code/vue-study

3.https://github.com/vuejs/vue-router