文章目录
一、location对象
location对象含有很多的属性,其中我们此次使用到的是 hash,接下来我们需要熟悉一下hash的用法。
<a href="#/index">首页</a>
<a href="#/app">app</a>
<div id="app"></div>
<script type="text/javascript">
window.addEventListener('load', ()=>{
// 初始化url的hash模式,得到url地址,更改视图数据
let hash = location.hash;
hash?'':location.hash='/';
app.innerHTML = hash.slice(1);
})
window.addEventListener('hashchange',()=>{
// 获取到当前的值,改变视图
let hash = location.hash.slice(1);
app.innerHTML = hash;
})
</script>
运行结果:
二、history对象
1. 常见方法
- go(n) 从浏览器的记录中跳转跳转n页
- back() 从浏览器的记录中向后跳转
- forword() 从浏览器的记录中向前跳转
- history.pushState(data, title, url);
- 参数1,代表自定义数据
- 参数2,代表网页的title,可忽略
- 参数3,代表新追加的历史记录
注意:
- history必须同源,不能跨域;
- history需要后端对所有的路由情况进行处理,否则就会404。
三、功能分析
1. 组件
我们需要注册两个组件,分别是vue-router和vue-link。两者之间通过VueRouter做调度,当vue-link或者其他方式改变了VueRouter的history属性(记录当前路由信息),那么就会触发Vue的监听,然后通知router-view来更新当前路由地址所对应的组件视图。
2. 接口
在每一个组件上都有
$route路由信息,
$router路由方法
四、开始手写vue-router
1. 构造函数
constructor(options) {
this.mode = options.mode || 'hash';
this.routes = (function () {
let res = {};
options.routes.forEach(val=>{
res[val.path] = val.component;
})
return res;
})();
this.history = {
current: null
};
// 初始化VueRouter
this.init();
}
2. 初始化操作
// 初始化函数
init() {
if (this.mode === 'hash') {
// 初始化地址
// console.log(location.hash);
location.hash ? '' : location.hash = '/';
// 当页面加载完成后将当前的地址保存
window.addEventListener('load', () => {
this.history.current = location.pathname;
console.log(1,location.pathname);
})
// 监听页面的跳转
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1);
console.log(2,location.hash);
})
} else {
// 如果为 / 说明第一次使用,否则用户改变了url,此时不做任何操作
location.pathname === '/' ? location.pathname = '/' : '';
window.addEventListener('load', () => {
this.history.current = location.pathname;
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname;
})
}
}
分析:
初始化的操作并不是很难,我们在一开始便进行了练习。
3. vue-router安装方法
我们在使用vue-router的时候都需要使用一个Vue.use(VueRouter)方法将vue-router注册到Vue中,use方法要求我们必须提供一个install方法。具体功能如下:
① 得到唯一实例
所谓的得到唯一实例是指。每个组件的this.$router和this.$route的得到的是同一个实例,具体的实现思路是递归,但是这个递归却决于Vue的渲染机制:父1->子1->孙1->父2->子2->孙2,所以代码如下:
// 确保每个组件得到的是同一个router对象
vue.mixin({
beforeCreate() {
// 将router挂载到根组件,因此必须确保接下来所有的this都为跟组件,即用this._root保存
if (this.$options && this.$options.router) {
this._root = this;
this._router = this.$options.router;
vue.util.defineReactive(this, 'xxx', this._root._router);
} else {
// 递归的思想,如果子组件没有$router属性,那么就去父组件去找,不断循环。
this._root = this.$parent._root;
}
// 拦截$route属性
Object.defineProperty(this, '$route', {
get() {
return {
current: this._root._router.history.current
}
}
})
// 拦截$router属性
Object.defineProperty(this, '$router', {
get() {
return this._root._router;
}
})
},
})
分析:
- 我们使用到了Vue中的混入,混入的意思是我们可以提供一个自己的钩子函数,这些钩子函数会在我们组件的钩子执行函数之前执行。混入分为两种模式:
- Vue。mixin({}),这种方式是全局混入,即所有的组件都会有混入的方法。
- mixins: [‘mixin1’], 这是组件的私有混入。
- 在上述代码中我们使用了vue的核心方法defineReactive(this, xxx, this._root._router)这个方法会将我们传入的this._root._router进行深度的遍历,将其转换为响应式的数据,目的是为了监听history的变换,从而通知router-view的视图更新。
③ 组件的注册
// 注册router-view组件
vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current;
let component = this._self._root._router.routes[current];
return h(component);
}
});
// 注册router-link组件
vue.component('router-link', {
props: ['to'],
render(h) {
return <a href={ this._root._router.mode === 'hash' ? `#${this.to}` : this.to }>{this.$slots.default}</a>;
}
});
分析:
- 我们使用了jsx的语法,读者可以自行学习
五、总结
以上基本完成了一个简易的vue-router,当然还存在很多的缺陷,希望读者见谅,在手写viue-router的过程中我也得到了一些心得体会,和读者进行分享。
- vue-router的核心功能并不是很难,主要使用了location和history,因此我们需要充分了解这两个api
- 在我以前对递归有一种思维定势,认为递归就是函数调用自己,但是在实现每个组件得到唯一vuerouter实例的过程中打破了我对递归的认识,递归其实也可以结合 深度优先遍历 来达到递归的作用。
- vue的响应式原理无处不再,这也是vue的核心之一,读者需要对响应式的原理做到深入的理解。