2021-01-01 22:47:46 配套了网站和git仓库
2020-05-04 15:41:02 补充了源码的解释
2019-12-11 21:37:07 完成初稿
按:
第一次尝试写本文,还是去年年底(2019-12-11 21:37:07),后来入职新公司忙的要死,也就鸽了。今天重新梳理,争取弄得清楚一些:带你读 Vue-Router源码。
vue-router 是什么?
- 是官方组件
- 有 history 和 hash 两种模式
- 注册了
this.$router和this.$route - 注册了
router-link和router-view两个全局组件 - 其他特性
是官方组件
是 vue 官方的 router 插件,我们通过使用这个插件来完成 vue 项目中路由的导航。
既然提到插件,就不得不说 vue 的插件系统, Vue.use的技术细节看这里 《Vue 从基础到高级的概念》 (未来统一整合吧)。
因此,可以料想,router 插件的代码结构大致如下:
// step 1class VueRouter {}VueRouter.install = function() {};
两种模式
HTML5 history mode or hash mode, with auto-fallback in IE9
显然我们需要监听路由变化,这是一个主动通知也就是响应式的过程。
注册两个方法
我们可以在全局通过 this.$route 和 this.$router 来控制 router 的逻辑细节。
注册两个组件
route-link导航route-view视图
使用流程
Vue-Router 的核心使用流程:
// 1 导入 main.jsimport VueRouter from 'vue-router'Vue.use(VueRouter) // 说明是Vue插件,自然要想到install方法// 2 创建router实例 规则。router.jsconst routes = [{ path: '/foo', component: Foo },{ path: '/bar', component: Bar }] //export// 3 添加到根实例 main.jsimport routes from 'router.js'const router = new VueRouter({routes // (缩写)相当于 routes: routes})new Vue({router}).$mount('#app')// 4 两个全局组件 App.vue// <router-view /> // 路由视图// <router-link to="/">去首页</router-link> // 路由导航
源码解析
如果要实现这种使用方式,需要完成下面的需求:
- 是一个 Vue插件 — Vue插件编写, install
- 书写规则,解析规则 router.js
- 随 根实例一起创建
- 注册全局组件
- 监听路由变化,展示内容
实现 vue-router 插件
刚才已经提到,我们的结构如下:
// step 1class VueRouter{}VueRouter.install = function(){}
困难点:router install时候,还没有 new Vue,也就是拿不到实例。该如何处理?
针对这个方法,官方妥善使用了 mixin混入,在 Vue的 beforeCreate 声明周期中执行相关逻辑,也需要进一步判断是否是根实例。
官方是这么做的,点开查看 官方router/install.js 。当install方法执行的时候。忽略一些代码,能看到 mixin.
// 使用混入来做router挂载这件事情Vue.mixin({beforeCreate() {// 只有根实例才有router选项 此时this是vue实例if (this.$options.router) {Vue.prototype.$router = this.$options.router;}}});
接下来就可以准备 注册全局组件。
// step2let Vue; // 这里定义 Vue 方便后续使用class vueRouter{} // 这里先不关注// 插件需要实现install静态方法// 接收一个参数,Vue构造函数,主要用于数据响应式VueRouter.install = function (_Vue) {// 保存Vue构造函数在VueRouter中使用Vue = _Vue// 任务1:使用混入来做router挂载这件事情Vue.mixin({beforeCreate() {// 只有根实例才有router选项if (this.$options.router) {Vue.prototype.$router = this.$options.router}}})// 任务2:实现两个全局组件// 这部分代码放到下面Vue.component('router-link',{template: "<a>aaa</a>"})Vue.component('router-view',{template: "<a>aaa</a>"})}
You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
不能要求用户一定使用 runtime 的vue。
注册全局组件
文档中也提到了,插件中是如何注册组件的
- Import 组件导入
- Vue.component() 注册全局组件
router-link
先只考虑 hash模式。<router-link to="/">aaa</router-link> 这个组件有 props slot,注意插件不适用template,vue版本不一定存在编译器,所以唯一选项是使用render函数。因为 href 是 attribute,所以这样编写如下组件:
// router-link: 生成一个a标签,在url后面添加#// <a href="#/about">aaaa</a>Vue.component('router-link', {props: {to: {type: String,required: true}},render(h) {// h(tag, props, children)return h('a',{ attrs: { href: '#' + this.to } },this.$slots.default)// 使用jsx// return <a href={'#'+this.to}>{this.$slots.default}</a>}})
这样就实现了渲染为html:<a href='#/'>aaa</a>
此时,点击页面已经可以正常跳转了,虽然页面不是响应式的。
route-view
暂时先不实现。
Vue.component('router-view', {render(h) {return h('div','route-view ...')}})
完善逻辑
接下来实现点击a连接,变化 hash,也就是监听hashchange事件。
我们补充下面代码
class VueRouter {constructor(options) {this.$options = options;// 当前hashthis.current = window.location.hash.slice(1) || "/";// 监听变化window.addEventListener("hashchange", () => {this.current = window.location.hash.slice(1);console.log("this.current", this.current);});}}
和这个代码
Vue.component("router-view", {render(h) {// 实例中获取当前hashconsole.log("render", this.$router.current);return h("div", "route-view ...");}});
观察控制台效果。可以看到 render只触发了一次。原因是的 this.current 不是响应式的数据。
要解决很简单,添加一行代码即可:注意 line8
class VueRouter {constructor(options) {this.$options = options;// 当前hashconst currentHash = window.location.hash.slice(1) || "/";this.current = currentHash;console.log("this.current", this.current);// 定义数据响应式// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerenderVue.util.defineReactive(this, "current", currentHash);// 监听变化window.addEventListener("hashchange", () => {this.current = window.location.hash.slice(1);// console.log("this.current", this.current);});}}
接下来就是渲染组件了,找到组件交给render即可
Vue.component("router-view", {render(h) {// 当前this是组件实例// console.log("this", this.$router);// 实例中获取当前hashconsole.log("render", this.$router, this.$router.current);// 从路由表里匹配当前的路由const component =this.$router.$options.routes.find(route => route.path === this.$router.current)?.component ?? "div";return h(component);}});
最终,页面导出类
export default VueRouter
这样就 my-vue-router.js 就写完了,可以看我 code/01 文件夹里的内容了。
当然很粗糙,可以进一步优化,比如路由表缓存,而不是每次都循环遍历。line10-lin13
class VueRouter {constructor(options) {// ...// 一次性缓存路由映射表this.routeMap = {};this.$options.routes.forEach(route => {this.routeMap[route.path] = route;});// ...}}
这里做修改
Vue.component("router-view", {render(h) {const { current, routeMap } = this.$router;const component = routeMap[current]?.component ?? "div";return h(component);}});
至此,需要去搞定嵌套路由的逻辑了。
嵌套路由
嵌套路由,是 route-view 的嵌套。在官方源码中的 src/component/view.js 可以看到解决方式。
给路由增加层级 depth。
- 给 view组件挂载
viewtag 表名是 RouteView - 控制matched 数组
虽然是函数式组件,但不重要,line16 设定routeview=true 做标记
Lin27 表示深度 depth ,看祖辈有没有 routeview,如果有 虚拟dom的data里有,就+1 表示深度
在 line58 匹配,匹配的是数组,是个数组,打印下看看。
line59 component
lin115 渲染时候,渲染 component
源码怎么写的?
官方git仓库 Vue-router
