一.简介
核心实现就是根据路径变化找到对应组件,显示到 router-view 中
二.实现 vue-router
整体目录结构
├─vue-router
│ ├─components # 存放vue-router的两个核心组件
│ │ ├─link.js
│ │ └─view.js
│ ├─history # 存放浏览器跳转相关逻辑
│ │ ├─base.js
│ │ └─hash.js
│ │ └─h5.js
│ ├─create-matcher.js # 创建匹配器
│ ├─create-route-map.js # 创建路由映射表
│ ├─index.js # 引用时的入口文件
│ ├─install.js # install方法
1. install
Vue.use(Router)默认会调用当前返回 VueRouter 对象的 install 方法,挂载路由实例对象到 vue 上。
所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象。
install.js
export let _Vue;
VueRouter.install = function(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {// 如果有router属性说明是根实例
this._router = this.$options.router;
this._routerRoot = this;
} else {// 父组件渲染后会渲染子组件
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
Object.defineProperty(Vue.prototype, "$router", {
// 方法
get() {
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, "$route", {
// 属性
get() {
return this._routerRoot._route;
},
});
Vue.component("router-link", RouterLink);
Vue.component("router-view", RouterView);
};
在 Vue-Router 上增加一个 init 方法,主要目的就是初始化功能
Index.js
export default class VueRouter{
constructor(options){
this.matcher = createMatcher(options.routes || []);
}
+ init(app){}
}
2. createMatcher
根据用户传递的 routes 创建匹配关系,createMatcher 需要提供两个方法
match:match 方法用来匹配规则addRoutes:用来动态添加路由 动态路由的实现就是将新的路由插入到老的路由映射表 oldPathMap 中
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
// 收集所有的路由路径, 收集路径的对应渲染关系
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = {'/':'/的记录','/about':'/about记录'...}
let {pathList,pathMap} = createRouteMap(routes);
function addRoutes(routes){
createRouteMap(routes,pathList,pathMap);
}
function match(path){
return pathMap[path]
}
return {
addRoutes,
match
}
}
2.1 createRouteMap
createRouteMap 要把路由创建成映射表,路径和记录匹配。还可以添加映射表
映射表pathMap如下图所示
export function createRouteMap(routes, oldPathMap) {
// 如果有oldPathMap 我需要将routes格式化后 放到oldPathMap中
// 如果没有传递 需要生成一个映射表
let pathMap = oldPathMap || {};
routes.forEach((route) => {
addRouteRecord(route, pathMap);
});
return {
pathMap,
};
}
function addRouteRecord(route, pathMap, parent) {
let path = parent ? `${parent.path}/${route.path}` : route.path;
let record = {
path,
component: route.component,
props: route.props || {},
parent: parent,
};
pathMap[path] = record;
route.children &&
route.children.forEach((childRoute) => {
addRouteRecord(childRoute, pathMap, record);
});
}
3. 路由模式
index.js
class VueRouter {
constructor(options = {}) {
const routes = options.routes;
+ this.mode = options.mode || 'hash';
this.matcher = createMatcher(options.routes || []);
+ switch(this.mode) {
case 'hash': // location.hash => push
this.history = new Hash(this)
break
case 'history': // pushState => push
this.history = new HTML5History(this);
break
}
}
match(location){
return this.matcher.match(location);
}
+ init(app) {
const history = this.history;
const setUpListener = () =>{
history.setUpListener();
}
history.transitionTo(
history.getCurrentLocation(), // 各自的获取路径方法
setUpListener
);
}
}
history/base.js
transitionTo 匹配出 path 对应的 record 赋值给 current
function createRoute(record, location) {
// 创建路由
const matched = [];
// 不停的去父级查找
if (record) {
while (record) {
matched.unshift(record);
record = record.parent;
} // /about/a => [about,aboutA]
}
return {
...location,
matched,
};
}
export default class History {
constructor(router) {
this.router = router;
// 有一个数据来保存路径的变化
// 当前没有匹配到记录
this.current = createRoute(null, {
path: "/",
}); // => {path:'/',matched:[]}
}
transitionTo(path, cb) {
let record = this.router.match(path); // 匹配到后
this.current = createRoute(record, { path });
cb && cb(); // 默认第一次cb是hashchange
}
}
history/hash.js
import History from './base'
function ensureHash() {
if (!window.location.hash) {
window.location.hash = '/';
}
}
function getHash() {
return window.location.hash.slice(1);
}
export default class Hash extends History {
constructor(router) {
super(router);
// hash路由初始化的时候 需要增加一个默认hash值 /#/
ensureHash();
}
getCurrentLocation() {
return getHash();
}
setUpListener() {
window.addEventListener('hashchange', () => {
// hash值变化 再去切换组件 渲染
this.transitionTo(getHash());
})
}
}
History/h5.js
import History from './base'
export default class HTML5History extends History{
constructor(router){
super(router);
}
getCurrentLocation(){
return window.location.pathname;// 获取路径
}
setUpListener(){
window.addEventListener('popstate',()=>{ // 监听前进和后退
this.transitionTo(window.location.pathname);
})
}
pushState(location){
history.pushState({},null,location);
}
}
我们不难发现路径变化时都会更改 current ,我们可以把 current 属性变成响应式的,每次 current 变化刷新视图即可
current 里面的属性在哪使用,就会收集对应的 watcher
install.js
export let Vue;
import RouterLink from "./components/link";
import RouterView from "./components/view";
export default function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._router = this.$options.router;
this._routerRoot = this;
this._router.init(this);
+ Vue.util.defineReactive(this, "_route", this._router.history.current);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
Object.defineProperty(Vue.prototype, "$router", {
get() {
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, "$route", {
get() {
return this._routerRoot._route;
},
});
Vue.component("router-link", RouterLink);
Vue.component("router-view", RouterView);
}
改变current,this.$route却没有改变
所以我们当路径变化时需要执行此回调更新\_route 属性, 在init方法中增加监听函数
index.js
class VueRouter {
//...
init(app) {
//...
+ history.listen((route) => {
+ app._route = route;
});
}
}
base.js
export default class History {
//...
+ listen(cb) {
this.cb = cb;
}
transitionTo(path, cb) {
//..
+ this.current = route;
+ this.cb && this.cb(route);
}
}
三.实现 Router-Link 及 Router-View
可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。因为函数式组件只是函数,所以渲染开销也低很多。
1. router-view 组件
routeview 标记 true,表示渲染过。逐层 depth++匹配matched[depth]渲染。
export default {
functional: true,
render(h, { parent, data }) {
let route = parent.$route;
let depth = 0;
while (parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
if (!record) {
return h();
}
data.routerView = true;
return h(record.component, data);
},
};
2. router-link 组件
export default {
functional: true,
props: {
to: {
type: String,
required: true,
},
},
render(h, { props, slots, parent }) {
const click = () => {
parent.$router.push(props.to);
};
return <a onClick={click}>{slots().default}</a>;
},
};
四.实现 beforeEach
把所有的钩子组成一个数组依次执行
index.js
class VueRouter{
constructor(options={}){
this.beforeHooks=[];
}
beforeEach(fn){
this.beforeHooks.push(fn);
}
}
base.js
function runQueue(queue, iterator,cb) { // 迭代queue
function step(index){
if(index >= queue.length){
cb();
}else{
let hook = queue[index];
iterator(hook,()=>{ // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入
step(index+1)
})
}
}
step(0)
}
export default class History {
transitionTo(location, onComplete) {
// 跳转到这个路径
let route = this.router.match(location);
if (location === this.current.path && route.matched.length === this.current.matched.length) {
return
}
let queue = [].concat(this.router.beforeHooks);
const iterator = (hook, next) => {
hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数
next();
});
}
runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由
this.updateRoute(route);
onComplete && onComplete();
});
}
updateRoute(route) {
this.current = route;
this.cb && this.cb(route);
}
listen(cb) {
this.cb = cb;
}
}
五.小结
默认初始化init时,先进行一次路由跳转 transitionTo,跳转到对应路径 getCurrentLocation 之后设置路由监听事件 setUpListener。路由变化 触发监听回调 transitionTo(path);。current改变,_route改变,routeview逐层渲染对应匹配matched的记录。