学习笔记(一)简单实现 VueRouter
本系列笔记来源,开课吧前端架构课程
这个练习非常简单,只考虑 history 模式下路由和嵌套路由的实现。主要目的是理解路由的核心思想。至于更完善,更深入的内容,可以在学习完这些内容之后,阅读源码。
如果有问题,或者有更好的解决方案,欢迎大家分享和指教。
*最终效果
我们在这个练习中需要实现的效果如下:
- 考虑 hash 模式
- 实现
router-link
组件通过to
属性切换路由 - 实现
router-view
渲染路由对应的组件 - 实现 在任意组件内通过
this.$router.push(path)
的方式,跳转路由 - 实现嵌套路由
目录
在 src 下新建以下文件和文件夹
my-router
my-router.js # 封装 VueRouter 类
my-router-view.js # 封装 router-view
index.js # 配置路由
main.js 中配置如下:
import Vue from 'vue'
import App from './App.vue'
import router from './my-router/index'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
my-router/index.js
import Vue from 'vue'
import Router from './my-router'
import Home from '../views/Home.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
children: [
{
path: '/about/about-1',
name: 'about1',
component: () => import('../views/about/About-1')
}, {
path: '/about/about-2',
name: 'about2',
component: () => import('../views/about/About-2')
}
]
}
]
})
一、暂不考虑嵌套路由
不考虑嵌套路由
// 1. 插件
// 2. 两个组件 router-link router-view
/* vue 插件
* function or object
*
* 要求有一个 install 方法,将来会被 Vue.use 调用
* */
let Vue; // 保存 Vue 的 构造函数,插件中要使用
export default class VueRouter {
constructor(props) {
this.$options = props;
/* 将 current 作为响应式数据,router-view 的函数能够再次执行 */
const hash = window.location.hash.slice(1);
const initial = hash || '/'
Vue.util.defineReactive(this, 'current', initial);
// 监听 hash 的变化
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
});
}
/* *
任务一: 保证组件内 $router 能正常使用
* install 函数,为 VueRouter 的静态方法
* 此处 Vue 不使用 import 原因如下:
* 1. Vue.use() 调用插件 install 函数时,会将 Vue 的构造函数传入
* 2. 如果要单独打包该插件,会将 import 的 Vue 也打包在一起,会让插件的体积变大
* */
static install(_Vue) {
Vue = _Vue;
/* 全局混入
* 目的:延迟逻辑,保证使用 router 时,router 已经创建
* */
Vue.mixin({
/* 此钩子在每个组件创建实例时都会调用 */
beforeCreate() {
/* 因为 router 被挂载在根实例上,所以根实例会有 router */
const { router } = this.$options;
if(router) {
/*
* 将挂载在根实例上的 router 挂在 Vue 原型上,也是为了延迟执行,当该方法执行的时候,router 已经被创建了
* */
Vue.prototype.$router = router;
}
}
});
/* 任务二:注册 router-view 和 router-link 组件 */
Vue.component(
'router-link',
{
props: {
to: {
type: String,
required: true
}
},
render(h){
/* 方式一(不推荐,兼容性比较差) */
/* return <a href={'#' + this.to}>{this.$slots.default}</a> */
/* 方式二 */
return h('a', {
attrs: {
href: '#' + this.to
}
}, this.$slots.default)
}
}
);
Vue.component('router-view', {
render(h) {
/* 获取并渲染当前路由对应的组件 */
let component = null;
const route = this.$router.$options.routes.find(
route => route.path === this.$router.current
);
if (route) {
component = route.component;
}
return h(component);
}
});
}
push(path) {
window.location.hash = path;
}
}
二、嵌套路由
上面的写法会在出现嵌套路由时报错,要考虑嵌套循环,做以下改进:
- 声明一个新响应式数据:matched 数组,当其发生改变时,就会执行对应操作
- 使用 match 方法将要渲染的一级、二级…路由匹配成一个 mathced 数组
- 为每个要被 router-view 渲染的组件打上 routerView 的标记,便于遍历时确定层级
- 用 depth 标记当前被路由组件的路由层级,便于在 mathced 数组中直接定位并防止循环渲染 router-view
分析如下
假设有一个符合项目路由配置的 hash 为: /about/about-1/about-1-1
那么在配置路由时,routes 一定包含这样一个路由配置:
routes: [
...
{
path: '/about',
component: AboutComponet,
children: [
{
path: '/about/about-1',
component: About1Component,
chilren: [
{
path: '/about/about-1/about1-1',
component: About11Component
}
]
}, {
path: '/about/about-2'
component: About2Component
}
]
}
...
]
当路由改变为: /about/about-1/about-1-1 时,希望 match 函数能在 routes 数组中做匹配,匹配之后,matched 数组包含以下内容:
[
{
path: '/about',
component: AboutComponet,
children: [...]
}, {
path: '/about/about-1',
component: About1Component,
chilren: [...]
}, {
path: '/about/about-1/about1-1',
component: About11Component
}
]
当一级路由 '/about' 的 router-view 渲染时,会有一个 depth,值为 0,代表该router-view 要渲染 mathced[0].component
当二级路由 '/about/about-1' 的 router-view 渲染时,会有一个 depth,值为 1,代表该router-view 要渲染 mathced[1].component
当三级路由 '/about/about-1-1' 的 router-view 渲染时,会有一个 depth,值为 2,代表该router-view 要渲染 mathced[2].component
按照以上规律,我们可以将 router-view 的配置和渲染单独封装,并且改变 VueRouter 类的部分内容:
import RouterView from './my-router-view'
export default class VueRouter {
static install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
const { router } = this.$options;
if(router) {
Vue.prototype.$router = router;
}
}
});
Vue.component(
'router-link',
{
props: {
to: {
type: String,
required: true
}
},
render(h){
return h('a', {
attrs: {
href: '#' + this.to
}
}, this.$slots.default)
}
}
);
Vue.component('router-view', RouterView);
}
matched = [];
constructor(props) {
this.$options = props;
this.current = window.location.hash.slice(1) || '/';
Vue.util.defineReactive(this, 'matched', []);
// match 方法可以递归的遍历路由表,获得匹配关系数组
this.match();
// 监听 hash 的变化
window.addEventListener('hashchange', this.onHashChange.bind(this));
}
onHashChange() {
this.current = window.location.hash.slice(1);
this.matched = []; // 清空 matched 数组,重新匹配
this.match();
}
match(routes) {
routes = routes || this.$options.routes;
// 递归遍历
for(const route of routes) {
if (route.path === '/' && this.current === '/') {
this.matched.push(route);
return;
}
if(route.path !== '/' && this.current.indexOf(route.path) !== -1) {
this.matched.push(route);
// 如果存在 children,继续判断
if(route.children) {
this.match(route.children);
/* 递归遍历完成时,matched 数组中就会承接此次跳转的所有路由列表 */
}
return;
}
}
}
push(path) {
window.location.hash = path;
}
}
view封装
export default {
render(h) {
/* 在当前 router-view 的虚拟节点上,标记其深度 */
this.$vnode.data.routerView = true;
/* 深度值,一级路由为 0 */
let depth = 0;
/* 当前 router-view 所在的父级 vNode */
let parent = this.$parent;
while(parent) {
const vnodeData = parent.$vnode && parent.$vnode.data;
if(vnodeData && vnodeData.routerView) {
/*
每次 router-view 被 render 时,都会在其 data 上添加 routerView 属性,
所以如果这里的routerView存在,即证明该 parent 为一个 routerView,
那么当前 router-view 的深度就应该 +1
*/
depth++;
}
// 当前 parent 判断完毕,再获取上层 parent,继续判断,直至无 parent,遍历完毕
parent = parent.$parent;
}
/* 获取并渲染当前路由对应的组件 */
let component = null;
const route = this.$router.matched[depth];
if(route) {
component = route.component;
}
return h(component);
}
}