Vue@2.X --- 基于mixin,observable实现router-view和router-link(页面切换,组件切换的插件)

127 阅读4分钟

先来几个问题吧,

基础全局 options 是什么?

基础 options 就是:components、directives、filters 三兄弟,这三兄弟在初始化全局 API 的时候就设置在 Vue.options 上。所以这三个是最先存在全局 options。

那options里面还有什么呢?

挂载点:el 数据:data 模板:template 方法:methods生命周期[...] 监听器watch... 更多移步 官方文档

如果给选项配置对象传入一个自定义配置,我们如何在实例上获取它们?

我们可以通过实例方法获取

vm.$options[自定义的key]  获取

当我们的项目越来越大,可能会发现在相似的组件里一遍又一遍的在复制粘贴相同的代码段(data,method,watcher等)。当然,也可以把相似的组件写成一个组件,然后用props来定制它,但是使用太多的props很容易导致混乱。

  不过,我们还有一种解决方案,那就是Mixin(混入)。

正文

一、Mixin介绍

Mixin是什么

  Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能,Mixin对象能够使用组件的任何属性(data,mounted,created,update等),而且当组件使用Mixin时,这个Mixin的所有信息也会混合到这个组件里,这个组件就能够访问到所有Mixin的属性就像声明在自己对象中一样

这时,可以通过Vue的mixin功能将相同或者相似的代码提出来。如一些逻辑相同弹框,将其是否展示的逻辑抽离出来。或者当我们需要全局去注入一些methods, filter或者hooks时我们就可以使用mixin来做。

特性:全局和局部 对每一个选项配置的混入

混入的作用:

  • 将混入配置项和组件配置项进行合并

混入类型:全局混入,局部混入

  • 全局混入指的是对所有组件的配置进行合并
       //写在vue实例创建之前
       Vue.mixin()
    

image.png

  • 局部混入指的是对当前组件的配置进行合并
  •      //写在vue实例options内
         mixins:[{}]
    

image.png

  • 多个混入对象同时执行,不会覆盖

什么场景使用?

当你的逻辑需要多个组件复用,但是逻辑中需要使用多个data methods 生命周期的时候就需要混入

混入的特性?

  • data/computed/methods/filter/components配置项不同名属性合并,同名属性以组件自己的为准 生命周期函数是将所有的逻辑全部合并执行

mixin的规则

点我看看

定义了一个全局的混入配置
vue.mixin( {
data() {
return { msg: 'hello,mixin!' };},
});
导入全局mixin并渲染到页面
<template>
  <div class="App">
    <h1> App{{ msg }}</h1>
  </div>
</template>

<script>
export default {
 }
</script>

<style>
</style>

实现router-view和vue-link

基于此我们终于可以步入今天的正题啦,简单实现一下页面,组件切换的插件啦~~~ 其实就是router-view和router-link的底层逻辑

那么,在实现这个功能是我们将会用到哪些知识点呢?
  • 动态组件 components
  • Vue.mixin
  • Vue.use
  • vue.observable
  • vue.components
  • Location.hash
  • 路由系统 ---> (hash值对应组件渲染)
  • 通过hash值变化动态渲染组件
  • class语法

部分知识点将以注释的形式列出哦!!!

先贴出demo目录树

image.png

❀❀开始❀❀ --->

  1. 在项目根目录下新建vue-router文件夹,里面新建index.js
  2. 在根目录main.js里面引入该文件index.js,并使用vue.use关于use的使用点这里 vue.use 将其注册为全局组件,我们还应该实例化一个路由对象,该对象有其配置项,配置项是hash值与自己写的组件之间的对应关系,最后我们将其实例化对象注册到全局根vue实例的options里,前面说,我们的自定义配置项会保存在vm.options里面,可以通过vm.options.[自定义的key] 访问到 那么我们在根目录[main.js]下的任务就写完了,代码如下:

import Vue from 'vue/dist/vue';
import App from '@/App.vue';

//1.全局引入路由文件
import VueRouter from './vue-router'; 

// 2.通过全局方法 Vue.use() 使用插件,并且 VueRouter如果是对象就必须存在一个静态方法 install,它的参数就是我们的vue
Vue.use(VueRouter); 

// 3.实例化路由对象--传hash值与页面组件对应关系
const router = new VueRouter({
  '/': { template: '<h1>/页面</h1>' },
  '/a': { template: '<h1>A页面</h1>' },
  '/b': { template: '<h1>B页面</h1>' },
  '/c': { template: '<h1>C页面</h1>' },
});

const vm = new Vue({
  el: '#app',
  // 4.将router实例注入到vm.$options中以便后续操作
  router,
  template: '<App />',
  components: { App },
});

  1. 接着我们返回到刚刚index.js中,在里面导出一个定义路由的构造函数的类 [该类利用hash值的变化动态渲染组件] 并为其注册一个静态方法install,此时install里的参数就是该vue实例,我们后续还会用到很多次,避免每次都导入,那么我们可以将其保存下来,let _Vue = null将全局Vue实例挂载到该类,_vue = Vue以便以后每一个组件路由都有调用Vue实例的权力
  2. 这里我们就会在install方法里面用到我们的mixin,将该vue实例全局混入,那么它成功后会对后续每一个组件都都执行该方法,其中我们知道this在vue中,如果不限制组件,将指向当前组件,而我们这里的this在根实例执行,所以this指向Vue,接着beforeCreate函数,
  3. 然而当前的mixin是添加在根实例中的全局混入,我们又希望beforeCreate函数只在根实例中执行,所以,我们要判断!this.$parent,当一个组件没有父组件时,它就一定是根组件,这时,实例化的options中的东西就又可以在构造函数中拿到。
  4. 定义一个初始化方法init,并在beforeCreate中调用该方法 代码如下:

let _Vue = null; //声明储存全局Vue实例的变量
// 定义路由的构造函数的类---利用hash值的变化动态渲染组件
export default class VueRouter {
  // 7.构造器接收实例化的路由对象
  constructor(options) {
      this.name = 'vue-router'
  }
  // 8. 定义init方法 执行初始化
  init() {
    console.log('init执行了');
  }
  // 3. 必须存在一个静态方法 install
  static install(Vue) {
    // 4.将全局Vue实例挂载到该类,以便以后每一个组件路由都有调用Vue实例的权力
    _Vue = Vue;
    // 5.全局混入
    Vue.mixin({
      beforeCreate() {
        //此处this指向Vue实例
        // console.log('--------------', this);
        // 6.确保只在根实例中执行
        if (!this.$parent) {
         // console.log(this.$options.router);
         // 9. 调用init方法
         this.$options.router.init()
        }
      },
    });
  }
}

  1. 因为我们在main.js中实例化该类时传了路由关系的参数,所以我们要在当前类中接受这组参数options,那么当前默认渲染根组件,此时,细心的你如果打印location.hash,就会发现它是一个空值,因为我们的页面并没有发生变化,那么对应的hash值也将不会发生变化。

  2. 如果想要我们的页面发生变化,我们就需要获取到hash值,并且注册hashChange事件,并且,将当前渲染页面的hash值保管起来,当hash值变化后,将变化后的hash值赋值给保存的当前路径,修改hash,页面同步更改,

  3. 修改后的地址变化image.png

  4. 此时要想同步改变页面,我们还需要写一个动态组件的方法initComponents并在组件中注册一个叫做router-viewvue.components, 代码如下:


let _Vue = null; //声明储存全局Vue实例的变量

// 定义路由的构造函数的类---利用hash值的变化动态渲染组件
export default class VueRouter {
  // 7.构造器接收实例化的路由对象
  constructor(options) {
    this.options = options;
    // 12.将当前页面hash保存起来,Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象
    this.current = _Vue.observable({ path: '' });
  }
  // 8. 定义init方法 执行初始化
  init() {
    // 15.初始化组件
    this.initComponents();
    // 10.调用初始化事件
    this.initEvent();
  }
  // 9.定义一个 window.onhashchange 监听函数,
  initEvent() {
    // 11.定义hashchange事件
    window.onhashchange = () => {
      console.log(window.location.hash);
      // 13.将变化后的hash赋值给当前路径中并去除#
      this.current.path = window.location.hash.substring(1);
    };
    // 14.由默认的页面中的hash变化
    window.location.hash = '/';
  }
  // 16. 注册两个路由实例
  initComponents() {
    console.log('---------', this);
    // 18.保存当前this
    const _this = this; //this指向VueRouter实例
    // 17.注册渲染当前页面组件
    _Vue.component('router-view', {
      template: '<component :is="currentView" />',
      computed: {
        currentView() {
          // 传过来的路径[/]
          return _this.options[_this.current.path];
        },
      },
    });
  }
  // 3. 必须存在一个静态方法 install
  static install(Vue) {
    // 4.将全局Vue实例挂载到该类,以便以后每一个组件路由都有调用Vue实例的权力
    _Vue = Vue;
    // 5.全局混入
    Vue.mixin({
      beforeCreate() {
        //此处this指向Vue实例
        // console.log('--------------', this);
        // 6.确保只在根实例中执行
        if (!this.$parent) {
          // 将router的实例挂载到vue的原型上,每个vue实例都可以访问到
          _Vue.prototype.$router = this.$options.router;
          // console.log('++++', this.$options.router); //此处是挂载到Vue实例上的路由实例
          // 8.调用路由实例上的init方法
          this.$options.router.init();
        }
      },
    });
  }
}


  1. 此时我们就注册好了一个全局组件,我们可以在任何地方直接使用它,例如我们在App.vue,
<template>
  <div class="App">
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  
};
</script>

<style>
</style>

示图: image.png image.png 12. 当然这给只能在地址栏输入渲染新页面,显然不能满足我们,那我们还需要一个类似于a标签点击跳转的功能,

  1. 我们就还需要注册一个router-link的全局组件
 //14.注册点击跳转的组件
    _Vue.component('router-link', {
      template: '<a :href="`#${to}`"> <slot /></a>',
      props: {
        to: {
          type: String,
          required: true,
        },
      },
    });

那么在页面中这么用

<template>
  <div class="App">
    <router-view></router-view>
    <router-link to="/c">去c页面</router-link>
    <router-link to="/a">去A页面</router-link>
  </div>
</template>

  1. 那我希望用普通的button也可以跳转呢?其实都写到这了,也不难,只需要在刚刚我们定义的类里添加自定义的push方法,让当前路径等于点击后传过来的路径即可😀
  2. 将router的实例挂载到vue的原型上,每个vue实例都可以访问到 App的button按钮点击跳转
<template>
  <div class="App">
    <router-view></router-view>
    <router-link to="/c">去c页面</router-link>
    <router-link to="/a">去A页面</router-link>
    <button @click="toB">去B页面</button>
  </div>
</template>

<script>
import Test from './components/test.vue';
export default {

  methods: {
    toB() {
      this.$router.push('/b');
    },
  },
 
 
};
</script>

<style>
</style>

push方法的逻辑---15

  // 15.让当前路径==传过来的路径
  push(path) {
    this.current.path = path;
  }

将router的属性挂载到所有vue原型上---16

 // 5.全局混入
    Vue.mixin({
      beforeCreate() {
        //此处this指向Vue实例
        // console.log('--------------', this);
        // 6.确保只在根实例中执行
        if (!this.$parent) {
          // 16.将router的实例挂载到vue的原型上,每个vue实例都可以访问到
          _Vue.prototype.$router = this.$options.router;
          // 8.调用路由实例上的init方法
          this.$options.router.init();
        }
      },
    });

到这里,我们的router-view与router-link的底层逻辑就实现完了,想要了解更多,请移步官方文档😀😀😀

结束❀❀

每天的学习都是痛苦,煎熬与恍然一悟,惊喜的过程,虽然很难,但坚持下去,结局总不会太差!!! 加油,林寒!!