likes
comments
collection
share

Vue系列深入教程(一)-- 实现基本的vue-router

作者站长头像
站长
· 阅读数 14

大家好,我是小黑。

本人打算写一个和vue全家桶相关的系列教程,从vue2到vue3,这系列不会有太多讲解vue全家桶的使用,所以需要对vue及其全家桶比较熟练。

系列文章

  1. Vue系列深入教程(一)-- 实现基本的vue-router 

  2. Vue系列深入教程(二)-- 实现基本的vuex

建议系列文章从头读起,不要跳着读。

本文使用vue2基于hash实现简单的vue-router,因为是vue2写的,所以实现vue-router的版本也是3版本。

看完本文,你不仅学会了vue-router的基本实现原理,你还可以尝试自己写vue插件。

系列教程代码menorepo仓库地址gitee.com/jimpp/front…

本节代码在vue-series-demo/my-vue-router下

VueRouter使用回顾

首先回顾一下,使用vueRouter的时候,需要进行什么操作

1. npm install vue-router;(废话,但还是得写)

2. 在main.js中

import Router from 'vue-router'
Vue.use(Router)

引入并通过Vue.use注入Router

3. 添加路由表routes.js

const routes = [ 
    { path: 'home', component: importCom('home', '首页')},
    { path: 'about', component: importCom('about', '关于')}
]
export default routes;

4. 初始化router实例

let router = new VueRouter({
    routes: 第3步的routes
});

5. 初始化vue实例的时候注入router

new Vue({
    el: '#app',
    router, // 在这里注入了
    render: c => c(App),
})

6. 注入成功后,会得到router-view和router-link两个组件,同时获得全局route,route,route,router对象,能够使用vueRouter实例的所有方法

<router-view></router-view> // 路由的内容在这里渲染
<router-link to="/home"></router-link> // 记得要加"/"开头,点击就会渲染/home对应的组件内容

可以通过$router.push和$router.replace跳转

vue-router的实现原理

hash路由

  1. hash是url中#后面部分,hash的变化不会刷新页面,不会发起http请求
  2. hash变化有,浏览器的前进后退也会添加记录,前进后退可以前进或后退到前一个hash
  3. hash常用来让界面定位到某一个位置
  4. 关键:window.onhashchange来监听hash的变化

history路由

  1. h5 history api 新增了 pushState()replaceState() 两个方法
  2. 执行这两个方法后,只是修改了当前url,并不会刷新页面或者发起http请求、
  3. 关键:window.onpopstate来监听url的变化

Vue.use的用法

实现前必须要了解一下这个关键方法看,这是写vue插件的关键方法

官方传送门:cn.vuejs.org/guide/reusa…

这里我也简单说一下

首先,我想写一个插件myPlugin,那么它可以是一个有install方法的对象

const myPlugin = {
  install(app, options) {
  }
}

其中app是vue实例,options看下面

app.use(myPlugin, {
    // 这就就会传到上面options
})

vue.use的使用总结如下

  1. vue.use接受两个参数,第一个是插件对象,第二个是传给插件的参数

  2. vue.use会自动调用插件的install方法,把vue实例注入作为第一个参数,第二个就是第一步传进来的参数

  3. 因为可以获得vue的实例,所以可以通过实例访问到原型,给vue原型挂载参数,router、router、routerroute对象就是通过这种方式注入的

开发环境准备

需要做什么

通过上面的VueRouter使用回顾,可以知道,实现VueRouter需要以下的东东

  1. 一个拥有install方法的对象Router
  2. 通过Vue.use(Router)使用Router对象
  3. const router = new Router(), 创建router实例,注入路由表,全局注册router-link和router-view组件
  4. Router对象中使用window.onhashchange监听hash变化,重新渲染界面
  5. new Vue({ router }) 使用router实例

目录结构

既然官方都推荐使用vite,就用vite来预览吧

// package.json
{  
    "name": "my-vue-router",  
    "version": "1.0.0",  
    "description": "",  
    "main": "index.js",  
    "scripts": {    "dev": "vite"  },  
    "keywords": [],  
    "author": "",  
    "license": "ISC",  
    "dependencies": {    
        "vue": "2.7.9"  
    },  
    "devDependencies": {    
        "vite": "3.0.7",    
        "vue-template-compiler": "2.7.9",    
        "vite-plugin-vue2": "1.9.3"  
    }
}

vite.config.js

import path from "path";
import { defineConfig } from "vite";
import { createVuePlugin } from "vite-plugin-vue2";
export default defineConfig({  
    resolve: {    
        alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],  
    },  
    plugins: [createVuePlugin()],
});

Router/index.js

因为Router 要 new Router获得router 实例,又必须拥有install方法,所以Router的雏形如下

class Router{
   static install(app, options = {}) {
        // todo
   }
}

export default Router

Router/router-link.js

export default {  
    props: {    
        to: String,    
        required: true,  
    },  
    render(h) {    
        return h("a",{        
            attrs: {          
                href: "#" + this.to,        
            },      
        },[this.$slots.default]);  
    },
};

Router/router-view.js

export default {  
    render(h) {    
        return h("div", "123");  
    },
};

看不懂router-link和router-view两个js的请点击vue渲染函数传送门

路由表

const routes = [ 
    { path: '/home', component:() => import('@/views/home.vue')},
    { path: '/about', component: import('@/views/about.vue')}
]
export default routes;

模板

// about.vue
<template>
<div>关于</div>
</template>

// home.vue
<template>
<div>首页</div>
</template>

index.js

import Router from "./Router/index";
import Vue from "vue";
import routes from "./routes";
import App from "./App.vue";

Vue.use(Router);
const router = new Router({ routes,});

const app = new Vue({  
    router,  
    render: (h) => h(App),
}).$mount("#app");

index.html

<!DOCTYPE html>
    <html lang="en">  
        <head>    
        <meta charset="UTF-8" />    
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />    
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />   
        <title>Document</title>  
    </head>  
    <body>    
        <div id="app"></div>  
    </body>  
    <!-- 引用入口文件 -->  
    <script type="module" src="./src/index.js"></script>
</html>

此时运行npm run vite,可以再浏览器看到a:1

实现hash router

添加路由功能

按照上面vue router使用回顾,先修改App.vue的代码

<template>  
    <div>    
        <router-link to="/">首页</router-link>    
        <router-link to="/about">关于</router-link>    
        <router-view></router-view>  
    </div>
</template>
<script>export default {};</script>

此时刷新浏览器,会发现报错

Vue系列深入教程(一)-- 实现基本的vue-router

意思是router-link和router-view还没注册,想想使用vue-router的时候,我们是不需要写任何的注册组件代码的,所以注册组件的操作必定是在Router内部实现,全局注册组件使用的是Vue.component方法,所以在install方法中会注册router-link和router-view组件

全局注册RouterView和RouterLink

import RouterView from "./router-view";
import RouterLink from "./router-link";
class Router {  
    constructor(option = {}) {  }  
    static install(app, options = {}) {    
        app.component("router-view", RouterView);    
        app.component("router-link", RouterLink);  
    }
}
export default Router;

刷新界面可以看到

Vue系列深入教程(一)-- 实现基本的vue-router

这不行,router-view现在是写死的,必须要根据routes中的component来渲染出组件才行,修改一下router-view的代码

根组件的$options

怎么样可以在RouterView获取到对应的component呢,只要拿到当前的hash,找到routes对应的路由表就好了,怎么获取hash和routes呢

首先回顾一下,在new Router的时候,把routes传给了Router的构造函数

然后console.log(app);输出一下当前app实例和app下的所有children实例看看看看

Vue系列深入教程(一)-- 实现基本的vue-router

Vue系列深入教程(一)-- 实现基本的vue-router

Vue系列深入教程(一)-- 实现基本的vue-router

还有其它的就不贴图了,发现了什么东西,app实例的$options才有router这个对象,这个对象就是Router的实例,其它组件是没有的!这个非常关键!通过这个特性可以将router挂载在原型上,因为所有组件都可以访问到原型上的属性

怎样挂载到原型?

使用app.mixin,通过混入beforeCreate钩子,再根据上面只有根组件的$options才有router的特性,修改Router/index.js代码如下

class Router {  
    constructor(option = {}) {    
        const { routes = [] } = option;     
        const app = Router.VueInstance; // 获取install中的app实例    
        this.routes = routes; // 获取传入的routes    
        app.mixin({      
            beforeCreate() {        
                if (this.$options.router) {          
                    app.prototype.$router = this.$options.router; // 在原型上挂载router        
                }      
            },    
        });  
    }  
    static VueInstance = null;  
    static install(app, options = {}) {    
        Router.VueInstance = app; // 保存app实例    
        app.component("router-view", RouterView);    
        app.component("router-link", RouterLink);  
    }
}

此时刷新浏览器,可以看到原型里面已经有$router了

Vue系列深入教程(一)-- 实现基本的vue-router

根据hash渲染

vue-router官方文档中router实例有一个currentRoute参数,代表着当前的路由对象

Vue系列深入教程(一)-- 实现基本的vue-router

大家应该都清楚,route对象应该包含path,component,meta等参数,这里我们只需要currentRoute的path和component就可以了,path不就是当前url中的hash吗,component也简单,从this.routes找到对应path的component就可以了

修改下constructor代码

constructor(
    option = {}) {    
        // ...省略部分代码    
        this.routes = routes;    
        const initPath = window.location.hash.slice(1) || "/"; // 1    
        this.currentRoute = routes.find((route) => route.path === initPath); // 2    
        // ...省略app.mixin的代码  
}

解释一下,注释中1,2的含义

  1. window.location.hash获取到的hash是带#的,把#去掉,没有的话默认是/首页,例如输入/#/aaa,那么获取到的是 /aaa

  2. 从routes中找到path等于当前hash的route复制给this.currentRoute

分别改变url的hash为 /#/home 和 /#/about 并按下回车,可以看到

Vue系列深入教程(一)-- 实现基本的vue-router

Vue系列深入教程(一)-- 实现基本的vue-router

修改一下router-view.js

export default {  
    render(h) {    
        const component = this.$router.currentRoute?.component || null;    
        return h(component);  
    },
};

再次改变一下hash并刷新界面,如下图

Vue系列深入教程(一)-- 实现基本的vue-router

可以看到界面可以渲染出来,但是也遗留了3个问题

  1. hash发生变化时没有重新渲染
  2. 点击浏览器前进后退的时候也没有重新渲染
  3. 当hash是"/"时,routes中配置的是redirect到 /home, 还没实现

解决redirect

封装一个getRoute方法,需要传入path

function getRoute(path) {  
    const route = this.routes.find((route) => route.path === path);  
    if (route.redirect) {    
        return (window.location.href = `/#${route.redirect}`);  
    }  
    return route;
}

这个方法就是如果根据initPath获取到的route path有redirect,则修改url的hash

然后constructor修改如下

constructor(option = {}) {
    // ...    
    const initPath = window.location.hash.slice(1) || "/";    
    this.currentRoute = getRoute.call(this, initPath); //getRoute的this指向Router实例    
    console.log(this.currentRoute);
    // ...  
}

刷新浏览器

Vue系列深入教程(一)-- 实现基本的vue-router

点击首页已经渲染成/home了

监听hash变化并渲染界面

Router 类中添加实例方法onHashChange,并在contructor中

class Router {  
    constructor(option = {}) {
        // ...    this.routes = routes;    
        this.currentRoute = null;    
        window.addEventListener("hashchange", this.onHashChange.bind(this));    
        window.addEventListener("load", this.onHashChange.bind(this));
        // ...  
    }  
    onHashChange() {    
        console.log("hash change");    
        const initPath = window.location.hash.slice(1) || "/";    
        this.currentRoute = getRoute(this.routes, initPath); //getRoute的this指向Router实例 
    }
}

刷新一下

Vue系列深入教程(一)-- 实现基本的vue-router

onHashChange明明执行了,为什么没有重新渲染,因为this.currentRoute不是响应式的,重新赋值并不会更新dom,所以this.currentRoute必须是响应式对象

vue2.6 新增了一个api Vue.observable(object),用于将一个对象变成响应式对象

Vue系列深入教程(一)-- 实现基本的vue-router

修改一下代码

class Router {  
    constructor(option = {}) {    
        this.routes = routes;    
        this.currentRoute = app.observable({      
            path: "",      
            component: null,    
        }); // 1   
        window.addEventListener("hashchange", this.onHashChange.bind(this)); // 2    
        window.addEventListener("load", this.onHashChange.bind(this)); // 3  
    }  
    onHashChange() {    
        const initPath = window.location.hash.slice(1) || "/";    
        const route = getRoute.call(this, initPath); // 4    
        this.currentRoute.path = route.path;    
        this.currentRoute.component = route.component;  
    }
}
  1. this.currentRoute转换为响应式对象
  2. 首次加载和hash变化是触发currentRoute的更新
  3. 重新给currentRoute的path和component赋值,界面就会自动更新

效果如下

Vue系列深入教程(一)-- 实现基本的vue-router

这里其实已经实现了简单的vue-router

锦上添花

getRoute方法中,每次都要执行find,假如先从首页切到关于,再切回来首页,其实首页已经是找过的了,但是目前的实现会导致又执行一次find,所以修改一下getRoute方法

constructor(option = {}) {
    // ...    
    this.routes = routes;    
    this.cache = {}; // 1    
    this.currentRoute = app.observable({      
        path: "",      
        component: null,    
    });    
    // ...  
}

构造函数中添加一个cache对象

function getRoute(path) {  
    if (this.cache[path]) { // 1    
        return this.cache[path];  
    }  
    const route = this.routes.find((route) => route.path === path);  
    if (route.redirect) {    
        return (window.location.href = `/#${route.redirect}`);  
    }  
    this.cache[path] = route; // 2  
    return route;
}
  1. 如果缓存中有,直接从缓存读取
  2. 初次加载添加到缓存中

刷新后跟上面的效果是一样的