正文
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-link
和router-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;
复制代码