1. 初始化项目
先使用 vue 脚手架(这里使用的是 vue-cli 2)初始化项目,执行命令 vue init webpack demo01-vue-router
,生成的目录结构如下:
node
├── buile
│ ├── build.js
│ ├── check-versions.js
│ ├── logo.png
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── node_modules
├── src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ ├── router
│ │ └── index.js
│ ├── App.vue
│ └── main.js
├── static
│ └── .gitkeep
├── test
│ ├── e2e
│ │ ├── custom-assertions
│ │ │ └── elementCount.js
│ │ ├── specs
│ │ │ └── test.js
│ │ ├── nightwatch.conf.js
│ │ └── runner.js
│ └── unit
│ ├── specs
│ │ └── HelloWorld.spec.js
│ ├── .eslintrc
│ ├── jest.conf.js
│ └── setup.js
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── index.html
├── package.json
└── README.md
1.1 src/router/index.js
js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
// 路由映射表
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
其中 Vue.use(Router)
作用是引入插件,并调用了 vue-router
插件的 install
1.2 src/main.js
js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router, // 传入了路由器实例给根实例作为组件选项
components: { App },
template: '<App/>'
})
1.3 router-link 和 router-view
思考两个问题:
- 1、为什么可以组件中可以直接使用
router-link
和router-view
实现路由跳转?(router-view 路由的出口,承载内容的容器) - 2、为什么组件中能使用
this.$router
访问路由器的实例,并能通过this.$router.push
进行命令式的路由跳转?
html
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
js
this.$router.push('/')
this.$router.push('/about')
能直接使用说明进行了全局注册,那在哪里注册的呢?我们来看一下 vue-router 的 install 方法:
在 vue-router 的 install 方法中做了什么呢?
- 在 Vue 构造函数的原型上添加 <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r ,这样可以在所有的子组件中使用 t h i s . router,这样可以在所有的子组件中使用 this. </math>router,这样可以在所有的子组件中使用this.router
- 注册了两个全局组件:router-link 和 router-view
手写 vue-router 需要实现上面两件事,
2. 需求分析
- 1、SPA 页面路由发生变化,但是页面不能刷新
- hash: #/about
- History API: /about
- 2、根据路由的变化页面显示对应的内容
- router-view
- 数据响应式:定义一个变量 curr 持有 url 地址,当变量发生变化时动态的重新执行 render
实现步骤如下:
- 创建并导出 VueRouter 类
- 处理路由选项
- 监听 url 变化,hashChange
- 响应变化
- 实现 install 方法
- 在 Vue 原型上添加 $router
- 全局注册组件:router-link 和 router-view
3. 开始
在初始化项目中,默认生成了路由配置文件 src/router/index.js
,现在我们重新创建我们的路由配置文件及插件 vue-router
的文件:
src/myRouter/my-vue-router.js
- 实现 vue 插件 VueRouter
- 插件必须有一个静态的 install 方法
- 注册 $router
- 注册全局组件
- 插件必须有一个静态的 install 方法
- 实现 vue 插件 VueRouter
src/myRouter/index.js
- 正常的路由配置文件,内容同
src/router/index.js
- 其中
import Router from 'vue-router'
改为import Router from 'my-vue-router'
- 正常的路由配置文件,内容同
在 main.js 中修改相应的引用:
js
import router from './router'
// 改为
import router from './myRouter'
3.1 编写 my-vue-router.js
js
let Vue
class VueRouter {
constructor(options){
this.$options = options;
// 缓存path和route映射关系
this.routeMap = {};
this.$options.routes.forEach(item => {
this.routeMap[item.path] = item;
})
Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
window.addEventListenner('hashchange', () => {
this.current = window.location.hash.slice(1)
})
window.addEventListenner('load', () => {
this.current = window.location.hash.slice(1)
})
}
}
VueRouter.install = function(_Vue) {
Vue = _Vue
// 注册 $router
Vue.mixin({
beforeCreate(){
if(this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 注册全局组件 router-link
Vue.component('router-link', {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
},
}, [this.$slots.default]);
},
})
// 注册全局组件 router-view
Vue.component('router-view', {
render(h) {
const { routeMap, current } = this.$router;
const component = routeMap[current] ? routeMap[current].component : null;
return h(component);
}
})
}
export default VueRouter;
3.2 对 my-vue-router.js
内容进行简单解说
1、一个插件的的基本结构如下:
js
class VueRouter {}
VueRouter.install = function(Vue){
// do something
};
export default VueRouter;
2、文件中定义的全局变量 Vue:
js
let Vue
用于存放 Vue 构造函数,方便使用,这样使用 vue 不需要通过 import 形式引入,打包的时候就不会把 vue 打包进去。
3、类的构造函数:
js
constructor(options){
this.$options = options;
// 缓存path和route映射关系
this.routeMap = {};
this.$options.routes.forEach(item => {
this.routeMap[item.path] = item;
})
Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
window.addEventListenner('hashchange', () => {
this.current = window.location.hash.slice(1)
})
window.addEventListenner('load', () => {
this.current = window.location.hash.slice(1)
})
}
先返回来看看 src/myRouter/index.js
中实例化 VueRouter 的部分:
js
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
构造函数 constructor 中的参数 options 就是通过 new Router
实例化 VueRouter 时传入的参数,并将传入的选项保存到 VueRouter 实例中。
通过 Vue.util.defineReactive
创建一个响应式数据 current,当 current 的值发生变化的时候,与它相关的函数会重新执行。
window.addEventListenner('hashchange', () => { })
监听url中 hash 的变化。
4、VueRouter.install
我们再回来看看 src/myRouter/index.js
:
js
Vue.use(Router)
Vue.use 的作用是引入插件,并调用了插件的 install
方法,这里 VueRouter.install
方法的 形参1 是 Vue 的构造函数。
- 挂载 $router
js
Vue.mixin({
beforeCreate(){
if(this.$options.router) {
// 将路由器实例赋值给了Vue原型
Vue.prototype.$router = this.$options.router
}
}
})
因为 $router 要添加到 Vue 构造函数的原型上需要确保 Vue 已实例化,也就是说将 $router 添加到 Vue 原型上的操作需要延迟到未来的某个时刻(Vue实例创建之时),这里使用 Vue 的全局混入,通过钩子函数 beforeCreate 来实现这个延迟。
【注意】因为 vue-router 与 vue 是强耦合的关系,所以我们可以使用 Vue.mixin
4、注册全局组件 router-link
我们通常使用 router-link 是这样的:<router-link to="/about">XXX</router-link>
,不难发现,跟我们平时开发过程中封装的组件使用是一样的。这里使用 Vue.component 创建组件:
js
Vue.component('router-link', {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
},
}, [this.$slots.default]);
},
})
this.$slots.default
获取当前组件的默认插槽,vue slot插槽的相关使用可以查阅:vue slot插槽
【注意】:为什么这里使用的是:
js
Vue.component('router-link', {
render: {}
})
而不是:
js
Vue.component('router-link', {
template: ''
})
原因可参考:Vue 中的 Runtime + Compiler 和 Runtime-only 的简单理解
5、注册全局组件 router-view
js
Vue.component('router-view', {
render(h) {
const { routeMap, current } = this.$router;
const component = routeMap[current] ? routeMap[current].component : null;
return h(component);
}
})
这里的 this 指的是 Vue 实例,this.$router 指的是 VueRouter 实例。