小白前端之路:手写一个简单的vue-router这几年,好像过的好快,怀念我的大学生活。 - 连某人 大三实习生,之前写过简单MVVM框架、简单的vuex、但是看了vue-router的源码(看了

这几年,好像过的好快,怀念我的大学生活。

  • 连某人

大三实习生,之前写过简单MVVM框架、简单的vuex、但是看了vue-router的源码(看了大概)之后就没有写,趁着周末不用工作(大三趁着不开学出来实习,现在水平比较低,代码也没有优化,请小喷)来写一下,写的比较仓促。

github仓储地址
使用

复制代码
//main.js
import LJRouter from “./lib/LJRouter/src”;
Vue.config.productionTip = false
Vue.use(LJRouter)
let router = new LJRouter({
mode:‘hash’,
routes:[
{
path:’/’,component:hello
},
{
path:’/tagone’,component:tagone
},
{
path:’/tagtwo’,component:tagtwo
}
]
});
/router.beforeEach(function(from,to,next) {
let path = to.path;
if(path===’/tagtwo’) {
next();
}else {
next(’/tagtwo’)
}
});
/
复制代码
原理
vue-router核心原理其实很简单,分为hash模式和history模式(一共三种,还有一种我还没有去了解),

hash模式
其实就是监听onhashchange事件,当hash改变就会触发该事件,然后拿到对应record,然后去触发router-view的更新(这里为什么会触发更新,请看router-view的实现),当调用router的push/replace其实也是去拿到对应的record去触发组件更新重新调用render函数

history模式
其实就是监听popState事件(注意是调用history.go/history.forward的时候才会触发),然后通过pushState和replaceState去改变path,注意这里通过html5新增的这两个方法改变path是不会重新发起请求的,当改变path之后,再拿到改变的path,从而获取对应的record,再触发组件更新,重新调用render函数。

实现流程分析
前提概要
PS:(我这里是以我写的LJRouter来分析,名字可以忽略)

请各位大佬小喷,以及真心求志同道合的朋友,另外本人征女友。

install初始化
function install(vue) {
let _vue = install._vue;
if(_vue) {//防止多次注册安装
throw new Error(‘Cannot be reinstalled’);
}
install._vue = vue;
mixinApply(vue);//全局混入
}
复制代码
vue有一个特点就是插件化,类似vue-router、vuex这些都是以插件的方式引入,而不是内置,通过Vue.use注册(这里不讲Vue.use实现,很简单,不懂的话,可以看Vue.use的源码实现),然后会调用install函数

function mixinApply(vue) {
vue.mixin({
beforeCreate:routerInit
});
definedRouter(vue);
vue.component(‘ljView’,ljView);
vue.component(‘ljLink’,ljLink);
}
复制代码
mixinApply函数主要是全局混入(不理解Vue.mixin的同学,请看官网文档)并注册了两个全局组件LJ-View以及LJ-Link。

function routerInit() {
if(this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …_router = this.options.router;
this.KaTeX parse error: Expected 'EOF', got '}' at position 127: …ntRouter); }̲ else { …parent._router;
}

}
复制代码
routerInit 通过Vue.mixin全局混入,所以每个组件都会调用该函数,该函数主要是的作用主要是为每个组件添加一个指针指向LJRouter实例

function definedRouter(vue) {
Object.defineProperty(vue.prototype,‘ r o u t e r ′ , g e t ( ) r e t u r n t h i s . r o u t e r . r o u t e r ; ) ; O b j e c t . d e f i n e P r o p e r t y ( v u e . p r o t o t y p e , ′ router',{ get() { return this._router.router; } }); Object.defineProperty(vue.prototype,' router,get()returnthis.router.router;);Object.defineProperty(vue.prototype,route’,{
get() {
return this._router.route;
}
});
}
复制代码
definedRouter这个函数比较简单,所以就不讲了

LJRouter实例构建
LJRouter的编写

import install from ‘./install.js’;
import Hashhistory from “./history/Hashhistory”;
import Htmlhistory from ‘./history/Htmlhistory’;
import {deepCopy} from ‘./util/util.js’
class LJRouter {
static install = install;
constructor(options) {
this.$options = options;
this.mode = options.mode||‘hash’;
switch (this.mode) {
case “hash”:this.history = new Hashhistory(options);break;
case “history”:this.history = new Htmlhistory(options);break;
}
}
addRoutes(routes) {
this.history.addRoutes(routes);
}
get router() {
return {
push:this.push.bind(this),
back:this.back.bind(this),
replace:this.replace.bind(this)
};
}
get route() {
let query = this.history.currentRouter.current.query;
query = deepCopy(query);
return {query}
}
push({path,name,query}) {
this.history.push({path,name,query});
}
replace({path}) {
this.history.replace({path});
}
back() {
this.history.back();
}
//全局前置守卫
beforeEach(fn) {
if(this.mode === ‘hash’||this.mode === ‘history’) {
this.history.beforeEach(fn);
} else {
throw new TypeError(‘beforeEach is undefined’);
}
}
}
export default LJRouter;

复制代码
我们先看LJRouter的构造器,其实LJRouter的构造器也是很简单,其实就是各种参数代理到LJRouter实例上,然后再根据mode来创建对应的实例(Hashhistory||Htmlhistory),在LJRouter显式原型上还有一些函数,其实都是一个代理。本质还是会调用实例上的history隐式原型上的方法,先讲个大概逻辑,之后再按某个功能来介绍实现逻辑

Hashhistory
import createMatcher from “…/createMatcher”;
class Hashhistory {
constructor(options) {
this.$options = options;
let routes = options.routes;
const {addRoutes,getCurrentRecord} = createMatcher(routes);
this.addRoutes = addRoutes;
this.getCurrentRecord = getCurrentRecord;
this.currentRouter = {current:{}};
this.pathQueue = [];
this.beforeEachCallBack = null;
this.transitionTo();
this.setUpListener();
}
setUpListener() {
window.onhashchange = function() {
let hash = location.hash;

  if(hash==='') {
    location.href = location.href+'#/';
    hash = '/';
  } else if(hash === '#/') {
    hash = '/'
  }
  else {
    hash = hash.slice(1);
  }
  this.confirmTransitionTo({hash});
}.bind(this);

}
transitionTo() {
let hash = location.hash;
if(hash.indexOf(’#’)=-1) {
location.href = location.href+’#/’;
hash = ‘/’;
} else {
hash = hash.slice(1);
}
if(hash
=’’) {
hash = ‘/’;
}
this.pathQueue.push(hash);
this.confirmTransitionTo({hash});
}
confirmTransitionTo({hash,name,query}) {
let currentRecord;
currentRecord = this.getCurrentRecord({path:hash,name,query});
let that = this;
function next(resolve,reject) {
return function(path) {
if(Object.is(undefined,path)) {
resolve(currentRecord);
}else {
location.hash = ‘#’+path;
that.confirmTransitionTo({hash:path});
}
}
}

new Promise(function(resolve,reject){
        if(this.beforeEachCallBack) {
           this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
        }else {
          resolve(currentRecord);
        }
      }.bind(this)).then(record=>{
         this.currentRouter.current = record
      });

}
push({path,name,query}) {
this.pathQueue.push(path);
this.confirmTransitionTo({hash:path,name,query});
}
back() {
let pathQueue = this.pathQueue;
let path = null;
if(pathQueue.length<=1) {
console.error(‘pathQueue value is < 2:redirect to /’);
path = ‘/’
} else {
path = pathQueue[pathQueue.length-2];
pathQueue.length = pathQueue.length-2;
}
this.pathQueue.push(path);
this.confirmTransitionTo({hash:path});
}
replace({path}) {
this.pathQueue.length = 0;
this.confirmTransitionTo({hash:path});
}
//全局前置守卫
beforeEach(fn) {
this.beforeEachCallBack = fn;
}
}
export default Hashhistory;
复制代码
Hashhistory实例构建时,会对routes进行解析(解析过程看createMatcher),然后就调用transitionTo,transitionTo获取hash,然后调用confirmTransitionTo进行组件切换.transitionTo做了一个逻辑判断,如果没有检测到#的话会自动追加hash。

Htmlhistory
import createMatcher from “…/createMatcher”;
class Htmlhistory {
constructor(options) {
this.$options = options;
let routes = options.routes;
const {addRoutes,getCurrentRecord} = createMatcher(routes);
this.addRoutes = addRoutes;
this.getCurrentRecord = getCurrentRecord;
this.currentRouter = {current:{}};
this.beforeEachCallBack = null;
let path = this.getPath();
this.transitionTo(path)
this.setUpListener();

}
setUpListener() {
window.onpopstate = function() {
let path = this.getPath();
this.confirmTransitionTo({path});
}.bind(this);
}
getPath() {
let path = location.href.split(’/’)[3];
path = path?’/’+path:’/’;
return path;
}
transitionTo(path) {
this.confirmTransitionTo({path});
}
confirmTransitionTo({path,name,query}) {
let currentRecord;
currentRecord = this.getCurrentRecord({path,name,query});
let that = this;
function next(resolve,reject) {
return function(path) {
if(Object.is(undefined,path)) {
resolve(currentRecord);
}else {
that.confirmTransitionTo({path});
}
}
}
new Promise(function(resolve,reject){
if(this.beforeEachCallBack) {
this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
}else {
resolve(currentRecord);
}
}.bind(this)).then(record=>{
this.currentRouter.current = record
});
}
push({path,name,query}) {
history.pushState(null,null,path);
this.confirmTransitionTo({path,name,query});
}
back() {
history.go(-1);
}
replace({path}) {
history.replaceState(null,null,path);
this.confirmTransitionTo({path});
}
//全局前置守卫
beforeEach(fn) {
this.beforeEachCallBack = fn;
}
}
export default Htmlhistory;

复制代码
HtmlHistory和HashHistory的实现其实差不多,不过HtmlHistory获取的是path,通过replaceState和pushState进行path切换,并监听popstate事件.大概的逻辑差不多,就不一一讲了。

confirmTransitionTo
confirmTransitionTo({hash,name,query}) {
let currentRecord;
currentRecord = this.getCurrentRecord({path:hash,name,query});
let that = this;
function next(resolve,reject) {
return function(path) {
if(Object.is(undefined,path)) {
resolve(currentRecord);
}else {
location.hash = ‘#’+path;
that.confirmTransitionTo({hash:path});
}
}
}

new Promise(function(resolve,reject){
        if(this.beforeEachCallBack) {
           this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
        }else {
          resolve(currentRecord);
        }
      }.bind(this)).then(record=>{
         this.currentRouter.current = record
      });

}
复制代码
confirmTransitionTo是一个核心函数,主要就是根据相应hash获取对应的record,然后更新 currentRouter.current的指向,currentRouter其实是响应式的,相关请看LJ-View实现

createMatcher
class Record {
constructor(path,url,query,params,name,component) {
this.url = url;
this.path = path;
this.query = query;
this.params = params;
this.name = name;
this.component = component;
}
}
function addRecord(pathList,pathMap,nameMap,route) {
let record = new Record(route.path,route.url,route.query,route.params,route.name,route.component);
if(route.path) {
pathList.push(route.path);
}
if(route.name) {
nameMap[route.name] = record;
}
if(route.path) {
pathMap[route.path] = record;
}
}
function addRouterRecord(pathList,pathMap,nameMap,routes) {
routes.forEach(item=>{
addRecord(pathList,pathMap,nameMap,item);
});
}
function createMatcher(routes) {
let pathList = [];
let pathMap = {};
let nameMap = {};
addRouterRecord(pathList,pathMap,nameMap,routes);
function getCurrentRecord({path,name,query}) {
let record = null;
if(name) {
record = nameMap[name];
}
if(path) {
record = pathMap[path];
}
if(record) {
record.query = query;
}
return record||{};
}
function addRoutes(appendRoutes) {
addRouterRecord(pathList,pathMap,nameMap,appendRoutes);
}
return{getCurrentRecord,addRoutes};
}
export default createMatcher;
复制代码
createMatcher这个函数其实比较简单,其实就是根据routes的每一项创建对应的record,然后以path为key record为值添加到pathMap上,这里也有nameMap,参考了vue-router源码,返回addRoutes、getCurrentRecord,addRoutes主要作用时可以使用该函数动态添加路由规则,这里实现了query传值,但是没有实现params。你可以自己加上去。注意:(我这里没有实现子路由,如果你想的话可以加上去);

API实现
前提概要
//install,js
function definedRouter(vue) {
Object.defineProperty(vue.prototype,‘ r o u t e r ′ , g e t ( ) r e t u r n t h i s . r o u t e r . r o u t e r ; ) ; O b j e c t . d e f i n e P r o p e r t y ( v u e . p r o t o t y p e , ′ router',{ get() { return this._router.router; } }); Object.defineProperty(vue.prototype,' router,get()returnthis.router.router;);Object.defineProperty(vue.prototype,route’,{
get() {
return this._router.route;
}
});
}
复制代码
在install.js,就通过Object.defineProperty在Vue.Prototype上定义了 r o u t e r 和 router和 routerroute,实际上会被LJRouter上的get Router getRoute劫持到

//LJRouter
get router() {
return {
push:this.push.bind(this),
back:this.back.bind(this),
replace:this.replace.bind(this)
};
}
get route() {
let query = this.history.currentRouter.current.query;
query = deepCopy(query);
return {query}
}
复制代码
实际上还是会调用LJRouter实例上history属性上的同名方法

push
push({path,name,query}) {
this.pathQueue.push(path);
this.confirmTransitionTo({hash:path,name,query});
}
复制代码
Hash模式-获取传过去的path,然后在调用confirmTransitionTo函数,实现路由切换,这里多了个pathQueue,其实是用来存储记录的一个数组,为之后实现back做一个铺垫。很简单的逻辑。

push({path,name,query}) {
history.pushState(null,null,path);
this.confirmTransitionTo({path,name,query});
}
复制代码
history模式-主要是通过pushState进行path切换,然后再调用confirmTransitionTo

replace
replace({path}) {
this.pathQueue.length = 0;
this.confirmTransitionTo({hash:path});
}
复制代码
Hash模式-会把记录栈清空,再调用confirmTransitionTo

replace({path}) {
history.replaceState(null,null,path);
this.confirmTransitionTo({path});
}
复制代码
history模式-主要是通过replaceState进行path切换,然后再调用confirmTransitionTo

back
back() {
let pathQueue = this.pathQueue;
let path = null;
if(pathQueue.length<=1) {
console.error(‘pathQueue value is < 2:redirect to /’);
path = ‘/’
} else {
path = pathQueue[pathQueue.length-2];
pathQueue.length = pathQueue.length-2;
}
this.pathQueue.push(path);
this.confirmTransitionTo({hash:path});
}
复制代码
Hash模式-这里做了一个判断,如果记录栈只有一个记录,如果调用back就会返回根目录,如果不止一个记录,就会返回上一个记录。

setUpListener() {
window.onpopstate = function() {
let path = this.getPath();
this.confirmTransitionTo({path});
}.bind(this);
}
back() {
history.go(-1);
}
复制代码
history模式-其实就是调用history,go然后操作游览器历史记录栈,这里会触发popstate事件,然后获取返回的path,再进行路由切换

beforeEach
//LJRouter
beforeEach(fn) {
if(this.mode === ‘hash’||this.mode === ‘history’) {
this.history.beforeEach(fn);
} else {
throw new TypeError(‘beforeEach is undefined’);
}
}
复制代码
//对应history 例如Hashhistory
//全局前置守卫
beforeEach(fn) {
this.beforeEachCallBack = fn;
}

复制代码
confirmTransitionTo({hash,name,query}) {
let currentRecord;
currentRecord = this.getCurrentRecord({path:hash,name,query});
let that = this;
function next(resolve,reject) {
return function(path) {
if(Object.is(undefined,path)) {
resolve(currentRecord);
}else {
location.hash = ‘#’+path;
that.confirmTransitionTo({hash:path});
}
}
}

new Promise(function(resolve,reject){
        if(this.beforeEachCallBack) {
           this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
        }else {
          resolve(currentRecord);
        }
      }.bind(this)).then(record=>{
         this.currentRouter.current = record
      });

}
复制代码
这里我用了写koa2洋葱圈的写法,不太好说。express中间间机制也差不多.

组件实现
LJView
//LJView
let LJView = {
functional:true,
render(c,context) {
let root = context.parent.KaTeX parse error: Expected '}', got 'EOF' at end of input: … return c(root.notFoundComponent);
}

return c(root._route.current.component);

}
}
export default LJView;
复制代码
function routerInit() {
if(this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …_router = this.options.router;
this.KaTeX parse error: Expected 'EOF', got '}' at position 127: …ntRouter); }̲ else { …parent._router;
}

}
复制代码
confirmTransitionTo({hash,name,query}) {
let currentRecord;
currentRecord = this.getCurrentRecord({path:hash,name,query});
let that = this;
function next(resolve,reject) {
return function(path) {
if(Object.is(undefined,path)) {
resolve(currentRecord);
}else {
location.hash = ‘#’+path;
that.confirmTransitionTo({hash:path});
}
}
}

new Promise(function(resolve,reject){
        if(this.beforeEachCallBack) {
           this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
        }else {
          resolve(currentRecord);
        }
      }.bind(this)).then(record=>{
         this.currentRouter.current = record
      });

}
复制代码
LJView是一个函数式组件,这里其实很数据响应式有关,通过defineReactived定义一个响应式数据,然后在render函数中使用会产生依赖收集过程(普遍认为模板编译时候会产生依赖收集,但模板终究会编译成render函数,因此在render函数中会使用该变量,产生依赖收集(如果不对,请大佬指教)),当confirmTransitionTo调用时,会改变该数据,从而触发组件更新。

LJLink
let LJLink = {
props:{
to:{
type:String,
default:""
},
tag:{
type:String,
default:‘a’
}
},
render© {
let tag = this.tag;
return c(tag,{on:{click:()=>{this.KaTeX parse error: Expected 'EOF', got '}' at position 28: …{path:this.to})}̲}},this.slots.default[0].text);
}
}
export default LJLink;

复制代码
LJLink的实现比较简单

想学习更多的前端的知识请加q群:
获取方式:
一、搜索QQ群,前端学习交流群:1017810018​

二、点击链接加入群聊【web前端技术交流学习群】:https://jq.qq.com/?_wv=1027&k=kox240jl

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值