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 1
class 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.js
import VueRouter from 'vue-router'
Vue.use(VueRouter) // 说明是Vue插件,自然要想到install方法
// 2 创建router实例 规则。router.js
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
] //export
// 3 添加到根实例 main.js
import 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 1
class 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;
}
}
});
接下来就可以准备 注册全局组件。
// step2
let 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;
// 当前hash
this.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) {
// 实例中获取当前hash
console.log("render", this.$router.current);
return h("div", "route-view ...");
}
});
观察控制台效果。可以看到 render只触发了一次。原因是的 this.current 不是响应式的数据。
要解决很简单,添加一行代码即可:注意 line8
class VueRouter {
constructor(options) {
this.$options = options;
// 当前hash
const currentHash = window.location.hash.slice(1) || "/";
this.current = currentHash;
console.log("this.current", this.current);
// 定义数据响应式
// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
Vue.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);
// 实例中获取当前hash
console.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组件挂载
view
tag 表名是 RouteView - 控制matched 数组
虽然是函数式组件,但不重要,line16 设定routeview=true 做标记
Lin27 表示深度 depth ,看祖辈有没有 routeview,如果有 虚拟dom的data里有,就+1 表示深度
在 line58 匹配,匹配的是数组,是个数组,打印下看看。
line59 component
lin115 渲染时候,渲染 component
源码怎么写的?
官方git仓库 Vue-router