热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从零开始实现一个vuerouter插件

一、回顾一下官方vue-router插件的使用要想自己实现一个vue-router插件,就必须先了解一下vue-router插件的基本使用,我们在使用vue-router的时候,通

一、回顾一下官方vue-router插件的使用

要想自己实现一个vue-router插件,就必须
先了解一下vue-router插件的基本使用,我们在使用vue-router的时候,通常会定义一个router.js文件,里面主要就是干了以下几件事:

引入vue-router模块

import Router from 'vue-router'

② 引入Vue并使用vue-router,即所谓的安装vue-router插件

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

创建路由对象并对外暴露
配置路由对象,即在创建路由对象的时候传递一个options参数,里面主要包括mode(路由模式)、routes(路由表)

export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
}
]
});

当然,还有最后一步就是在main.js中
引入router.js对外暴露的路由对象,并且将该路由对象
配置到Vue项目的根实例上,至此,就可以在项目中使用vue-router了,所谓使用路由,就是
在页面中使用组件来实现路由的跳转和跳转内容的显示。而组件这两个组件是
vue-router提供的
只有经过安装路由之后,才能识别这两个组件,否则会报错,提示
Unknown custom element: – did you register the component correctly?,因为vue-router内部会通过Vue.component()方法进行
全局注册这两个组件。

二、理解路由的两种模式

路由有两种模式,一种是
hash路由,即以
#号加路径名的方式,将路由添加到
浏览器url地址末尾;另一种就是
history路由,就是通过
浏览器提供的history api实现路由的跳转,可以实现直接将路由路径追加到
浏览器域名末尾。并且这两种方式,都是
路径变化,即
浏览器url地址变化,但是
页面不会跟着刷新,所以vue-router中要做的事,就是
监听浏览器路径即路由的变化,然后动态渲染对应的路由组件,可以通过
双向绑定机制实现,后面会具体讲。我们先来简单了解一下这两种路由的使用方式

① hash路由
// index.html

Home
About

当用户点击上面的这两个链接,就会在浏览器
url地址末尾追加上”#/home”或者”#/about”,但此时页面并没有刷新,所以我们要监听hash路由的变化,浏览器提供了一个hashchange事件,我们可以通过该事件实现路由显示区内容的变化

window.addEventListener("hashchange", () => {
content.innerHTML = location.hash.slice(1); // 监听到hash值变化后更新路由显示区内容
});

② history路由

history路由,要实现路由地址的变化,那么需要用到history提供的
pushState(state, title, path)方法,从该方法名可以知道,第一个参数为
state对象,
可以为null;第二个参数为
title字符串,即路由title,暂时无具体作用,后续可能会有用,
可以为null;第三个参数才是有用的,即
路由要跳转的路径字符串,虽然
前两个参数都可以为null,但是我们为了给State进行标识,可以传递一个state对象,
对象中包含title和path属性分别表示路由的名称和要跳转的路径,这样就可以通过
history.state知道当前路由的状态信息,是哪个路由了。

// index.html

Home
About

这里没有直接使用href=”/home”,因为路由的变化要通过pushState()实现,路由变化的时候才能监听到popstate事件,所以这里监听了click事件通过pushState()的方式去更改路由地址

function go(path) {
history.pushState({title:path.slice(1), path}, path.slice(1), path);
content.innerHTML = path;
}
window.addEventListener("popstate", () => { // 监听到路由变化后再次调go()方法更新路由显示区内容
go(location.pathname);
});

三、开始实现自己的vue-router路由插件

① 在路由插件中声明一个VueRouter类,因为我们使用vue-router的时候,是先引入路由插件,然后通过路由插件去new出一个router对象,所以引入的路由插件是一个类,同时在创建router对象的时候需要传递一个options参数配置对象,有moderoutes等属性配置,如:
// vue-router.js

class VueRouter { // VueRouter实际上是一个function,也可以看做是一个对象
constructor(options) {
this.mode = options.mode || '';
this.routes = options.routes || [];
}
static install(Vue) { // 添加一个静态的install方法,该方法执行的时候会传入Vue构造函数
// 这里主要是给所有Vue实例添加一些属性等操作
}
}
export default VueRouter;

上面在VueRouter类中添加了一个
静态的install方法,之所以是静态的,是因为我们使用路由插件的时候传递给Vue.use()方法的参数是导出的这个VueRouter类,而
use方法执行的时候会调用传递给其参数的install方法,即调用
VueRouter.install(),所以install是静态方法,所以如果路由插件导出的是一个对象,那么
这个对象上也必须要有一个install()方法

// 以下导出插件对象的方式也可以,比如,vuex就是导出的插件对象

class VueRouter {}
const install = () => {}
export default { // 导出插件对象
VueRouter,
install // 导出的插件对象中必须要有一个install方法
}

// 使用的时候也要进行相应的变化,如:

import rt from "./vue-router"
Vue.use(rt);
const router = new rt.VueRouter({});

② 创建路由表对象,我们要根据路径的变化动态渲染出相应的组件,所以为了方便,我们需要构造一个路由表对象,其属性为路径属性值为组件,这样当路径发生变化的时候,我们可以直接通过路由表对象获取到对应的组件并渲染出来

this.routesMap = this.createMap(this.routes); // 给VueRouter类添加一个routesMap属性,用于保存构建的路由表对象
createMap(routes) { // 通过传递进来的routes构建路由表对象
return routes.reduce((result, current) => {
result[current.path] = current.component;
return result;
}, {});
}

这里构造路由表对象使用到了reduce方法,reduce方法是一个用于
实现累加操作的方法,array.reduce(function(total, currentValue, currentIndex, arr), initialValue),当传入了initialValue,那么total就会等于initialValue的值,currentValue就是数组的第一个元素,接着下一轮循环,会把函数的返回值当做total传入,数组的第二个元素当做currentValue传入,一直循环直到数组元素遍历完毕。

保存当前路由,我们可以创建一个对象用于专门保存当前路由信息,我们只需要去更改当前路由信息,就可以动态渲染出相应的路由视图了,如:

class CurrentRoute { // 创建一个类,专门用于保存当前路由信息,同时方便在当前路由对象上添加属性和方法
constructor() {
this.path = null; // 添加一个path属性保存当前路由的路径
}
}
this.currentRoute = new CurrentRoute();// 给VueRouter添加一个currentRoute属性,保存当前路由信息对象

执行init()方法进行路由初始化,就是根据当前url地址进行判断,如果页面一加载什么路径都没有,那么就跳转到”/”首页,如果路径变化则保存当前路由信息,如:

init() { // 初始化路由信息
if (this.mode === "hash") { // 如果是hash路由
if (location.hash) { // 如果页面一加载的时候就有hash值
this.currentRoute.path = location.hash.slice(1); // 保存当前路由的路径
} else { // 如果页面一加载的时候没有hash值
location.hash = "/"; // 跳转到首页"/",即在url地址末尾添加"#/"
}
window.addEventListener("hashchange", () => { // 监听浏览器地址栏hash值变化
this.currentRoute.path = location.hash.slice(1); // hash改变,同样更新当前路由信息
});
} else { // 如果是history路由
if (location.pathname) { // 如果页面一加载的时候就有pathname值
this.currentRoute.path = location.pathname; // 保存当前路由的路径
} else { // 如果页面一加载的时候没有pathname值
location.pathname = "/"; // 跳转到首页"/",即在域名地址末尾添加"/"
}
window.addEventListener("popstate", () => { // 监听点击浏览器前进或后退按钮事件
this.currentRoute.path = location.pathname;
});
}
}

需要注意的是,
history.pushState()方法不会触发popstate事件
只有点击浏览器前进或后退按钮才会触发popstate事件,但是只要浏览器地址栏hash值变化就会触发hashchange事件

在每个Vue实例上都添加上$router和$route属性

我们在使用vue-router的时候,
每个实例上都可以通过this.$router和this.$route获取到对应的路由对象和当前路由对象,所以我们需要使用Vue的
mixin()方法在每个实例上混入$router和$route,我们在第①步的时候还遗留了install()方法的具体实现,我们可以在执行install()方法的时候
混入一个beforeCreate钩子函数,mixin混入的方法如果和vue实例上的方法
同名
并不会覆盖,而是将同名的方法
放到一个数组中,并且mixin中混入的方法在数组的最前面,即
mixin中混入的方法先执行,这样
每个实例创建的时候都会执行该beforeCreate(),那么我们可以在这里将$router和$route混入到每个实例上,如:

static install(Vue) { // install方法执行的时候会传入Vue构造函数
Vue.mixin({ // 调用Vue的mixin方法在每个Vue实例上添加一个beforeCreated钩子
beforeCreate () {
if (this.$options && this.$options.router) { // 如果是根组件,那么其options上就会有router属性
this._router = this.$options.router;
} else { // 非根组件
this._router = this.$parent && this.$parent._router; // 从其父组件上获取router
}
Object.defineProperty(this, "$router", { // 给每个Vue实例添加$router属性
get() { // 返回VueRouter实例对象
return this._router;
}
});
Object.defineProperty(this, "$route", { // 给每个Vue实例添加$route属性
get() {
return { // 返回一个包含当前路由信息的对象
path: this._router.currentRoute.path
}
}
});
}
});
}

在使用路由插件的时候,
会在根实例上注入一个router属性,所以如果this.$options有router的就是根实例,即
main.js中创建的那个Vue实例,由于Vue组件的创建顺序是
由外到内的,也就是说
根组件–>子组件 –> 孙组件 –>…,所以渲染的时候可以
依次从其父组件上获取到父组件上保存的_router实例,从而保存到当前组件的_router上。

注册router-link和router-view组件

我们需要在install()方法执行的同时,通过Vue在全局上注册router-link和router-view这两个组件,可以使用
Vue.component()方法全局注册。

static install(Vue) {
Vue.component("router-link", { // 全局注册router-link组件
props: {
to: {
type: String,
default: "/"
}
},
methods: {
handleClick(e) {
if (this._router.mode === "history") { // 如果是history路由
history.pushState({}, null, this.to); //通过pushState()方法更新浏览器地址栏路径,不会刷新页面
this._router.currentRoute.path = this.to; // 点击链接后更新当前路由路径
e.preventDefault(); // 阻止标签的默认行为,防y页面默认跳刷新s页面
}
}
},
render() {
const mode = this._router.mode; // 当前组件上也会注入_router属性从而可以获取到路由的mode
return
{this.$slots.default};
}
});
Vue.component("router-view", { // 全局注册router-view组件
render(h) {
const currentPath = this._router.currentRoute.path;
const routesMap = this._router.routesMap;
return h(routesMap[currentPath]); // 根据路由表传入当前路由路径,获取对应的组件,并渲染
}
});
}

上面给标签添加了一个click事件,因为当使用history路由的时候,标签上的href是一个路径,点击后会进行默认跳转到该路径,从而
会刷新页面,所以需要
阻止其默认跳转行为,并
通过history的api进行跳转

添加响应式路由

这里目前有一个问题,就是现在点击的链接,仅仅是浏览器地址栏url地址发生了变化,我们也可以看到点击链接后,
我们在handleClick事件函数中确实更新了当前路由的path,但是中的内容并没有跟着变化,因为这个当前路由currentRoute对象里面的数据并不是响应式的,所以当前路由变化,视图并不会跟着变化,要想让currentRoute对象中的数据变成响应式的,那么我们可以通过
Vue.util提供的一个defineReactive()方法,其可以给某个对象添加某个属性,并且
其属性值是响应式的,如:

static install(Vue) { // 在install方法中添加响应式路由,因为install方法会传入Vue
// 将当前路由对象定义为响应式数据
Vue.util.defineReactive(this, "current", this._router.currentRoute);
}

这样,this._router.currentRoute这个当前路由对象中的数据就
变成响应式的了,
组件的render()函数中使用到了this._router.currentRoute,所以其
render()函数就会再次执行从而动态渲染出当前路由

四、总结

至此,已经实现了一个简单的vue-router插件,其核心就是,
监听路由路径变化,然后动态渲染出对应的组件,其完整代码如下:

class CurrentRoute {
constructor() {
this.path = null;
}
}
class VueRouter { // VueRouter实际上是一个function,也可以看做是一个对象
constructor(options) {
this.mode = options.mode || '';
this.routes = options.routes || [];
this.routesMap = this.createMap(this.routes);
this.currentRoute = new CurrentRoute();
this.init();
}
init() {
if (this.mode === "hash") { // 如果是hash路由
if (location.hash) { // 如果页面一加载的时候就有hash值
this.currentRoute.path = location.hash.slice(1); // 保存当前路由的路径
} else { // 如果页面一加载的时候没有hash值
location.hash = "/"; // 跳转到首页"/",即在url地址末尾添加"#/"
}
window.addEventListener("hashchange", () => { //监听浏览器地址栏hash值变化
this.currentRoute.path = location.hash.slice(1); // hash改变,同样更新当前路由信息
});
} else { // 如果是history路由
if (location.pathname) { // 如果页面一加载的时候就有pathname值
this.currentRoute.path = location.pathname; // 保存当前路由的路径
} else { // 如果页面一加载的时候没有pathname值
location.pathname = "/"; // 跳转到首页"/",即在域名地址末尾添加"/"
}
window.addEventListener("popstate", () => { // 监听点击浏览器前进或后退按钮事件
this.currentRoute.path = location.pathname; // 如果页面一加载就带有pathname,那么就将路径保存到当前路由中
});
}
}
createMap(routes) {
return routes.reduce((result, current) => {
result[current.path] = current.component;
return result;
}, {});
}
// Vue的use方法会调用插件的install方法,也就是说,如果导出的插件是一个类,那么install就是类的静态方法
// 如果导出的是一个对象,那么install就是该对象的实例方法
static install(Vue) { // install方法执行的时候会传入Vue构造函数
Vue.mixin({ // 调用Vue的mixin方法在每个Vue实例上添加一个beforeCreated钩子
beforeCreate () {
if (this.$options && this.$options.router) { // 如果是根组件,那么其options上就会有router属性
this._router = this.$options.router;
} else { // 非根组件
this._router = this.$parent && this.$parent._router; // 从其父组件上获取router
}
// 将当前路由对象定义为响应式数据
Vue.util.defineReactive(this, "current", this._router.currentRoute);
Object.defineProperty(this, "$router", {
get() { // 返回VueRouter实例对象
return this._router;
}
});
Object.defineProperty(this, "$route", {
get() {
return { // 返回一个包含当前路由信息的对象
path: this._router.currentRoute.path
}
}
});
}
});
Vue.component("router-link", {
props: {
to: {
type: String,
default: "/"
}
},
methods: {
handleClick(e) {
if (this._router.mode === "history") { // 如果是history路由
history.pushState({}, null, this.to); //通过pushState()方法更路径,不会刷新页面
this._router.currentRoute.path = this.to; // 更新路径
e.preventDefault(); // 阻止
标签的默认行为,防y页面默认跳刷新s页面
}
}
},
render() {
const mode = this._router.mode; // 当前组件上也会注入_router属性从而可以获取到路由的mode
return
{this.$slots.default};
}
});
Vue.component("router-view", {
render(h) {
const currentPath = this._router.currentRoute.path;
// const currentPath = this.current.path;
const routesMap = this._router.routesMap;
return h(routesMap[currentPath]);
}
});
}
}
export default VueRouter;

推荐阅读
  • vue使用
    关键词: ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • 本文介绍了在Go语言中可见性与scope的规则,包括在函数内外声明的可见性、命名规范和命名风格,以及变量声明和短变量声明的语法。同时,还介绍了变量的生命周期,包括包级别变量和局部变量的生命周期,以及变量在堆和栈上分配的规则和逃逸分析的概念。 ... [详细]
  • 一、路由首先需要配置路由,就是点击good组件进入goodDetail组件配置路由如下{path:goodDetail,component:goodDetail}同时在good组件中写入如下点击事件,路由中加入 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 用Vue实现的Demo商品管理效果图及实现代码
    本文介绍了一个使用Vue实现的Demo商品管理的效果图及实现代码。 ... [详细]
  • 本文讨论了将HashRouter改为Router后,页面全部变为空白页且没有报错的问题。作者提到了在实际部署中需要在服务端进行配置以避免刷新404的问题,并分享了route/index.js中hash模式的配置。文章还提到了在vueJs项目中遇到过类似的问题。 ... [详细]
  • loader资源模块加载器webpack资源模块加载webpack内部(内部loader)默认只会处理javascript文件,也就是说它会把打包过程中所有遇到的 ... [详细]
  • Vue基础一、什么是Vue1.1概念Vue(读音vjuː,类似于view)是一套用于构建用户界面的渐进式JavaScript框架,与其它大型框架不 ... [详细]
  • 1.脚本功能1)自动替换jar包中的配置文件。2)自动备份老版本的Jar包3)自动判断是初次启动还是更新服务2.脚本准备进入ho ... [详细]
  • Allegro总结:1.防焊层(SolderMask):又称绿油层,PCB非布线层,用于制成丝网印板,将不需要焊接的地方涂上防焊剂.在防焊层上预留的焊盘大小要比实际的焊盘大一些,其差值一般 ... [详细]
author-avatar
星空下的舞者j
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有