做为国内使用最多的库之一的Vue,你们都在学习其源码,作到知其然而知其因此然;
由于咱们不只要懂得如何使用它,还得了解其原理或者怎么模拟实现;
可是光看可不行,我们还得动手,一遍不行就两遍,两遍不行就三遍...
谁也不是天才,一用就会,一问就懵,那今天从默写实现Vue-router开始,删繁就简,手写vue-router核心源码。html
路由经历了从多页面应用到单页面应用的发展,最开始的网页是多页面的,一个完整的网页应用有多个完整的html构成,经过a标签对应到不一样url,服务器端来根据URL的不一样返回不一样的页面,那些页面在服务端都是实实在在存在的。前端
我毕业参加工做是2014年,这个时候前端能作的事情颇有限,先后端还不能彻底分离,依赖于 Ajax ,使得前端可以胜任更多更复杂的事,先后端的职责愈来愈清晰,在业务不断发展的过程当中,因为前端项目变得愈来愈复杂,因此咱们要考虑拆分出前端应用部分,使之成为一个能独立开发、运行的应用,而非依赖于后端渲染出HTML的多页面应用。单页面应用就应运而生了。vue
单页面应用就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会由于用户的操做而进行页面的从新加载或跳转。 取而代之的是利用 JS 动态的变换 HTML 的内容,从而来模拟多个视图间跳转。nginx
说到底路由是根据不一样的URL来展现不一样的内容或页面,而路由的本质 就是创建起url和页面之间的映射关系。vue-router
大部分单页面应用的架构都是为响应式Dom铺路,咱们都知道常规的Dom操做开销太大,尤为是在各个板块有大量数据交互和用户IO交互场景的时候。单页面应用就是在一个Public的Html架子上,进行虚拟Dom的展现和响应式数据模型的运用,让一切看起来都在观察者的监视下运行着,借助路由展现着不一样的模板内容。编程
Hash 模式使用了浏览器 URL 后缀中的#xxx部分来实现前端路由。默认状况下,URL后缀中的#xxx hash 部分是用来作网页的锚点功能的,如今前端路由看上了这个点,并对其加以利用。 好比这个 URL:http://www.abc.com/#/hello,hash 的值为 #/hello。 为何会看上浏览器URL后缀中的 hash 部分呢?缘由也简单:后端
浏览器URL后缀中的 hash 改变了,不会触发请求,对服务器彻底没有影响。它的改变不会从新加载浏览器页面。 更关键的一点是,由于hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退均可以用了,页面的状态与浏览器的URL就发生了挂钩。api
大白话: hash模式不会真实跳转,留心观察浏览器加载状态并没有变化,监听背后的原理是onhashchange事件而已。浏览器
History 路由模式服务器
随着 HTML5 中 history api 的到来,前端路由开始进化了。hashchange 只能改变 # 后面的代码片断,history api (pushState、replaceState、go、back、forward) 则给了前端彻底的自由。
在 HTML5 以前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面的跳转,正如早期咱们编写路由时,总会用到以下api控制页面跳转。
history.go(-1); // 后退一页history.go(2); // 前进两页history.forward(); // 前进一页history.back(); // 后退一页//Html5 新增history.pushState(); // 添加新的状态到历史状态栈history.replaceState(); // 用新的状态代替当前状态history.state // 返回当前状态对象复制代码
大白话:一般在单页面使用中是须要后台配置nginx跳转支持的,试想当用户在浏览器直接访问oursite.com/user/id,默认会由服务器检索这个文件,检索不到就会返回 404;还有另外一个使用场景:***的服务端渲染项目中,资料挺多,具体你们能够查一下,当前最火的一些衍生生态库next和nuxt都很好的诠释了这个特色,可是咱们今天不展开history模式,就默写最简单的核心代码来实现路由。
VueRouter 是做为插件的形式引入到 Vue 系统内部的。而将具体的 router 信息嵌入到每一个 Vue 实例中,则是做为 Vue 的构造函数参数传入。
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>今天进步了吗?</title><script src="https://unpkg.com/vue/dist/vue.js"></script><!-- <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> --><script src="./vue-router-recode.js"></script> </head> <body><div id="app"> <h1>手写<strong>Vue-Router</strong></h1> <p><!-- 使用 router-link 组件来导航. --><!-- 经过传入 `to` 属性指定连接. --><!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --><router-link to="/foo">Go to Foo</router-link><router-link to="/bar">Go to Bar</router-link> </p> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view></div><script> // 0. 若是使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter) // 1. 定义 (路由) 组件。 // 能够从其余文件 import 进来 const Foo = { template: '<div>foo</div>' }; const Bar = { template: '<div>bar</div>' }; // 2. 定义路由 // 每一个路由应该映射一个组件。 其中"component" 能够是 // 经过 Vue.extend() 建立的组件构造器, // 或者,只是一个组件配置对象。 // 咱们晚点再讨论嵌套路由。 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar }, ]; // 3. 建立 router 实例,而后传 `routes` 配置 // 你还能够传别的配置参数, 不过先这么简单着吧。 const router = new VueRouter({ routes, // (缩写) 至关于 routes: routes }); // 4. 建立和挂载根实例。 // 记得要经过 router 配置参数注入路由, // 从而让整个应用都有路由功能 const vm = new Vue({ router, }).$mount('#app'); // 如今,应用已经启动了!</script> </body></html>复制代码
直接使用router-link和router-view这两个组件。它们是随着 Vue Router 一块儿引入的,做为全局组件使用。有了以上的想法,咱们构建了这样的简单页面,接下来咱们就手写一个vue-router-recode.js用于替换vue-router.js的职能。
就从业这些年的经验来说,通常开发功能或者本身造轮子的时候,合格的开发者都有着本身的一套体系,像什么Markdown记录笔记啊,代码良好的md说明文件啊,还有些画图的使用Xmind或流程图ProcessOn之类的,均可以,不局限方法,可是得有该有的习惯,不然哪天老板让写个工具,写完给别人使用还得手把手传给别人,那可太痛苦了。
基本原理图:
完整实现思路图:
对,干就完了!
完整浏览了以上思路的,都不是难事儿,手敲速度慢的,建议多敲多写。Open with Live Server运行完美,直接上代码:
let _Vue;class VueRouter { constructor({ routes }) {let routerMap = {}; routes.forEach((route) => { let path = route.path; if (!routerMap[path]) { routerMap[path] = route; } });this.routerMap = routerMap;console.log(this.routerMap);//TODOthis.current = { path: '', component: {template: `<div>default</div> `, }, };this.linstener(); } linstener() {window.addEventListener('load', () => { debugger; console.log('load'); let hash = window.location.hash; if (!hash) {window.location.hash = '/'; } let route = this.search(hash.slice(1)); if (route) {this.current.path = route.path;this.current.component = route.component; } });window.addEventListener('hashchange', () => { debugger; console.log('hashchange'); let hash = window.location.hash; let route = this.search(hash.slice(1)); if (route) {this.current.path = route.path;this.current.component = route.component; } }); } search(path) {if (this.routerMap[path]) { return this.routerMap[path]; } } } VueRouter.install = function (Vue, options) { _Vue = Vue; _Vue.mixin({beforeCreate() { let vm = this; // console.log(vm); if (vm.$options.router) { vm._routerRoot = this; vm._router = vm.$options.router;//定义响应式数据_Vue.util.defineReactive(vm, '_route', vm._router.current); } else { vm._routerRoot = vm.$parent && vm.$parent._routerRoot; } }, }); _Vue.component('router-link', {props: { to: String, },render(c) { // h => createElement return c('a', { attrs: { href: '#' + this.to } }, this.$slots.default); }, }); _Vue.component('router-view', {render(c) { debugger; let component = this._routerRoot._route.component; return c(component); }, }); };if (typeof Vue !== 'undefined') { Vue.use(VueRouter); }复制代码
本文主要为理解vueRouter源码提供一个最基础的框架和思路,如须要对vue生态有着较为深刻的理解,如vue的插件机制,vue的双向绑定原理,后端路由和前端路由,hash模式相关api以及history模式相关api,更多相关的 Vue-Router 的细节,能够参考其官网。但愿本文对你有用。
几点关键之处:
- 自定义插件内部必须实现一个 install 方法,传入参数是Vue的构造函数。
- 使用了一个新的Vue 实例,将 URL 的 hash 变量进行数据响应化处理。
- util.defineReactive属于Vue源码中提供的自有方法,就是用于建立响应式数据。
- 关于渲染函数 render 的参数 c,它其实是 createElement 函数。