今天我们来写一个简单版的vue-router!
vue-router在vue的使用中是作为一个插件来使用,首先我们先来写一下install方法供vue.use()去调用。
1.install的实现
那么intall做了什么呢?
- 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
- 挂载全局组件 如:router-link、router-view等
- 原型方法:vue.prototype.router
- 接着我们需要router在所有组件中使用,当每个组件创建之前,那我们使用mixin和beforeCreate将根父亲传入的router实例共享给所有的子组件
- 实例共享后 启动入口 this._router.init(this);
export default function install(Vue,options){
// 插件安装的入口
_Vue = Vue;
Vue.mixin({
beforeCreate(){ // this指向的是当前组件的实例
// 将父亲传入的router实例共享给所有的子组件
if(this.$options.router){//父组件
this._routerRoot = this;// 我给当前根组件增加一个属性_routerRoot 代表的是他自己
this._router = this.$options.router
//install完成后 启动入口
this._router.init(this); // 这里的this就是根实例
// 如何获取到current属性 将current属性定义在_route上
Vue.util.defineReactive(this,'_route',this._router.history.current);
}else{//孩子组件
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
// 无论是父组件还是子组件 都可以通过 this._routerRoot._router 获取共同的实例
}
});
// 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
Vue.component('router-link',Link);
Vue.component('router-view',View);
// 代表路由中所有的属性
Object.defineProperty(Vue.prototype,'$route',{
get(){
return this._routerRoot._route; // path matched
}
})
Object.defineProperty(Vue.prototype,'$router',{
get(){
return this._routerRoot._router; // 方法 push go repace..
}
});
}
接着,我们需要根据用户的配置,创建匹配器(扁平化用户routers,返回match和addRoutes)
路由映射表的创建
创建匹配器
import createRouteMap from "./create-route-map"
import {createRoute} from './history/base'
export default function createMatcher(routes) {
// pathMap = {'/':Home,'/about':About,'/about/a':'aboutA','/about/b':'aboutB'}
let { pathMap } = createRouteMap(routes); // 扁平化配置
function addRoutes(routes) {
createRouteMap(routes,pathMap);
}
function match(location) {
let record = pathMap[location]; // 可能一个路径有多个记录
if(record){
return createRoute(record,{
path:location
})
}
// 这个记录可能没有
return createRoute(null,{
path:location
})
}
return {
addRoutes, // 添加路由
match // 用于匹配路径
}
}
1.做路由的扁平化操作
//当有动态加载时需两个参数
export default function createRouteMap(routes,oldPathMap){
let pathMap = oldPathMap || Object.create(null); // 默认没有传递就是直接创建映射关系
routes.forEach(route => {
addRouteRecord(route,pathMap);
});
return {
pathMap
}
}
// 先序深度
function addRouteRecord(route,pathMap,parent){ // parent就是父组件的路由
let path =parent? (parent.path + '/' + route.path) :route.path
let record = {
path,
component:route.component,
parent
}
if(!pathMap[path]){ // 不能定义重复的路由
pathMap[path] = record;
}
if(route.children){
route.children.forEach(childRoute=>{
// 在遍历儿子时 将父亲的记录传入进去
addRouteRecord(childRoute,pathMap,record);
})
}
}
我们拿到扁平化后的match后,我需要根据不同的路径进行切换
先看用户传入的mode是什么模式在去实例它。
// 我需要根据不同的 路径进行切换
options.mode = options.mode || 'hash'; // 默认没有传入就是hash模式
switch (options.mode) {
case 'hash':
this.history = new HashHistory(this);
break;
case 'history':
this.history = new BrowserHistory(this);
break;
}
路由核心跳转模式
history
-base.js(共有的方法写在这)
-hash.js
-history.js
init(app) { // 初始化
// 监听hash值变化 默认跳转到对应的路径中
const history = this.history;
const setUpHashListener = () =>{
history.setupListener(); // 监听路由变化 hashchange
}
// 初始化 会先获得当前hash值 进行跳转, 并且监听hash变化
history.transitionTo(
history.getCurrentLocation(), // 获取当前的位置
setUpHashListener
)
history.listen((route)=>{ // 每次路径变化 都会调用此方法 订阅
app._route = route;
});
// setupListener 放到hash里取
// transitionTo 放到base中 做成公共的方法
// getCurrentLocation // 放到自己家里 window.location.hash / window.location.path
}
hash.js里的setupListener(监听hash变化)及getCurrentLocation(获取当前路径)
import { History } from "./base";
function ensureSlash() {//确保路径有/
if(window.location.hash){ // location.hash 是有兼容性问题的
return;
}
window.location.hash = '/'; // 默认就是 / 路径即可
}
function getHash(){
return window.location.hash.slice(1);
}
class HashHistory extends History{
constructor(router){
super(router);
this.router = router;
// 确保hash模式下 有一个/路径
ensureSlash();
}
getCurrentLocation(){
// 这里也是要拿到hash值
return getHash();
}
push(location){
this.transitionTo(location,()=>{ // 去更新hash值,hash值变化后虽然会再次跳转但是 不会重新更新current属性
window.location.hash = location
})
}
setupListener(){
window.addEventListener('hashchange',()=>{
// 当hash值变化了 在次拿到hash值进行跳转
this.transitionTo(getHash()); // hash变化在次进行跳转
})
}
}
export default HashHistory
base中的transitionTo
class History {
constructor(router) {
this.router = router;
// 当我们创建完路由号 ,先有一个默认值 路径 和 匹配到的记录做成一个映射表
// 默认当创建history时 路径应该是/ 并且匹配到的记录是[]
this.current = createRoute(null, { // 存放路由状态的
path: '/'
});
console.log(this.current)
// this.current = {path:'/',matched:[]}
}
listen(cb) {
this.cb = cb;
}
transitionTo(location, onComplete) {
// 跳转时都会调用此方法 from to..
// 路径变化了 视图还要刷新 , 响应式的数据原理
let route = this.router.match(location); // {'/'.matched:[]}
// 这个route 就是当前最新的匹配到的结果
if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
return
}
let queue = [].concat(this.router.beforeHooks); // 拿到了注册方法
const iterator = (hook,next) =>{
hook(this.current,route,()=>{
next();
})
}
runQueue(queue, iterator, () => {
// 在更新之前先调用注册好的导航守卫
this.updateRoute(route);
// 根据路径加载不同的组件 this.router.matcher.match(location) 组件
// 渲染组件
onComplete && onComplete();
})
}
updateRoute(route) {
// 每次你更新的是current
this.current = route; // 每次路由切换都会更改current属性
this.cb && this.cb(route); // 发布
// 视图重新渲染有几个要求? 1.模板中要用 2.current得是响应式的
}
}
- transitionTo方法的注意
- 在我们创建路由时刷新this.current = {path:'/',matched:[]}
- 在嵌套路由跳转时需要需要,需把父亲放到matched数组位置的前面
export function createRoute(record, location) {
let res = []; //[/about /about/a]
if (record) {
while (record) {
res.unshift(record);
record = record.parent;
}
}
return {
...location,
matched: res
}
}
- this.router.match这个发放做了转接,实际调用的是create-matcher.js里的createMatcher
- 当路由变化时用updateRoute()更改this.current
- 视图重新渲染有几个要求? 1.模板中要用 2.current得是响应式的
这是我们需要在install.js中把current变成响应式的,需要使用Vue.util.defineReactive变成响应式的方法,这是可以看看install.js如何写的
Vue.util.defineReactive(this,'_route',this._router.history.current);
- 当路由更改时,我们需要把this._route的值更改,初始化时调用history.listen去更改this._route
这是我们就明白了this.router是router的实例
接下来我们就可以写两个组件了,router-link、router-view
router-link、router-view函数式组件的编写
components
-link.js
-view.js
这是就可以在install.js引入组件了
import Link from './components/link';
import View from './components/view';
Vue.component('router-link',Link);
Vue.component('router-view',View);
link.js(router-link)
export default {
name:'routerLink',
props:{ // 属性接受
to:{
type:String,
required:true
},
tag:{
type:String,
default:'a'
}
}, // 写组件库 都可以采用jsx 来写
methods:{
handler(to){
this.$router.push(to);
}
},
render(){
let {tag,to} = this;
// jsx 语法 绑定事件
return <tag onClick={this.handler.bind(this,to)}>{this.$slots.default}</tag>
}
}
- 调用handler方法,实际是调用hash的跳转push方法,在调用bash中的transitionTo去更新路径变化
view.js(router-view)
export default {
name: 'routerView',
functional: true, // 函数式组件,函数式组件的特点 性能高,不用创建实例 = react函数组件 new Ctor().$mount()
render(h, { parent, data }) { // 调用render方法 说明他一定是一个routerView组件
// 获取 当前对应要渲染的记录
let route = parent.$route; // this.current;
let depth = 0;
data.routerView = true; // 自定义属性
// App.vue 中渲染组件时 默认会调用render函数,父亲中没有 data.routerView属性
// 渲染第一层,并且标识当前routerView为true
while (parent) { // router-view的父标签
// $vnode 代表的是占位符vnode 组件的标签名的虚拟节点
// _vnode 组件内部渲染的虚拟节点
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
parent = parent.$parent; // 不停的找父组件
}
// 第一层router-view 渲染第一个record 第二个router-view渲染第二个
let record = route.matched[depth]; // 获取对应层级的记录
if (!record) {
return h(); // 空的虚拟节点 empty-vnode 注释节点
}
// components
return h(record.component, data)
}
}
那路由就大致完成了,加下来写路由的钩子函数了
例如:
router.beforeEach((from,to,next)=>{
console.log(1);
setTimeout(() => {
next();
}, 1000);
})
router.beforeEach((from,to,next)=>{
console.log(2);
setTimeout(() => {
next();
}, 1000);
})
实现:
- 在router类上定义this.beforeHooks = [];
- 在实例上加一个方法,做订阅
beforeEach(fn){
this.beforeHooks.push(fn);
}
- 在base中的transitionTo的方法中,在更新之前先调用注册好的导航守卫
function runQueue(queue,iterator,cb){
// 异步迭代
function step(index){ // 可以实现中间件逻辑
if(index >= queue.length) return cb();
let hook = queue[index]; // 先执行第一个 将第二个hook执行的逻辑当做参数传入
iterator(hook,()=>step(index+1));
}
step(0);
}
class History {
transitionTo(location, onComplete) {
// 跳转时都会调用此方法 from to..
// 路径变化了 视图还要刷新 , 响应式的数据原理
let route = this.router.match(location); // {'/'.matched:[]}
// 这个route 就是当前最新的匹配到的结果
if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
return
}
let queue = [].concat(this.router.beforeHooks); // 拿到了注册方法
const iterator = (hook,next) =>{
hook(this.current,route,()=>{
next();
})
}
runQueue(queue, iterator, () => {
// 在更新之前先调用注册好的导航守卫
this.updateRoute(route);
// 根据路径加载不同的组件 this.router.matcher.match(location) 组件
// 渲染组件
onComplete && onComplete();
})
}
}
好了,差不多写完了,看完给个赞!