50行代码带你初步理解 Vue-Router 的原理

正文

Vue-Router 的基础用法。

    // router/index.js
    import Vue from 'vue'l
    import Router from 'vue-router';

    Vue.use(Router);

    export default new Router({...});
复制代码
// main.js
import Vue from 'vue';
import router from './router';

new Vue({
	router,
	render: (h) => h(App),
}).$mount('#app');
复制代码
<!-- App.vue -->
<div id="nav">
	<router-link to="/">Home</router-link> |
	<router-link to="/about">About</router-link>
</div>
<router-view />
复制代码

上面的代码大家都很熟悉了,不知道大家有没有这么几个疑问。

  • Vue.use() 是什么?他做了什么事情?
  • main.js 里为什么要挂载 router 呢?
  • router-linkrouter-view 这两个组件是怎么来的?

请带着上面的疑问来看下面的内容。

基础知识

Vue.use()

先来看看 Vue 官网的解释。

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。

当 install 方法被同一个插件多次调用,插件将只会被安装一次。

也就是说如果你想实现一个插件,你就必须实现插件的 install 方法。

Vue.mixin

接下来我们还要先了解一个 混入 的概念,也就是 Vue 全局的 API Vue.mixin

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

需求分析

动手前我们先进行一波需求分析,便于更好的去做接下来的事情。

  • 实现 vue 插件
  • 解析 routes 选项
  • 监控 url 变化
    • html5 history api /login
    • hash xx.html#/login
  • 实现全局组件
    • <router-link>
    • <router-view>

代码实现

实现 Vue 插件

保存 Vue 的局部变量,便于 VueRouter 使用。

至于为什么要用混入在 beforeCreate 声明周期中执行 VueRouter 的初始化是因为:

Vue.use 会执行插件的 install 方法,这时候 Vue 的实例还不存在,所以把操作放在根组件的 beforeCreate 里执行。

并且要保证 router 只在根组件挂载一次。

let Vue;

class VueRouter {}

Vue.install = function (_Vue) {
	Vue = _Vue;

	Vue.mixin({
		beforeCreate() {
			let router = this.$options.router;
			if (router) {
				Vue.prototype.$router = router;
				router.init();
			}
		},
	});
};
复制代码

解析 routes 选项

我们希望得到这样的数据,便于根据 path 来找到对应的 Component

    {
        '/': {
            path: '',
            component: Index,
            ...
        },
        '/about': {
            path: '/about',
            component: About,
            ...
        }
    }
复制代码

在做一些初始化的操作后,我们对 routes 做个简单的处理。

    class VueRouter {
        constructor(options) {
            this.$options = options;
            this.routeMap = {};
        }

        init() {
            this.createRouteMap();
            ...
        }

        createRouteMap() {
            this.$options.routes.forEach(route => {
                this.routeMap[route.path] = route;
            })
        }
    }
复制代码

监控 url 变化

这里有几点需要注意。

  • this 的指向问题。
  • 要保证 current 是响应式的。
  • 要拿到 # 之后的部分便于和组件对应。

利用了 vue 做数据响应式,至于为什么 current 要是响应式的,那就继续往下看喽。

    class VueRouter {
        constructor(options) {
            ...
            this.app = new Vue({
                data: { current: '/' }
            });
        }

        init() {
            ...
            this.bindEvents();
        }

        bindEvents() {
            window.addEventListener('hashchange', this.onHashChange.bind(this));
        }

        onHashChange() {
            this.app.current = window.location.hash.slice(1) || '/';
        }
    }
复制代码

实现全局组件

Vue.component

  • 参数

    • {string} id
    • {Function | Object} [definition]
  • 用法

    注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称

    // 注册组件,传入一个扩展过的构造器
    Vue.component(
    	'my-component',
    	Vue.extend({
    		/* ... */
    	})
    );
    
    // 注册组件,传入一个选项对象 (自动调用 Vue.extend)
    Vue.component('my-component', {
    	/* ... */
    });
    
    // 获取注册的组件 (始终返回构造器)
    var MyComponent = Vue.component('my-component');
    复制代码

router-link

router-link 这个组件在这里的实现很简单,只是封装了 a 标签。

我们希望得到的东西应该是这样的:

    <router-link to="/">首页</router-link>
    // 转换成
    <a href="/">首页</a>
复制代码

下面的代码里有这样几个知识点

  • render 里的 this 指向的是当前组件的实例。
  • render 里面实际上是可以写 JSX 的,不过最后还是会被 loader 转换成 VNode
  • href 是属性,所以要在 attrs里。
  • this.$slots 是插槽相关的知识,default可以拿到标签里的内容。
    class VueRouter {
        ...

        init() {
            ...
            this.initComponent();
        }

        initComponent() {
            Vue.component('router-link', {
                props: {
                    to: String
                },
                render(h) {
                    return h(
                        'a',
                        attrs: {
                            href: `#${ this.to }`
                        },
                        [this.$slots.default]
                    )
                }
            })
        }
    }
复制代码

补充知识 createElement

render 里的 h 函数实际上就是 createElement,它返回的就是大名鼎鼎的 VNode

// @returns {VNode}
createElement(
	// {String | Object | Function}
	// 一个 HTML 标签名、组件选项对象,或者
	// resolve 了上述任何一种的一个 async 函数。必填项。
	'div',

	// {Object}
	// 一个与模板中 attribute 对应的数据对象。可选。
	{},

	// {String | Array}
	// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
	// 也可以使用字符串来生成“文本虚拟节点”。可选。
	[
		'先写一些文字',
		createElement('h1', '一则头条'),
		createElement(MyComponent, {
			props: {
				someProp: 'foobar',
			},
		}),
	]
);
复制代码

router-view

现在来解释为什么数据要是响应式的:

只要 render 组件里面,用到了某个响应式的数据,那么这个响应式的数据发生了变化就会重新执行 render,这样页面才会刷新。

因为这里的 this 指向的是当前组件的实例,所以这里使用了 函数式组件 相关的知识请移步官方文档 函数式组件

通过第二个参数可以拿到 parent.$router 这样我们就拿到了 router 的实例。

    initComponent() {
        ...

        Vue.component('router-view', {
            functional: true,
            render(h, { parent }) {
                const router = parent.$router;
                return h(router.routeMap[router.app.current].component)
            }
        })
    }
复制代码

完整代码

let Vue;

class VueRouter {
	constructor(options) {
		this.$options = options;
		this.routeMap = {};
		this.app = new Vue({ data: { current: '/' } });
	}

	init() {
		this.bindEvents();
		this.createRouteMap();
		this.initComponent();
	}

	bindEvents() {
		window.addEventListener('hashchange', this.onHashChange.bind(this));
	}

	onHashChange() {
		this.app.current = window.location.hash.slice(1) || '/';
	}

	createRouteMap() {
		this.$options.routes.forEach((route) => {
			this.routeMap[route.path] = route;
		});
	}

	initComponent() {
		Vue.component('router-link', {
			props: {
				to: String,
			},
			render(h) {
				return h('a', { attrs: { href: `#${this.to}` } }, [
					this.$slots.default,
				]);
			},
		});

		Vue.component('router-view', {
			functional: true,
			render(h, { parent }) {
				const router = parent.$router;
				return h(router.routeMap[router.app.current].component);
			},
		});
	}
}

VueRouter.install = function (_Vue) {
	Vue = _Vue;
	Vue.mixin({
		beforeCreate() {
			let router = this.$options.router;
			if (router) {
				Vue.prototype.$router = router;
				router.init();
			}
		},
	});
};

export default VueRouter;
复制代码

猜你喜欢

转载自juejin.im/post/7085674827603771423