前言
路由以及是前端必须掌握的技能之一。“用上"路由很简单,"用好"路由却需要了解以route是什么,才能用得更好。本文适合1.5~3.5经验的博友们,提高自己。
再同步一下笔者的博客进度:
序号 | 博客主题 | 相关链接 |
---|---|---|
1 | 手写vue_mini源码解析 | https://juejin.im/post/6847902225138876424 |
2 | 手写react_mini源码解析 | https://juejin.im/post/6854573212018147342 |
3 | 手写webpack_mini源码解析 | https://juejin.im/post/6854573219245441038 |
4 | 手写jquery_mini源码解析 | https://juejin.im/post/6854573220356423694 |
5 | 手写vuex_mini源码解析(即本文) | https://juejin.im/post/6855295553794736142 |
6 | 手写vue_route源码解析 | https://juejin.im/post/6859565866552393741 |
7 | 手写diff算法源码解析 | 预计8月 |
8 | 手写promise源码解析 | 预计8月 |
9 | 手写原生js源码解析(手动实现常见api) | 预计8月 |
10 | 手写react_redux,fiberd源码解析等 | 预计9月 |
11 | 手写koa2_mini | 预计9月,前端优先 |
route的概念
什么是路由。可以简单的理解成一个url的地址,来展示不同的内容或者页面。
很久以前,在单页面时代还没开启之前,我们习惯用"页面链接"来描述他,他后来有个叫法叫"后端路由"。后续单页面架构的出现,也就是前端路由。
后端路由,很好理解,就是找到对应的文件地址,讲文件经过一层物理转换,输出到前端。每一次获取地址,都需要连接服务器重新加载(暂不考虑缓存问题)
前端路由,相对后端路由复杂一点。分为hash模式与history模式
hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件,再匹配到对应的路由规则,再渲染到页面。
而history模式是history.pushState API 来完成 URL 跳转而无须重新加载页面。
看了不少route的文章。其中觉得yck这里的描述最通俗易懂,这里借鉴一下他的图片(结尾会给链接),来帮助大家理解route的原理。
hash模式:
history模式:
我们理解完这两个图,对路由的一个基本的了就有了。
手写html-hash-route
写vue-route的mini版之前,我们来写一个简单html帮忙理解原理。我们新建一个zRouter.js,新建一个类。我们分析一下我们要做什么:
- 1.从上图分析,我们是要监听原生api的load,跟hashchange事件的,触发对应的地址。我们需要在初始化时,就对他们完成监听
- 2.监听后,我们需要匹配到对应的规则。我们可以拿到原来的路由地址,再去匹配对应的路由地址,触发他的回调函数。
我们可以推出一个简版本的源码:
zRouter.js:
class zRouter{
constructor(){
this.routes = {};
this.init();
}
refresh(){
let path = location.hash;
this.routes[ path]();
}
init(){
window.addEventListener( 'load', this.refresh.bind( this ), false );
window.addEventListener( 'hashchange', this.refresh.bind( this ), false );
}
route( path, cb ){
this.routes["#" + path] = cb || function(){};
}
}
然后我们的页面,再对应的路由,跳转对应的路由地址。一个简单hash处理就出来了。 zRouter.html:
<div id="app" class="body">
<div class="b_header">
<span>zRouter</span>
</div>
<div class="b_content">
<div>
<a href="#/" class="c_tag" >首页</a>
<a href="#/product" class="c_tag" >商品页面</a>
<a href="#/order" class="c_tag" >订单页面</a>
</div>
<div class="c_inner" id="content" >
这是首页
</div>
</div>
</div>
<script>
window.onload = function () {
var cont = document.querySelector('#content');
var router = new zRouter();
router.route( "/", function(){
cont.innerHTML = "这是首页";
});
router.route( "/product", function(){
cont.innerHTML = "这是商品页面";
});
router.route( "/order", function(){
cont.innerHTML = "这是订单页面";
});
}
</script>
手写vue-route
写完最简的html-route。我们继续vue-route,其实原理是一样的,只是vue框架中,考虑的事情相对多一点。比如,如何混入vue实例,路由如何完成实施监听等。我们逐步完成他。
1)基本架子搭建
npm install --global vue-cli vue init webpack my-vue-router
然后自己写两个组件,以及配置好路由跑起来。
2)新建vueRouter实例
修改import Router from 'vue-router’为import Router from ‘…/vue-router/index.js’
我们新建抒写vue-router/index.js来替换vue-router。那么源码vue-router中到底有什么?
- 首先Vue.use(Router)。说明源码中暴露了自己的install方法。
- 根据new Router({routes: []});,说明源码中,暴露了一个对象,且接受了routes参数
- 接收到routes要做什么?保存对应的页面内容,方便下次触发路由时,再跳转到对应的内容。
我们可以简单的推出…/vue-router/index.js:
class VueRouter {
constructor(options) {
this.routes = options.routes || [];
this.routesMap = this.createMap(this.routes);
}
createMap(routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre;
}, {})
}
}
VueRouter.install = function () {}
export default VueRouter;
3) 存储当前路由值
上述的代码,已经存好hash值跟路由组件的关系(假设是hash模式),那么我们肯定需要,先拿到路由hash值, 我们新建一个currentObj对象来存储他。而且需要监听他的变化。我们写多一个initListener方法:
class VueRouter {
constructor(options) {
...//省略
this.currentObj = {
path: null
};
this.initListener();
}
initListener() {
location.hash ? '' : location.hash = "/";
window.addEventListener("load", () => {
this.currentObj.path = location.hash.slice(1)
})
window.addEventListener("hashchange", () => {
this.currentObj.path = location.hash.slice(1)
})
}
}
4) 获取VueRouter实例对象
我们的目标是啥,router-view根据hash的值,展示出正确的内容。
首先router-view是个啥?其实就是个组件。官方已经帮我写好了component。那我们只需要找到对应的内容,已经对应的路径即可。我们上边已经将路由关系存在的routesMap。即是此时,拿到当前路由的地址,即可触发
来看代码:
const routerView = {
name: 'RouterView',
render(h) {
const currentPath = null; // "当前地址路径",即上边的 currentObj.path
const routeMap = null; // VueRouter实例对象的.routesMap;
return h(routeMap[currentPath])
}
}
这样,我们只要拿到currentPath跟routeMap,我们的组件即可渲染。现在的关键点变成,如何拿到VueRouter对象,以及currentPath存储的对象。
这的确是一个开始让我疑惑的地方,我们看一下官方源码如何获取的(https://github.com/vuejs/vue-router/blob/dev/src/install.js#L20):
分析一下他的获取思路,利用Vue.mixin获取对象模式,每个组件都经过Vue.mixin,如果是根组件,存储在_routerRoot, 这就是我们的VueRouter对象。(因为你的Vue.use(vueRouter)是在根组件混入的没错吧? 那vueRouter存储的信息的对象,即是根组件对象)
那么非根组件怎么获取vueRouter对象?根组件居然拿到了,非根组件,那就一层一层往上拿,拿到根为止。
此时,我们来修改install的方法:
VueRouter.install = function (v) {
const Vue = v;
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._routerRoot = this; //把当前实例挂载到_routerRoot上
this._router = this.$options.router;//顺便存储一下router的值
} else { //如果是子组件
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
});
}
此时_routerRoot就是我们的VueRouter实例对象。
5) 渲染router-view
上述说到,拿到vueRouter对象,即可渲染router-view。我们再来修改一下渲染方法。
const routerView = {
name: 'RouterView',
render(h) {
const currentPath = this._self._routerRoot._router.currentObj.path;
const routeMap = this._self._routerRoot._router.routesMap;
return h(routeMap[currentPath])
}
}
此时,应该拿到路径,渲染出组件。调用组件,初步路由完成!
v.component('RouterView', routerView);
但是,页面却还是没有效果。思路是对的,来调试原因。最后发现currentPath还是underfine。此时如果你写死currentPath的话,组件就正确渲染了。
currentObj.path的获取方法肯定是没问题的,但是读取出来的结果切是underfine。
那只有一种可能,currentObj.path还没有拿到的时候,我们的组件就已经开始渲染了。这首先想到的方法的解决,当然是双向绑定,实时触发数据。
双向绑定?方案1可以将 把我们的: currentObj对象 = 一个new Vue(); 这就使我们的currentObj实时的。
后来看到博友cobish就是这么干的。
但是我们看源码:
源码是通过 Vue.util.defineReactive(this, ‘_route’, this._router.history.current)来实现双线绑定。
defineReactive不知道是什么的朋友看看代码:
我们跟着源码依葫芦画瓢,添加代码:
Vue.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定
即将我们的this._router.currentObj对象赋值到_route里边。此时,刷新组件,已经成功渲染。代码如下:
VueRouter.install = function (v) {
const Vue = v;
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._routerRoot = this; //把当前实例挂载到_root上
this._router = this.$options.router;
Vue.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定
} else { //如果是子组件
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
});
v.component('RouterView', routerView);
}
6) 添加router-link
写完router-view,我们继续router-link的抒写,其实就是一个a标签,再把里边的元素放到a标签中即可,直接撸代码:
const routerLink = {
name: 'RouterLink',
props: {
to: String
},
render(h) {
const mode = this._self._routerRoot._router.mode;
return h('a', {
attrs: {
href: (mode === "hash" ? "#" : "") + this.to
}
}, this.$slots.default)
}
}
然后全局混入即可:
v.component('RouterLink', routerLink);
7)暴露对象
此时,整个路由已经完成了。可能会问,vue实例中,拿不到 $ router对象。那么我们在初始化router,顺便暴露$router即可。
我们先看看官方怎么写:
相信看完你就明白,官方用了数据劫持,当vue实例获取$router或者$route,直接通过Object.defineProperty获取vue-router中设置对象的值:
Vue.mixin({
beforeCreate() {
...//省略
Object.defineProperty(this, '$router', {
get() {
return this._routerRoot._router;
}
});
Object.defineProperty(this, '$route', {
get() {
return this._routerRoot._router.currentObj.path;
}
})
}
})
8)支持history模式
以上代码,全部是跟着hash模式走通。需要支持history,其实很简单,当history模式时,监听hashChange改成监听popstate即可。
此外,拿路径的时候注意#跟/怎么处理即可。直接看下方完整mini源码。
完整源码
//渲染routerView组件
const routerView = {
name: 'RouterView',
render(h) {
const currentPath = this._self._routerRoot._router.currentObj.path;
const routeMap = this._self._routerRoot._router.routesMap;
return h(routeMap[currentPath])
}
}
//渲染routerLink组件
const routerLink = {
name: 'RouterLink',
props: {
to: String
},
render(h) {
const isHashMode = this._self._routerRoot._router.isHashMode;
return h('a', {
attrs: {
href: ( isHashMode? "#" : "") + this.to
}
}, this.$slots.default)
}
}
class VueRouter {
constructor(options) {
this.isHashMode = !( options.mode == "history" );//只要不是history模式,其他均为hash模式
this.routes = options.routes || [];//存储路由地址
this.routesMap = this.getRoutesMap(this.routes);
this.currentObj = { path: '' };
this.initListener()
}
initListener() {
//hash模式以hash为准,history以pathname为准,且为空时默认为/
const initPath = ( this.isHashMode ? location.hash.slice(1) : location.pathname ) || "/";
const listName = this.isHashMode ? "hashchange": "popstate";//监听的对象名称,hash模式监听hashchange,history监听popstate
window.addEventListener("load", () => {
this.currentObj.path = initPath;
})
window.addEventListener( listName, () => {
this.currentObj.path = ( this.isHashMode ? location.hash.slice(1) : location.pathname ) || "/";;
})
}
getRoutesMap(routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre;
}, {})
}
}
VueRouter.install = function (v) {
v.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根组件
this._routerRoot = this; //把当前实例挂载到_root上
this._router = this.$options.router;
v.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定
} else { //如果是子组件
this._routerRoot = this.$parent && this.$parent._routerRoot
}
Object.defineProperty(this, '$router', {
get() {
return this._routerRoot._router;
}
});
Object.defineProperty(this, '$route', {
get() {
return this._routerRoot._route;
}
})
}
});
v.component('RouterLink', routerLink);
v.component('RouterView', routerView);
}
export default VueRouter;
可下载demo运行。
文件结尾
源码地址
https://github.com/zhuangweizhan/codeShare
参考及感谢
本文为原创。思维上有几个文章借鉴的地方,感谢他们:
- cobish(使用Vue实现双向绑定版本): https://juejin.im/post/6844903629804011533
- 阳光是sunny:https://juejin.im/post/6854573222231605256
- yck:https://juejin.im/user/712139233840407
- 官方源码:https://github.com/vuejs/vue-router/blob/dev/src/install.js
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 逐步前行 原文链接:https://juejin.im/post/6860107861134540814