一、项目准备
创建vue项目
vue create projectName
进入项目
cd projectName
安装vue-router
vue add router
手写vue-router:在router文件夹中创建jvue-router.js文件。
将index.js文件中引入VueRouter地址改为jvue-router.js的地址
// import VueRouter from "vue-router";
import VueRouter from "./jvue-router";
二、手写vue-router源码
1在router/index.js中使用VueRouter时是new VueRouter(),所以要创建一个VueRouter类并暴露出来,同时vue插件都要有一个install进行初始化,install中的参数为vue,保存起来以便后面使用。
let Vue;
class VueRouter {
constructor() {}
}
VueRouter.install = function(_vue) {
Vue = _vue;
}
export default VueRouter;
2.在组件中,我们是直接使用<router-link>和<router-view>两个组件,所以install方法中注册这两个组件
let Vue;
class VueRouter {
constructor() {}
}
VueRouter.install = function(_vue) {
Vue = _vue;
// 注册router-view和router-link
Vue.component('router-view', {
render(h) {
return h('div', 'router-view')
}
})
Vue.component('router-link', {
render(h) {
return h('a', 'router-link')
}
})
}
export default VueRouter;
3.先分析router-link组件:
使用时我们会传入一个to属性表示链接路径:
<router-link to="/about">About组件</router-link>
编译后是一个a标签和href属性,href属性的值为上面to属性的值:
<a href="/about">About组件</a>
所以我们注册router-link组件时应先用props获取to属性值,用$slots获取插值文本
let Vue;
class VueRouter {
constructor() {}
}
VueRouter.install = function(_vue) {
Vue = _vue;
// 注册router-view
Vue.component('router-view', {
render(h) {
return h('div', 'router-view')
}
})
// 注册router-link
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
}
})
}
export default VueRouter;
4.分析router-view组件:
获取当前hash并将其设为响应式数据,监听url变化。由于是响应式数据,所以当hash变化时,相应的组件会重新渲染。
获取路由映射表,根据当前hash在映射表中找到对应的组件,并渲染。
由于组件中使用router时是this.$router形式,所以要将router挂载到Vue原型链上。此时可以在Vue组件中获取到router和routes。
因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载。
路由映射表是在创建路由实例时传入(./router/index.js):
const routes = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue")
}
];
const router = new VueRouter({
routes
});
对应代码:
// 设置vue以便后面使用
let Vue;
class VueRouter {
constructor(options) {
this.options = options;
// 初始化current,设置current为响应式数据,当current改变时重新渲染页面
Vue.util.defineReactive(
this,
"current",
window.location.hash.slice(1) || "/"
);
console.log(Vue.util);
// 监听url变化,重置current
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
console.log(this.current);
});
}
}
// vue插件都要有install方法:注册router-link和router-view两个组件 使得我们在组件中使用时可以<router-view>,将router挂载到Vue原型链上,使得我们在组件中使用时可以this.$router
VueRouter.install = function(_vue) {
Vue = _vue;
console.log(_vue);
// 将router挂载到Vue原型链上
// 因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载
Vue.mixin({
beforeCreate() {
// 判断当前组件是否为根组件,只有根组件有$router
// 因为根组件的vue实例创建时传入了vueRouter实例,vue组件中获取传入的参数可使用this.$options,所以获取传入的vueRouter实例可以用this.$options.router
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
});
// 注册router-view
// 根据当前hash找到对应component
// hash:window.location.hash.slice(1)
// 获取路由映射表:VueRouter在创建实例时传入了路由映射表,所以从VueRouter初始化中获取
Vue.component("router-view", {
render(h) {
const { current, options } = this.$router;
let component = null;
const route = options.routes.find(route => route.path === current);
if (route) {
component = route.component;
}
return h(component);
}
});
// 注册router-link
Vue.component("router-link", {
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
}
});
};
export default VueRouter;
5.嵌套路由
在router/index.js中about路由中添加子路由intro:
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
children: [
{
path: "/about/intro",
name: "Intro",
component: {
render(h) {
return h('div', "intro page")
}
}
}
]
}
];
const router = new VueRouter({
routes
});
export default router;
在views/About.vue中添加router-view组件:
<template>
<div class="about">
<h1>This is an about page</h1>
<router-view></router-view>
</div>
</template>
测试发现上面第四点中路由的写法完全没有考虑到嵌套路由,当在About组件中检测到有router-view组件时,router-view组件内部又去查找现有的url中/about对应的组件,明显查找到的属性又是About组件,就此进入死循环直至栈溢出。
解决:获取路由的层级深度,路由匹配时获取代表深度层级的数组matched,调用router-view组件时找到matched[depth]对应的组件。
// 设置vue以便后面使用
let Vue;
class VueRouter {
constructor(options) {
this.options = options;
// 初始化current,设置current为响应式数据,当current改变时重新渲染页面
// Vue.util.defineReactive(
// this,
// "current",
// window.location.hash.slice(1) || "/"
// );
// 嵌套路由中,router-view组件中不再需要根据current查找渲染对应组件,所以不需要current是响应式的,而需要代表深度层级的响应式matched数组
this.current = window.location.hash.slice(1) || "/";
Vue.util.defineReactive(this, "matched", []);
// 匹配路由
this.match();
// 监听url变化,重置current
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
// 路径发生变化要把matched数组清空,重新匹配
this.matched = [];
this.match();
});
}
// 设置match方法,遍历路由表,获取匹配关系数组
match(routes){
routes = routes || this.options.routes;
// 递归遍历
for(const route of routes) {
// 一般不会在Home配置子路由,所以暂时不考虑该路由有子路由的情况
if(route.path === "/" && this.current === "/") {
this.matched.push(route)
console.log(this.matched);
return;
}
// 判断当前hash是否与路由匹配,若匹配则将路由加入matched数组中,并递归其子路由
if(route.path !== " /" && this.current.indexOf(route.path) != -1) {
this.matched.push(route);
if(route.children) {
this.match(route.children)
}
console.log(this.matched);
return;
}
}
}
}
// vue插件都要有install方法:注册router-link和router-view两个组件 使得我们在组件中使用时可以<router-view>,将router挂载到Vue原型链上,使得我们在组件中使用时可以this.$router
VueRouter.install = function(_vue) {
Vue = _vue;
console.log(_vue);
// 将router挂载到Vue原型链上
// 因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载
Vue.mixin({
beforeCreate() {
// 判断当前组件是否为根组件,只有根组件有$router
// 因为根组件的vue实例创建时传入了vueRouter实例,vue组件中获取传入的参数可使用this.$options,所以获取传入的vueRouter实例可以用this.$options.router
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
});
// 注册router-view
// 根据当前hash找到对应component
// hash:window.location.hash.slice(1)
// 获取路由映射表:VueRouter在创建实例时传入了路由映射表,所以从VueRouter初始化中获取
Vue.component("router-view", {
render(h) {
// 嵌套路由
// 思路:获取路由的层级深度,路由匹配时获取代表深度层级的数组matched,调用router-view组件时找到matched[depth]对应的组件
// 在虚拟dom中设置routerView表明此组件是否为routerView组件
this.$vnode.data.routerView = true;
let depth = 0;
let parent = this.$parent;
// 循环获取所有parent
while(parent) {
// 获取$vnode.data
const vnodeData = parent.$vnode && parent.$vnode.data;
// 判断当前parent是否为router-view组件,若是,则深度加1
if(vnodeData && vnodeData.routerView) {
depth ++
}
parent = parent.$parent
}
// 获取path对应的component
let component = null;
const route = this.$router.matched[depth];
if(route) {
component = route.component
}
return h(component)
// const { current, options } = this.$router;
// let component = null;
// const route = options.routes.find(route => route.path === current);
// if (route) {
// component = route.component;
// }
// return h(component);
}
});
// 注册router-link
Vue.component("router-link", {
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
}
});
};
export default VueRouter;