手动实现简易Vue-router
-
vue-router 包的使用
-
导入
import VueRouter from 'vue-router'
-
在Vue上进行VueRouter plugin 注册
Vue.use(VueRouter)
-
创建VueRouter 实例并导出 , 创建实例的时候传入配置项,routes是路由配置表
-
export default new VueRouter({ mode: 'hash', base: process.env.BASE_URL, routes })
-
-
最后创建Vue实例的时候,配置项中传入这个导出的VueRouter 实例
-
项目中则使用
router-link
和router-view
即可简易使用
-
-
根据上述vue-router包的使用, 可以自己实现简易vue-router
-
创建一个VueRouter的类,该类的constructor传入一个配置项options,我们需要将这个配置项目放入实例上
-
VueRouter这个类实现一个install方法,因为Vue.use(xxx) 注册plugin的时候就会调用 传入plugin的install方法
-
install 方法接收一个参数,这个参数就是我们Vue这个类, 我们需要将这个Vue保存在这个模块设置的变量Vue上,之后在VueRouter这个类的constructor方法中我们需要使用 Vue.util.defineReactive 这个方法,将数据进行响应式化
-
其次我们在install 方法中 利用Vue.mixin() 方法创建一个全局mixin, 这个mixin中 追入一个生命周期beforeCreate,这个每个组件实例被创建的时候,这个mixin都会执行,通过条件判断
this.$options.router
是否存在,如果存在说明这个这个组件就是根组件。ps: 只有根组件 new Vue 创建实例时,我们的配置项中才会传入router 这个实例。 我们将router实例挂在到Vue 的原型上,这样以后组件中就可以通过this.$router 进行一些路由操作-
Vue.mixin({ beforeCreate() { if (this.$options.router) { Vue.prototype.$router = this.$options.router } } })
-
-
之后我们还需要全局注册上
router-link
和router-view
这两个组件- router-link 主要就是render出一个a标签,他的href属性就是 # + 传递过来的to属性,名称就是默认插槽。ps: #拼接是为了使用hash 避免a标签跳转导致整个页面的刷新
- router-view 则主要是想办法拿到路由配置表,然后 render 当前匹配下的组件即可
-
-
整体代码
let Vue
class VueRouter {
constructor(options) {
const initial = window.location.hash.slice(1) || '/'
this.$options = options
// 让 current 变为响应式数据,一旦发生变化,则会导致render函数重新执行
Vue.util.defineReactive(this, 'current', initial)
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1)
})
}
}
VueRouter.install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
}
}, this.$slots.default)
}
})
Vue.component('router-view', {
render(h) {
let Component = null
// 想要拿到路由配置表
// 由于前面全局注册了mixin 并且已经将router实例放到了Vue 的原型对象上。所以下面可以通过this.$router获取
const routes = this.$router.$options.routes || []
const route = routes.find(route => route.path === this.$router.current)
if (route) {
Component = route.component
}
return h(Component)
}
})
}
export default VueRouter
嵌套路由实现
- router-view 深度标记
- 路由匹配时获取代表深度层级的matched数组
Vue.component('router-view', {
render(h) {
// 标记当前router-view 深度
this.$vnode.data.routerView = true
let depth = 0
console.log(this.$router.matched);
/**
* 递归往当前router-view 组件父级查找,
* 如果该router-view父级依然依然存在router-view组件,则这个router-view的深度depth需要+1
*/
let parent = this.$parent
while (parent) {
const vnodeData = parent.$vnode && parent.$vnode.data
if (vnodeData) {
if (vnodeData.routerView) {
// 说明当前parent是一个router-view
depth++
}
}
parent = parent.$parent
}
let Component = null
// 想要拿到路由配置表。
// matched在VueRouter构造器中已处理,创建实例的时候会递归匹配,将符合要求的路由对应表存入matched
const route = this.$router.matched[depth]
if (route) {
Component = route.component
}
return h(Component)
}
})
class VueRouter {
constructor(options) {
this.$options = options
this.current = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'matched', [])
// match方法可以递归便利路由表,获取匹配关系数组
this.match()
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1)
this.matched = []
this.match()
})
}
match(routes) {
routes = routes || this.$options.routes
/**
* 递归遍历路由表
* 当前地址栏的路由地址current进行匹配排查。
* 如果 current === '/' 那么只需要将route.path === '/' 的对应路由放入matched中即可
* 如果 current === '/about/info' 那么就需要将 current 中包含的对应路由 /about 和 /about/info 都放入 matched中
* 因此需要 route.path !== '/' && this.current.indexOf(route.path) !== -1 条件判断
* 然后判断 该route下是否存在children 并且children.length>0, 然后递归查找这个 route.children
*/
for (const route of routes) {
if (route.path === '/' && this.current === '/') {
this.matched.push(route)
return
}
// about/info
if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
this.matched.push(route)
if (route.children && route.children.length > 0) {
this.match(route.children)
}
return
}
}
}
}
手动实现Vuex
Vuex 实际上也是vue 生态中的一个插件,其实类似于vue-router , vuex 我们也可以通过一个Store类来实现,不同的是 Store 创建实例的时候是
new Vuex.Store({
...
})
Vue.use(Vuex)
到处Vuex 可以是一个对象, 包含属性 Store 和 install 方法,这样就满足vuex 的使用方式了。
// 使用
import Vue from 'vue'
import Vuex from './kvuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
add(state) {
state.counter++
}
},
actions: {
dispatchAdd({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
getters: {
doubleCounter(state) {
return state.counter * 2
}
},
modules: {}
})
export default store
创建 Store 类和 install 方法
install方法主要做两件事
-
保存传入的Vue
-
创建mixin方法,在生命周期beforeCreate中将根组件传入的store实例保存到Vue到原型对象上,方便之后在组件中通过 $store.xxx 使用
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
Store类要做的事主要有将创建store实例时传递的参数进行保存。 将state 进行响应式化。
其次定义好commit和 dispatch 方法。注意由于这两个方法中有使用this, 因此定义的时候需要重新绑定this指针
再次就是实现getters。 思路就是先将getters取过来,然后通过绑定到创建的Vue实例 _vm 的computed上,从而实现计算属性
let Vue
class Store {
constructor(options) {
this._mutations = options.mutations
this._actions = options.actions
this._wrappedGetters = options.getters
// 定义computed 选项
const computed = {}
this.getters = {}
// { doubleCounter(state){} }
const store = this
Object.keys(this._wrappedGetters).forEach(key => {
// 获取用户定义的getter
const fn = store._wrappedGetters[key]
// 转换为computed 可以使用的无参数形式
computed[key] = function () {
return fn(store.state)
}
// 为getters 定义只读属性
Object.defineProperty(store.getters, key, {
get() {
return store._vm[key]
}
})
})
this._vm = new Vue({
data: {
$$state: options.state
},
computed
})
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state() {
return this._vm._data.$$state
}
set state(v) {
throw new Error('please use replaceState to reset')
}
commit(type, payload) {
const entry = this._mutations[type]
if (!entry) {
throw new Error('this mutations is not found')
}
entry(this.state, payload)
}
dispatch = (type, payload) => {
const entry = this._actions[type]
if (!entry) {
throw new Error('this actions is not found')
}
entry(this, payload)
}
}
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
export default {
Store,
install
}