博文分享

这篇文章你可以学习到:

  • 实现一个自己的vue-router

  • 了解什么是Vue的插件

学习b站大佬后做的笔记整理和源码实现

1.1.3一步一步带你弄懂vue-router核心原理及实现哔哩哔哩bilibili

使用官方的Vue-router

通过vue-cli脚手架初始化一个项目

下载vue-router

ps: vue-cli脚手架生成的时候可以选择:是否安装vue-router

下面是手动安装过程:

  • 就是npm install vue-router之后,通过import引入了

  • 然后通过Vue.use() 引入

  • 之后定义一个路由表routes

  • 然后new VueRouter 就可以得到一个实例

  • 新建了Home和About两个组件

得到代码:

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import About from '@/components/about'

Vue.use(Router)

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

导入到main.js中

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})

在new Vue添加这个配置项

使用router-link和router-view

App.vue

<template>
<div id="app">
<router-link to="/">home</router-link>
<router-link to="/about">about</router-link>
<router-view/>
</div>
</template>

效果:

自己写一个vue-router

老规矩,先上源码

没注释版本:

let Vue;
class VueRouter {
constructor(options) {
this.$options = options;
let initial = window.location.hash.slice(1) || "/";
Vue.util.defineReactive(this, "current", initial);
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1) || "/";
})
}
}

VueRouter.install = (_Vue) => {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
}); Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
}
},
render(h) {
return h("a",
{
attrs: {
href: `#${this.to}`
},
},
this.$slots.default
);
}
});
Vue.component("router-view", {
render(h) {
let component = null; const current = this.$router.current;

const route = this.$router.$options.routes.find(
(route) => route.path === current
)

if (route) component = route.component;

return h(component);
}
})
}
export default VueRouter;

有个人注释版本:

// 1、实现一个插件
// 2、两个组件

// Vue插件怎么写
// 插件要么是function 要么就是 对象
// 要求插件必须要实现一个install方法,将来被vue调用的
let Vue; // 保存Vue的构造函数,在插件中要使用
class VueRouter {
constructor(options) {
this.$options = options;
// 只有把current变成响应式数据之后,才可以修改之后重新执行router-view中的render渲染函数的
let initial = window.location.hash.slice(1) || "/";
Vue.util.defineReactive(this, "current", initial);

window.addEventListener("hashchange", () => {
// 获取#后面的东西
this.current = window.location.hash.slice(1) || "/";
})
}
}

VueRouter.install = (_Vue) => {
Vue = _Vue;

// 1、挂载$router属性(这个获取不到router/index.js中new 出来的VueRouter实例对象,
// 因为Vue.use要更快指向,所以就在main.js中引入router,才能使用的
// this.$router.push()
// 全局混入(延迟下面的逻辑到router创建完毕并且附加到选项上时才执行)
Vue.mixin({
beforeCreate() {
// 注意此钩子在每个组件创建实例的时候都会被调用
// 判断根实例是否有该选项
if (this.$options.router) {
/**
* 因为每一个Vue的组件实例,都会继承Vue.prototype上面的方法,所以这样就可以
* 在每一个组件里面都可以通过this.$router来访问到router/index.js中初始化的new VueRouter实例了
*/
Vue.prototype.$router = this.$options.router;
}
}
}); // 实现两个组件:router-link、router-view
// <router-link to="/">Hone</router-link> 所以我们要把这个router-link标签转换成:<a href="/">Home</a>
/**
* 第二个参数其实是一个template,也就是一个渲染组件dom
* 我们这里使用的是渲染函数,也就是返回一个虚拟DOM
*/
Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
}
},
render(h) {
return h("a",
{
attrs: {
// 为了不重新更新页面,这里通过锚点
href: `#${this.to}`
},
},
// 如果要获取Home的话,可以是下面这样
this.$slots.default
);
}
});
Vue.component("router-view", {
render(h) {
let component = null;

// 由于上面通过混入拿到了this.$router了,所以就可以获取当前路由所对应的组件并将其渲染出来
const current = this.$router.current;

const route = this.$router.$options.routes.find(
(route) => route.path === current
)

if (route) component = route.component;

return h(component);
}
})
}
export default VueRouter;

一步一步分析——从零开始

首先,有几个问题

问题一:

router/index.js中

import Router from 'vue-router'

Vue.use(Router)

我们知道,通过Vue.use( ) 是个Vue引入了一个插件

那么这个插件vue-router 内部做了什么?

问题二:

router/index.js中

import Router from 'vue-router'

export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
})
  • 初始化了一个引入的vue-router插件对象

  • 括号里面传入的是一个{ } 对象,其实就是一个配置项

    • 配置项里面包含了一个routes路由表

之后在main.js中

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})

在new Vue实例的时候,把导出的router作为了配置项传入,这个又是为什么?

问题三:router-link 和 router-view

  • 在组件中使用router-link组件实现路由跳转

  • 使用router-view组件作为路由的出口

那么,这两个组件内部是怎么样实现的呢?

为什么,其他组件都是要在Component里面声明才可以使用的,但是这两个组件直接使用,就说明这两个组件肯定在某个地方进行了全局注册

拓展:大概的思路:

其实在jquery中是这样实现:就是监听当前哈希值hash的变换 或者是 history的变化,就可以得到一个触发的事件,然后就可以拿到当前的地址了(就是要跳转的地址),然后通过这个地址,就可以到我们router/index.js中定义的路由表,也就是匹配path,得到component,这样就可以拿到组件了,然后就要拿到真实的DOM,,然后追加到我们的router-view里面,也就是把之前的router-view里面的内容清空掉,然后把最新的DOM压入到router-view中进行显示的,这个就是一个很典型的dom操作

但是vue中有一个新东西:Vue的响应式原理,所以就可以用响应式来监听路由的变化

什么是Vue的插件

学习自:深入理解Vue的插件机制与install详细vue.js脚本之家 (jb51.net)

  • 插件内部为什么要实现一个install方法

vue的插件应该暴露出一个install方法,这个方法的e第一个参数是Vue构造器,第二个参数是一个可选的选项对象——这个是Vue官方对Vue插件的规范,

install函数可以做些什么?

install内部怎么实现的?

插件在install中到底做了什么?

经典三连问~

install在vue-router等插件中的处理

抛出问题:

  1. 为什么在项目中可以直接使用 $router 来获取其中的值以及一些方法

  2. 为什么这些插件都要先用Vue.use 引入,然后才创建实例,并且之后在Vue实例中引入

使用vue-router举例

class Router {
constructor(options) {
...
}
}

Router.install = function(_Vue) {

_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})

}

export default Router;
  • _Vue.mixin全局混入是什么呢?相当于在所有的组件中混入这个方法;

  • beforeCreate是什么呢?当然是Vue的一个生命周期,在create之前执行;

所以:

  1. Vue-Router是在install函数使用了一个全局混入,在beforeCreate生命周期触发的时候把this.$option.router挂载到Vue的原型上了,那么这样就可以使用this.$router来调用router实例啦

  2. 那么this.$options.router又是什么

    • 全局混入中的this.$options是我们在 在main.js中 new Vue({})的时候 { } 大括号里面传入的配置项,所以我们main.js传入的router,在这里就可以通过this.$options.router来获取到我们在router/index.js中new的vue-router实例了

    为什么要这样设计:因为在router/index.js中

    import Vue from 'vue'
    import Router from 'vue-router'
    import Home from '@/components/home'
    import About from '@/components/about'

    Vue.use(Router)

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

    是先执行了Vue.use 之后再进行new vue-router对象的操作,所以如果要在插件的install中使用到这个vue-router实例的话,就要把实例传入到main.js的new Vue({})配置项里面,这样的话,我们就可以用依赖注入的方式,把new Router({})里面定义的路由表获取到了,

    我们把 Vue.prototype.$router = this.$options.router; 所以其他组件就可以通过this.$router获取访问到我们定义的路由表了,所以为什么可以用this.$router.push()添加路由,一部分的原因就是,this.$router路由表是一个数组,所以可以通过push操作的

  • Vue.use的时候主要调用了 插件内部的install方法,并把Vue实例作为了参数进行传入

插件install在vue中的内部实现

下面是Vue.use的源码

export function initUse (Vue: GlobalAPI) {
// 注册一个挂载在实例上的use方法
Vue.use = function (plugin: Function | Object) {
// 初始化当前插件的数组
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 如果这个插件已经被注册过了,那就不作处理
if (installedPlugins.indexOf(plugin) > -1) {

return this

}

... // 重点来了哦!!!
if (typeof plugin.install === 'function') {
// 当插件中install是一个函数的时候,调用install方法,指向插件,并把一众参数传入
plugin.install.apply(plugin, args)

} else if (typeof plugin === 'function') {
// 当插件本身就是一个函数的时候,把它当做install方法,指向插件,并把一众参数传入
plugin.apply(null, args)

} // 将插件放入插件数组中
installedPlugins.push(plugin)

return this
}
}

看到这里大家对插件应该有了一定的认识了,坚持!!

开始实现

  • 首先:因为router/index 初始化了插件的实例,所以该插件可以用一个class表示,并且还要实现一个install方法

class VueRouter {

}

VueRouter.install = (_Vue) => {

}

上面也说了,插件的install方法,第一个参数就是Vue实例本身

优化

后面其他地方也要用到vue实例的,所以我们就在插件声明一个全局的vue,用来保存这个传入的vue实例

并且:也是一个保证插件和vue的独立性,有了这个操作之后,当我们打包该插件的时候,就不会把vue也打包到插件了

并且把从new Vue({router})的配置项router,挂载到Vue实例原型对象上

let Vue;
class VueRouter {

}

VueRouter.install = (_Vue) => {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
})
}

不仅如此,我们还在install函数中,实现了两个组件 router-link 和 router-view

原理:

<router-link to="/">Home</router-link> 所以我们要把这个router-link标签转换成:Home

  • 接收一个to属性

  • 并且返回的是一个render渲染函数,也就是返回一个虚拟DOM

那么怎么获得router-link中间的文字Home呢?

拓展:Vue.$slots

所以因为router-link里面只有home文字,所以可以直接通过 vue.$slots.default获取即可了

let Vue;
class VueRouter {

}

VueRouter.install = (_Vue) => {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
});

Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
}
},
render(h) {
return h("a",
{
attrs: {
// 为了不重新更新页面,这里通过锚点
href: `#${this.to}`
},
},
// 如果要获取Home的话,可以是下面这样
this.$slots.default
);
}
});
}

上面就是router-link具体实现了

下面是router-view实现

原理:获取到当前路由,并从路由表找到对应的component并进行渲染

注意:我们在install方法中,通过全局混入,把在router/index.js中实例化的vue-router实例,挂载到了vue原型对象上的$router上了

  • 那么:我们就可以在组件中通过this.$router来获取到我们的实例化组件

下面就要实现:该插件的类class怎么实现

我们在router/index.js中,通过

new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
})

传入了一个路由表,作为这个插件实例的配置项

所以就可以在该类的构造函数中,通过参数获取到这个配置项了,为了可以在其他组件中获取到路由表,我们把配置项挂载到该类本身

class VueRouter {
constructor(options) {
this.$options = options
}
}

为什么要这样做?

这样的话,在router-view这些组件中

就可以通过 this.$router.$options访问到我们在router/index里面new的vue-router类中传入的配置项里面的路由表了

class VueRouter {
constructor(options) {
this.$options = options
this.current = window.location.hash.slice(1) || "/";
window.addEventListener("hashchange", () => {
// 获取#后面的东西
this.current = window.location.hash.slice(1) || "/";
})
}
}

  

初始化current,并通过onhashchange来监听路由的变化,并赋值给current

通过slice(1)是为了获取到#后面的值

这样的话,就可以实现router-view组件了

let Vue;
class VueRouter {
constructor(options) {
this.$options = options
this.current = window.location.hash.slice(1) || "/";
window.addEventListener("hashchange", () => {
// 获取#后面的东西
this.current = window.location.hash.slice(1) || "/";
})
}
}

VueRouter.install = (_Vue) => {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
});

Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
}
},
render(h) {
return h("a",
{
attrs: {
// 为了不重新更新页面,这里通过锚点
href: `#${this.to}`
},
},
// 如果要获取Home的话,可以是下面这样
this.$slots.default
);
}
});
Vue.component("router-view", {
render(h) {
let component = null;

// 由于上面通过混入拿到了this.$router了,所以就可以获取当前路由所对应的组件并将其渲染出来
const current = this.$router.current;

const route = this.$router.$options.routes.find(
(route) => route.path === current
)

if (route) component = route.component;

return h(component);
}
})
}

所以目前代码是这样的

但是,我们可以发现current改变了,router-view不变,这是因为此时的current并不是一个响应式数据,所以current变化的时候,router-view里面的render函数并不会再次执行并重新渲染

所以下面就要对class类里面的current变成是响应式数据了

拓展:Vue.util.defineReactive

Vue.util.defineReactive(obj,key,value,fn)

obj: 目标对象,

key: 目标对象属性;

value: 属性值

fn: 只在node调试环境下set时调用

其实底层就是一个Object.defineProperty()

依赖通过dep收集,通过Observer类,添加ob属性

class VueRouter {
constructor(options) {
this.$options = options;
// 只有把current变成响应式数据之后,才可以修改之后重新执行router-view中的render渲染函数的
let initial = window.location.hash.slice(1) || "/";
Vue.util.defineReactive(this, "current", initial);

window.addEventListener("hashchange", () => {
// 获取#后面的东西
this.current = window.location.hash.slice(1) || "/";
})
}
}

所以完整代码就是:

// 1、实现一个插件
// 2、两个组件

// Vue插件怎么写
// 插件要么是function 要么就是 对象
// 要求插件必须要实现一个install方法,将来被vue调用的
let Vue; // 保存Vue的构造函数,在插件中要使用
class VueRouter {
constructor(options) {
this.$options = options;
// 只有把current变成响应式数据之后,才可以修改之后重新执行router-view中的render渲染函数的
let initial = window.location.hash.slice(1) || "/";
Vue.util.defineReactive(this, "current", initial);

window.addEventListener("hashchange", () => {
// 获取#后面的东西
this.current = window.location.hash.slice(1) || "/";
})
}
}

VueRouter.install = (_Vue) => {
Vue = _Vue;

// 1、挂载$router属性(这个获取不到router/index.js中new 出来的VueRouter实例对象,
// 因为Vue.use要更快指向,所以就在main.js中引入router,才能使用的
// this.$router.push()
// 全局混入(延迟下面的逻辑到router创建完毕并且附加到选项上时才执行)
Vue.mixin({
beforeCreate() {
// 注意此钩子在每个组件创建实例的时候都会被调用
// 判断根实例是否有该选项
if (this.$options.router) {
/**
* 因为每一个Vue的组件实例,都会继承Vue.prototype上面的方法,所以这样就可以
* 在每一个组件里面都可以通过this.$router来访问到router/index.js中初始化的new VueRouter实例了
*/
Vue.prototype.$router = this.$options.router;
}
}
}); // 实现两个组件:router-link、router-view
// <router-link to="/">Hone</router-link> 所以我们要把这个router-link标签转换成:<a href="/">Home</a>
/**
* 第二个参数其实是一个template,也就是一个渲染组件dom
* 我们这里使用的是渲染函数,也就是返回一个虚拟DOM
*/
Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
}
},
render(h) {
return h("a",
{
attrs: {
// 为了不重新更新页面,这里通过锚点
href: `#${this.to}`
},
},
// 如果要获取Home的话,可以是下面这样
this.$slots.default
);
}
});
Vue.component("router-view", {
render(h) {
let component = null;

// 由于上面通过混入拿到了this.$router了,所以就可以获取当前路由所对应的组件并将其渲染出来
const current = this.$router.current;

const route = this.$router.$options.routes.find(
(route) => route.path === current
)

if (route) component = route.component;

return h(component);
}
})
}
export default VueRouter;

后面的一些优化,比如通过mode来改变模式(history、hash)上面是默认用了hash的,还有就是路由拦截器这些。

手写vue-router & 什么是Vue插件的更多相关文章

  1. 手写@koa/router源码

    上一篇文章我们讲了Koa的基本架构,可以看到Koa的基本架构只有中间件内核,并没有其他功能,路由功能也没有.要实现路由功能我们必须引入第三方中间件,本文要讲的路由中间件是@koa/router,这个中 ...

  2. 手写一个超简单的Vue

    基本结构 这里我根据自己的理解模仿了Vue的单文件写法,通过给Vue.createApp传入参数再挂载元素来实现页面与数据的互动. 其中理解不免有错,希望大佬轻喷. 收集数据 这里将Vue.creat ...

  3. 不再手写import - VSCode自动引入Vue组件和Js模块

    :first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdow ...

  4. 自己手写简约实用的Jquery tabs插件(基于bootstrap环境)

    一直想改版网站首页的图书展示部分,以前的展示是使用BootStrap的传统的collapse,网页篇幅占用大,也不够美观,操作也相对来说比较麻烦.于是有了自己利用Jquery来做一个图书展示的tabs ...

  5. 一阶段项目 总结 之 两张图片对比 手写 jquery 也可以使用beer slider 插件

    <!DOCTYPE html><html> <head>  <meta charset="utf-8">  <title> ...

  6. 8. Vue - Router

    一.Vue Router 的使用 JavaScript: 1.创建组件:创建单页面应用需要渲染的组件 2.创建路由:创建VueRouter实例 3.映射路由:调用VueRouter实例的map方法 4 ...

  7. vue router 只需要这么几步

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  8. vue router & query params

    vue router & query params vue router get params from url https://zzk.cnblogs.com/my/s/blogpost-p ...

  9. 手写koa-static源码,深入理解静态服务器原理

    这篇文章继续前面的Koa源码系列,这个系列已经有两篇文章了: 第一篇讲解了Koa的核心架构和源码:手写Koa.js源码 第二篇讲解了@koa/router的架构和源码:手写@koa/router源码 ...

  10. 基于vue手写tree插件那点事

    目录 iview提供的控件 手写控件 手写控件扩展 手写控件总结 # 加入战队 微信公众号 主题 Tree树形控件在前端开发中必不可少,对于数据的展示现在网站大都采取树形展示.因为大数据全部展示出来对 ...

随机推荐

  1. [第一篇]——Docker 教程之Spring Cloud直播商城 b2b2c电子商务技术总结

    Docker 教程 Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源. Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然 ...

  2. Apollo 配置中心详细教程

    一.简介 Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性,适用于微服务配置管理 ...

  3. CodeForce-792B Counting-out Rhyme(模拟)

    Counting-out Rhyme CodeForces - 792B 题意: n 个孩子在玩一个游戏. 孩子们站成一圈,按照顺时针顺序分别被标号为 1 到 n.开始游戏时,第一个孩子成为领导. 游 ...

  4. 设置自启动nginx(适用于其他软件)(LinuxDeploy里的Ubuntu)

    LinuxDeploy里的Ubuntu自启动nginx(适用于其他软件) 网上的教程是这样的,基本能用 1.编写脚本(这个文件及其内容安装Nginx后自动生成,没有的话内容自己Google) $ su ...

  5. 判断手机浏览器还是微信浏览器(PHP)

    //判断是否 微信浏览器 function isWeixin1() { if (strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== fa ...

  6. Python return self

    在Python中,return self的作用为: Returning self from a method simply means that your method returns a refer ...

  7. python实现rtsp取流并截图

    import cv2 def get_img_from_camera_net(folder_path): cap = cv2.VideoCapture("rtsp://admin:admin ...

  8. Linux命令行:监视系统IO、内存、CPU、GPU

    [监视IO] Linux18.04自带工具sysstat,其中的iostat可以用于观察IO情况.Linux16.04系统没有默认安装,需要手动安装,安装sysstat时需要下载新的内核(我的原本是1 ...

  9. HttpClient遭遇Connection Reset异常,如何正确配置?

    最近工作中使用的HttpClient工具遇到的Connection Reset异常.在客户端和服务端配置不对的时候容易出现问题,下面就是记录一下如何解决这个问题的过程. 出现Connection Re ...

  10. NOIP模拟72

    T1 出了个大阴间题 解题思路 看了看数据,大概是个状压 DP,但是感觉记忆化搜索比较好写一点(然而并不是这样递归比迭代常熟大了许多..) 不难判断出来 b 的数值与合并的顺序无关于是我们可以预先处理 ...