vue的router原理及其实现
路由模式
在vue中路由主要分为了两种模式 分别是hash模式和history模式
hash模式 主要是通过window上的hashchange事件进行实现的
// 首次加载时
document.addEventListener('DOMContentLoaded', () => {
view.innerHTML = location.hash.slice(1);
})
window.addEventListener('hashchange', () => {
view.innerHTML = location.hash.slice(1);
}
history模式 主要是通过h5提供的history的api以及监听popstate事件实现的
function routerChange (pathname) {
history.pushState(null, null, pathname);
view.innerHTML = location.pathname;
}
window.addEventListener('popstate', () => {
view.innerHTML = location.pathname;
}
)
两种模式的区别
- Hash模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL
- History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
- Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
vue中路由的实现
首先给出官网的引入的方式
import VueRouter from 'vue-router'
Vue.use(VueRouter)
可以看出对于路由的使用分为了import与use两部分
import
对于import,我们先创建一个vue-router文件夹 并且创建index.js文件
class VueRouter {
}
export default VueRouter;
在实例化VueRouter对象的时候,我们通常会传入两个参数,分别是mode
来确定路由模式,以及routes
来记录相关的路由信息
对于routes中的路由信息而言,对于不同的router-view而言,应该对应了不同的组件,所以在实例化对象的时候应该对于routes所传入的路由信息进行处理,方便后续的处理
createRouteMap (routes) {
const routeMap = {};
for(let i = 0; i < routes.length; i ++) {
const route = routes[i];
routeMap[route.path] = route.component;
}
return routeMap;
通过该方法拿到一个属性名对应路径,属性值对应所需要渲染组件的对象
然后初始化我们的路由的监听事件
init () {
if(this.mode === 'hash') {
location.hash ? '' : location.hash = '/';
document.addEventListener('DOMContentLoaded', () => {
this.history.current.path = location.hash.slice(1);
})
window.addEventListener('hashchange', () => {
this.history.current.path = location.hash.slice(1);
})
} else {
document.addEventListener('DOMContentLoaded', () => {
this.history.current.path = location.pathname;
})
window.addEventListener('popstate', () => {
this.history.current.path = location.pathname;
})
}
在构造函数中调用这两个函数完成路由的初始化
constructor (options) {
this.routeMap = this.createRouteMap(options.routes || []);
this.history = new History();
this.mode = options.mode || 'hash';
this.init();
Vue.use()
对于Vue.use()
而言,Vue在处理该条语句的时候,会自动调用传入参数的install
属性
当前的Vue会自动作为install函数的参数传如,根据这个特性我们就可以在install这个函数中完成对路由的初始化了,包括了router-link
,router-view
两个标签以及$route
和$router
$route 和 $router的处理
首先我们知道一点在我们使用router的时候,我们只是在根实例初始化的时候挂载了router这一个属性,那么如何保证后续的子组件以及所有组件都可以访问到这一个属性呢?
这是我们想到了使用vue提供的mixin
功能
以下是官方对于混入的描述
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
我们通过混入一个beforeCreate ()
的生命周期函数,来判断我们的路由中传入的参数
Vue.mixin({
beforeCreate () {
if(this.$options.router) {
this._router = this.$options.router;
// 定义该对象的响应属性
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
}
router就是我们在实例化对象的时候传入的参数,那么route呢?
通过对比可以发现route和官方提供VueRouter实例的history对象是一致的
所以我们简单定义自己的history对象并且导出
class History {
constructor () {
this.current = {
path: null
}
}
}
export default History
然后在原型链上操作
Object.defineProperty(Vue.prototype, '$router', {
get () {
return this.$root._router;
}
})
Object.defineProperty(Vue.prototype, '$route', {
get () {
return this.$root._route;
}
}
router-link
router-link的默认tag是一个a
标签,主要有to和tag两个属性,其中to属性是必须的
props: {
to: {
type: String,
required: true,
},
tag: {
type: String,
default: 'a',
}
}
对于router-link而言,其处理逻辑主要分为了两部分,包括对于用户的点击的处理 和 相关的渲染
点击相关,通过路由模式的不同来,进行不同的判断
handleClick () {
const mode = this.$router.mode;
if(mode === 'hash') {
location.hash = this.to;
} else {
history.pushState(null, null, this.to);
this.$router.history.current.path = this.to;
}
渲染相关
<router-link to="/">首页</router-link>
// 会被默认渲染成a标签
利用render函数完成渲染
render (h) {
const data = {};
const to = this.to;
// 获取地址
const mode = this.$router.mode;
// 获取路由模式
if(this.tag === 'a' && mode === 'hash') {
const href = '#' + to;
data.attrs = { href };
} else {
data.on = { click: this.handleClick };
}
return h(this.tag, data, this.$slots.default);
// 使用默认插槽 让新标签里的文本与router-link一致
}
router-view
把对应路径的组件渲染出来即可
export default {
functional: true,
render (h, { parent }) {
const routeMap = parent.$router.routeMap;
const path = parent.$route.path;
return h(routeMap[path]);
}
}
最后的最后在install中全局注册该组件
Vue.component('router-link', Link)
Vue.component('router-view', View)
主要代码
index.js
import install from './install';
import History from './history';
class VueRouter {
constructor (options) {
this.routeMap = this.createRouteMap(options.routes || []);
this.history = new History();
this.mode = options.mode || 'hash';
this.init();
}
createRouteMap (routes) {
const routeMap = {};
for(let i = 0; i < routes.length; i ++) {
const route = routes[i];
routeMap[route.path] = route.component;
}
return routeMap;
}
init () {
if(this.mode === 'hash') {
location.hash ? '' : location.hash = '/';
document.addEventListener('DOMContentLoaded', () => {
this.history.current.path = location.hash.slice(1);
})
window.addEventListener('hashchange', () => {
this.history.current.path = location.hash.slice(1);
})
} else {
document.addEventListener('DOMContentLoaded', () => {
this.history.current.path = location.pathname;
})
window.addEventListener('popstate', () => {
this.history.current.path = location.pathname;
})
}
}
}
VueRouter.install = install;
export default VueRouter
install.js
import Link from './components/link';
import View from './components/view';
// defineReactive
export default function install (Vue) {
Vue.mixin({
beforeCreate () {
if(this.$options.router) {
this._router = this.$options.router;
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () {
return this.$root._router;
}
})
Object.defineProperty(Vue.prototype, '$route', {
get () {
return this.$root._route;
}
})
Vue.component('router-link', Link)
Vue.component('router-view', View)