Vue系列深入教程(一)-- 实现基本的vue-router
大家好,我是小黑。
本人打算写一个和vue全家桶相关的系列教程,从vue2到vue3,这系列不会有太多讲解vue全家桶的使用,所以需要对vue及其全家桶比较熟练。
系列文章
-
Vue系列深入教程(一)-- 实现基本的vue-router
建议系列文章从头读起,不要跳着读。
本文使用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路由
- hash是url中#后面部分,hash的变化不会刷新页面,不会发起http请求
- hash变化有,浏览器的前进后退也会添加记录,前进后退可以前进或后退到前一个hash
- hash常用来让界面定位到某一个位置
- 关键:window.onhashchange来监听hash的变化
history路由
- h5 history api 新增了
pushState()
和replaceState()
两个方法 - 执行这两个方法后,只是修改了当前url,并不会刷新页面或者发起http请求、
- 关键: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的使用总结如下
-
vue.use接受两个参数,第一个是插件对象,第二个是传给插件的参数
-
vue.use会自动调用插件的install方法,把vue实例注入作为第一个参数,第二个就是第一步传进来的参数
-
因为可以获得vue的实例,所以可以通过实例访问到原型,给vue原型挂载参数,router、router、router、route对象就是通过这种方式注入的
开发环境准备
需要做什么
通过上面的VueRouter使用回顾,可以知道,实现VueRouter需要以下的东东
- 一个拥有install方法的对象Router
- 通过Vue.use(Router)使用Router对象
- const router = new Router(), 创建router实例,注入路由表,全局注册router-link和router-view组件
- Router对象中使用window.onhashchange监听hash变化,重新渲染界面
- 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>
此时刷新浏览器,会发现报错
意思是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;
刷新界面可以看到
这不行,router-view现在是写死的,必须要根据routes中的component来渲染出组件才行,修改一下router-view的代码
根组件的$options
怎么样可以在RouterView获取到对应的component呢,只要拿到当前的hash,找到routes对应的路由表就好了,怎么获取hash和routes呢
首先回顾一下,在new Router的时候,把routes传给了Router的构造函数
然后console.log(app);输出一下当前app实例和app下的所有children实例看看看看
还有其它的就不贴图了,发现了什么东西,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了
根据hash渲染
vue-router官方文档中router实例有一个currentRoute参数,代表着当前的路由对象
大家应该都清楚,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的含义
-
window.location.hash获取到的hash是带#的,把#去掉,没有的话默认是/首页,例如输入/#/aaa,那么获取到的是 /aaa
-
从routes中找到path等于当前hash的route复制给this.currentRoute
分别改变url的hash为 /#/home 和 /#/about 并按下回车,可以看到
修改一下router-view.js
export default {
render(h) {
const component = this.$router.currentRoute?.component || null;
return h(component);
},
};
再次改变一下hash并刷新界面,如下图
可以看到界面可以渲染出来,但是也遗留了3个问题
- hash发生变化时没有重新渲染
- 点击浏览器前进后退的时候也没有重新渲染
- 当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);
// ...
}
刷新浏览器
点击首页已经渲染成/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实例
}
}
刷新一下
onHashChange明明执行了,为什么没有重新渲染,因为this.currentRoute不是响应式的,重新赋值并不会更新dom,所以this.currentRoute必须是响应式对象
vue2.6 新增了一个api Vue.observable(object),用于将一个对象变成响应式对象
修改一下代码
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;
}
}
- this.currentRoute转换为响应式对象
- 首次加载和hash变化是触发currentRoute的更新
- 重新给currentRoute的path和component赋值,界面就会自动更新
效果如下
这里其实已经实现了简单的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;
}
- 如果缓存中有,直接从缓存读取
- 初次加载添加到缓存中
刷新后跟上面的效果是一样的
转载自:https://juejin.cn/post/7237306107747663933