手写一个简易版的 vue-router(支持 history 和 hash)
简介:vue-router 相信大家都用过,内部到底是如何实现的呢,我根据源码以及一些参考资料,手写了一个简易版的 vue-router,供大家参考。
下面是实现步骤
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