20230703----重返学习-组件封装-Vue.extend()语法-创建一个使用函数来调用的组件-组件和插件的问题-vuex-Vue.mixin()

315 阅读6分钟

day-104-one-hundred-and-four-20230703-组件封装-Vue.extend()语法-创建一个使用函数来调用的组件-组件和插件的问题-vuex-Vue.mixin()

组件封装

  • 技术角度:

    1. 视图复杂使用jsx语法。

    2. 视图简单使用template语法。

      • fang/f20230703/day0703/src/components/Toast.vue

        <script>
        export default {
          name: "Toast",
          // 注册接收属性。
          props: {
            // text就是传递的提示消息信息;
            text: {
              type: String,
              default: "",
            },
            // 过多久后消失(单位毫秒)。
            time: {
              type: [Number, String],
              default: 2000,
            },
          },
          data() {
            return {};
          },
          // 第一次渲染完:设置定时器,到达指定时间后,把组件销毁。
          mounted() {
            let time = +this.time;
            if (isNaN(time)) {
              time = 2000;
            }
            this.destoryTimer = setTimeout(() => {
              this.$destroy();
              clearTimeout(this.destoryTimer);
            }, time);
          },
          // 销毁后:把真实的DOM从视图中移除掉。
          destroyed() {
            clearTimeout(this.destoryTimer);
            console.log("this", this);
            let ele = this.$el;
            ele.parentNode.removeChild(ele);
          },
          methods: {},
        };
        </script>
        
        <template>
          <div class="toast-box">
            {{ this.text }}
          </div>
        </template>
        
        <style lang="less" scoped>
        
  • 组件调用:

    1. template中标签语法。

      • fang/f20230703/day0703/src/views/Demo.vue

        <template>
          <div class="demo-box">
            <ToastVue>
                <div>
                    <p>3. 内部会通过 new ToastVueCtor(....);</p>
                    <p></p>
                </div>
            </ToastVue>
          </div>
        </template>
        
        <script>
        import ToastVue from "../components/Toast.vue"; //1. 导入一个组件的OptionsAPI。
        export default {
          components: {
            ToastVue, //2. const ToastVueCtor = Vue.extend(ToastVue)。创建Vue2类的一个实例。
          },
        };
        </script>
        
    2. 使用函数的方式去调用。

创建一个使用函数来调用的组件

  • 在Vue2中,想要渲染一个组件,我们只需要创建此类的一个实例即可。

  • 基于ES6Module导入的组件,不是一个类,只是这个组件的描述

    import Toast from '@/components/Toast.vue'
    console.log(Toast)
    
    • 如果我们打算new Toast()会报错;
  • 步骤:

    1. 导入组件的OptionsAPI。

    2. 基于函数执行渲染组件,得到一个Vue子类的构造函数。

      const ToastCtor = Vue.extend(Toast)
      //  - Vue.extend()中传递的是OptionsAPI(或导入的组件)。
      //  - Vue.extend()是创建Vue类的一个子类-VueComponent,即指定组件的类。
      //  - 后期就可以基于new ToastCtor()创建组件类-也就是Vue子类的实例,也属性Vue类的实例。 ==> 这就是动态渲染组件。
      
      • const ToastCtor = Vue.extend(Toast)

        • Vue.extend()中传递的是OptionsAPI(或导入的组件)。
        • Vue.extend()是创建Vue类的一个子类-VueComponent,即指定组件的类。
        • 后期就可以基于new ToastCtor()创建组件类-也就是Vue子类的实例,也属性Vue类的实例。 ==> 这就是动态渲染组件。
    3. 在Vue原型上挂载要用函数方式渲染组件的方法。

      Vue.prototype.$toast = function $toast(text = "", time = 2000) {
        //1. new只是创建实例,挂载一些数据。
        let vm = new ToastCtor({
          //动态渲染组件,给组件传递属性。
          propsData: {
            text,
            time,
          },
        });
        console.log(`vm-->`, vm);
      
        // vm.$mount('#app')//会把整个app进行组件。
        // vm.$mount('#app')
        // $mount():视图进行渲染(视图->虚拟DOM->真实DOM)-即让vm.$el有对应的DOM节点。但是如果不指定容器,渲染的真实DOM无法出现在页面中。
        vm.$mount();
        //console.log(`vm-->`, vm);
        //把渲染后的真实DOM ==> vm.$el,插入到指定的容器中。
        document.body.appendChild(vm.$el);
      };
      
      • 具体步骤:

        1. 调用Vue组件构造函数,创建一个Vue实例,让Vue实例挂载一些数据。
        2. 使用Vue组件实例. m o u n t ( ) ,将视图进行渲染,即让 v m . mount(),将视图进行渲染,即让vm. mount(),将视图进行渲染,即让vm.el有对应的DOM节点。即把Vue组件实例类编译为虚拟DOM,并把编译出来的虚拟DOM渲染成真实的DOM。并且如果不指定容器,渲染的真实DOM并不会出现在页面中。
        3. 把挂载后的页面插入到指定的容器中。
参考于element-ui的$message事件样式
  1. /node_modules/element-ui/packages/message/src/main.js中就是对于函数执行封装组件的控制。
  2. /node_modules/element-ui/packages/message/src/main.vue中就是对于组件的OptionsAPI。
具体代码
  • fang/f20230703/day0703/src/components/Toast.vue

    <script>
    export default {
      name: "Toast",
      // 注册接收属性。
      props: {
        // text就是传递的提示消息信息;
        text: {
          type: String,
          default: "",
        },
        // 过多久后消失(单位毫秒)。
        time: {
          type: [Number, String],
          default: 2000,
        },
      },
      data() {
        return {};
      },
      // 第一次渲染完:设置定时器,到达指定时间后,把组件销毁。
      mounted() {
        let time = +this.time;
        if (isNaN(time)) {
          time = 2000;
        }
        this.destoryTimer = setTimeout(() => {
          this.$destroy();
          clearTimeout(this.destoryTimer);
        }, time);
      },
      // 销毁后:把真实的DOM从视图中移除掉。
      destroyed() {
        clearTimeout(this.destoryTimer);
        console.log("this", this);
        let ele = this.$el;
        ele.parentNode.removeChild(ele);
      },
      methods: {},
    };
    </script>
    <template>
      <div class="toast-box">{{ this.text }}</div>
    </template>
    <style lang="less" scoped>
    .toast-box {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      box-sizing: border-box;
      z-index: 9999;
      padding: 10px 20px;
      background: rgba(0, 0, 0, 0.5);
      font-size: 14px;
      color: #fff;
      border-radius: 4px;
      line-height: 30px;
    }
    </style>
    
  • fang/f20230703/day0703/src/global.js

    import Vue from "vue";
    
    // 1. 导入组件的OptionsAPI。
    import Toast from "@/components/Toast.vue";
    
    
    //基于函数执行渲染组件。
    const ToastCtor = Vue.extend(Toast);
    Vue.prototype.$toast = function $toast(text = "", time = 2000) {
      //1. new只是创建实例,挂载一些数据。
      let vm = new ToastCtor({
        //动态渲染组件,给组件传递属性。
        propsData: {
          text,
          time,
        },
      });
      console.log(`vm-->`, vm);
    
      // vm.$mount('#app')//会把整个app进行组件。
      // vm.$mount('#app')
      // $mount():视图进行渲染(视图->虚拟DOM->真实DOM)-即让vm.$el有对应的DOM节点。但是如果不指定容器,渲染的真实DOM无法出现在页面中。
      vm.$mount();
      //console.log(`vm-->`, vm);
      //把渲染后的真实DOM ==> vm.$el,插入到指定的容器中。
      document.body.appendChild(vm.$el);
    };
    

Vue.extend()语法

  • 基于函数执行渲染组件,得到一个Vue子类的构造函数。

    const ToastCtor = Vue.extend(Toast)
    //  - Vue.extend()中传递的是OptionsAPI(或导入的组件)。
    //  - Vue.extend()是创建Vue类的一个子类-VueComponent,即指定组件的类。
    //  - 后期就可以基于new ToastCtor()创建组件类-也就是Vue子类的实例,也属性Vue类的实例。 ==> 这就是动态渲染组件。
    

常见面试题

  • 面试题:vue中组件和插件有什么区别?
  • 面试题:介绍一下你对vuex的理解?
  • 面试题:vuex 页面刷新数据丟失怎么解决?

对vuex的理解

  • 面试题:介绍一下你对vuex的理解?

    • 在我之前的项目中,vuex几乎是必用的。

      • 只不过后来的一个vue3项目中,我使用了pinia代替了vuex。
    • 我基于pinia/vuex:

      • 首先,利用其公共状态管理的机制,实现复合组件间的通信。
      • 其次,为了防止路由切换中,有些数据需要频繁向服务器发请求,我把这些不经常更新的数据,基于vuex/pinia进行了临时存储。
      • 最后,之前项目中的登录态校验和权限校验,也是基于vuex存储一些登录者信息,来配合实现的。
    • 在我使用vuex的时候:

      • 首先要基于Vue.use(Vuex)使用这个插件,基于new Vuex.store创建一个store容器,还需要把store挂载到根组件的配置项中,只有这样才可以保证每个组件的实例上都有$store这个属性。

      • Vuex的配置项中,核心的有5部分,还有3个辅助配置。

        • state 管理公共状态。

        • getters 管理公共的计算属性。

        • mutations 修改公共状态的方法。

        • actions 异步修改状态的方法。其内部,只是完成了异步操作,修改状态还需要基于commit通知mutations中的方法执行。

        • modules 在项目较大的情况下,分模块管理公共信息。

        • 除此之外,还可以设置:

          • namespaced 这个是模块化管理必备的。
          • strict 设置只允许在mutations方法中修改状态。
          • plugins 使用插件,比如我之前使用过logger插件,实现派发日志的输出。使用persist插件实现vuex信息的持久化存储!
      • 在组件中使用的时候,基于 s t o r e . s t a t e / g e t t e r s 直接获取公共状态与计算属性,基于 store.state/getters直接获取公共状态与计算属性,基于 store.state/getters直接获取公共状态与计算属性,基于store.commit/dispatch通知mutations/actions中的方法执行,实现公共状态更改!

      • 只不过这样每一次都操作$store比较麻烦,我一般都是基于mapState/mapGetters/mapMutations/mapActions等辅助函数处理的,可以简化vuex在组件中的使用,提高开发效率!

    • 不过Vue3对应的Vuex4中,其使用的语法和一些细节,和Vuex3还是有很大差别的。

      • 首先都是函数式编程,没有Store这个类,基于createStore创建store容器。
      • 组件中也不需要$store这个属性了,基于useStore这个Hook函数,获取到store对象,然后进行相关操作。
      • map国债函数也没有用了,操作什么东西,都是基于store对象来处理,例如:store.state/getters/commit/dispatch等等!
    • 以上这些就是我平时在项目中,使用到的内容!vuex很有用,不仅可以实现组件之间的信息共享,还可以对一些数据进行临时的存储,操作起来还比较简单,所以在之前的项目中,vuex我基本上是必用的!「只不过我感觉,现在pinia比vuex更好用!」

组件和插件的问题

  • 面试题:vue中组件和插件有什么区别?

    • 在Vue中,组件(Component)和插件(Plugin)是两个不同的概念,它们在功能和使用方式上有一些区别:

      • 组件(Component):

        • 组件是Vue应用中可重用的UI元素。
        • 它们将HTML、CSS和JavaScript逻辑封装在一起,以创建独立的、可重用的功能单元。
        • 组件可以包含模板(Template)、样式(Style)和行为(Behavior),使开发者能够构建具有组合性和可维护性的应用程序。
        • 在Vue中,组件通过Vue.component()方法或单文件组件(.vue文件)再或者Vue.extend()的形式定义和注册。
      • 插件(Plugin):

        • 插件是Vue的扩展,用于向Vue应用添加全局功能。
        • 插件可以添加新的全局方法、指令、混入(Mixin)或者为Vue实例添加新的功能。
        • 它们可以在整个应用程序中使用,而不需要在每个组件中单独导入和配置。
        • 插件通常是以Vue插件的形式提供,开发者可以使用Vue.use()方法在Vue应用中注册和安装插件。
    • 区别总结:

      • 组件用于创建可重用的UI元素,而插件用于向Vue应用添加全局功能。
      • 组件通过Vue.component()方法或单文件组件的形式定义和注册,而插件通过Vue.use()方法注册和安装。
      • 组件可以在应用的任何地方使用,但插件添加的功能可以在整个应用中全局访问。
      • 组件通常包含模板、样式和行为,而插件主要用于添加全局方法、指令、混入或为Vue实例添加功能。
    • 需要注意的是,组件和插件并不是互斥的概念,它们可以同时在Vue应用中使用,以实现更丰富的功能和更高的可重用性!

    • 记得组件和插件都列举一些实战例子…

vuex

  • 模块的版本:

    • 模块的版本:

      npm view vuex versions
      
    • 格式为:

      • 主版本号·副版本号·补丁包

        • 主版本号是大的更新
        • 副版本号是小更新
        • 补丁包一般语法不会有啥变化,是框架内部的完善
      • 主版本号·副版本号·补丁包 - alpha/beta/rc.数字

        • alpha 内测「和真正的版本可能有很多差别」

        • beta 公测「对外放开测试,和正式发版区别不大」

        • rc 预发版

        • 正式发版

          • 一般只有在主版本更新或较大改变的副版本时,才会有这些alpha与beta或rc。
      • 同一版本号及补丁包的:正式版本号大于有alpha与beta或rc的版本。

        npm view vuex versions
        /* [
          '0.1.0',        '0.2.0',        '0.3.0',        '0.4.0',
          '0.4.1',        '0.4.2',        '0.5.0',        '0.5.1',
          '0.6.1',        '0.6.2',        '0.6.3',        '0.7.0',
          '0.7.1',        '0.8.0',        '0.8.1',        '0.8.2',
          '1.0.0-rc',     '1.0.0-rc.2',   '1.0.0',        '1.0.1',
          '2.0.0-rc.1',   '2.0.0-rc.3',   '2.0.0-rc.4',   '2.0.0-rc.5',
          '2.0.0-rc.6',   '2.0.0',        '2.1.0',        '2.1.1',
          '2.1.2',        '2.1.3',        '2.2.0',        '2.2.1',
          '2.3.0',        '2.3.1',        '2.4.0',        '2.4.1',
          '2.5.0',        '3.0.0',        '3.0.1',        '3.1.0',
          '3.1.1',        '3.1.2',        '3.1.3',        '3.2.0',
          '3.3.0',        '3.4.0',        '3.5.0',        '3.5.1',
          '3.6.0',        '3.6.1',        '3.6.2',        '4.0.0-alpha.1',
          '4.0.0-beta.1', '4.0.0-beta.2', '4.0.0-beta.3', '4.0.0-beta.4',
          '4.0.0-rc.1',   '4.0.0-rc.2',   '4.0.0',        '4.0.1',
          '4.0.2',        '4.1.0'
        ] */
        
        • 2.1.0 > 2.1.0-alpha.12
  • 关于版本号的问题:

    /*
    Semantic Versioning是一个前端通用的版本定义规范。格式为“{MAJOR}.{MINOR}.{PATCH}-{alpha|beta|rc}.{number)",要求实现compare(a,b)方法,比较ab两个版本大小。
      + 当a>b是返回1;
      + 当a=b是返回0;
      + 当a<b是返回-1;
      其中:rc>beta>alpha,major>minor>patch;
      例子:1.2.3<1.2.4<1.3.0.alpha.1<1.3.0.alpha.2<1.3.0.beta.1<1.3.0.rc.1<1.3.0
    */
    
vuex源码处理逻辑
  • 源码在fang/f20230703/day0703/node_modules/vuex/dist/vuex.js。一般在项目中的/node_modules/vuex/dist/vuex.js

    /*!
    * vuex v3.6.2
    * (c) 2021 Evan You
    * @license MIT
    */
    
    //这个对象就是vuex导出的对象。
    var index_cjs = {
      Store: Store,//状态仓库类,用于new出一个状态仓库的。
      install: install,//用于让Vue.use()调用的,Vue的插件化处理。
      version: '3.6.2',
      // 四个辅助函数。
      mapState: mapState,
      mapMutations: mapMutations,
      mapGetters: mapGetters,
      mapActions: mapActions,
      // 帮助相关的东西。
      createNamespacedHelpers: createNamespacedHelpers,
      // 派发日志。
      createLogger: createLogger
    };
    
    return index_cjs;
    
    1. 所以看源码,可以看到vuex中可以导出并解构出Store这个类。

    2. 并可以直接基于Vue.use()来使用Vuex这个插件,进而调用vuex中install中的这个方法。

    3. 可以看到vuex中有四个辅助函数。

    4. 可以看到vuex中自带了createLogger这个vuex的官方插件。

    • 当前看的版本为v3.6.2。
  1. /src/store/index.js中学习并导入。

    function install (_Vue) {
      // Vue是vuex内部的一个自定义变量,初始为undefined。//_Vue是Vue.use()调用install方法时,传入的自身的对象。
      if (Vue && _Vue === Vue) {
        // 用来禁止Vue.use()调用Vuex多次。
        {
          console.error(
            '[vuex] already installed. Vue.use(Vuex) should be called only once.'
          );
        }
        return
      }
      Vue = _Vue;
      applyMixin(Vue);//执行全局混入。
    }
    
    import Vue from "vue";
    import Vuex, { createLogger } from "vuex";
    
  2. 通过Vue.use(Vuex)得到调用了Vuex,执行了Vuex中的install方法。

    function applyMixin (Vue) {
      var version = Number(Vue.version.split('.')[0]);//得到Vue的主版本号。
    
      if (version >= 2) {
        //执行全局混入。
        Vue.mixin({ beforeCreate: vuexInit });
      } else {
        // override init and inject vuex init procedure
        // for 1.x backwards compatibility.
        var _init = Vue.prototype._init;
        Vue.prototype._init = function (options) {
          if ( options === void 0 ) options = {};
    
          options.init = options.init
            ? [vuexInit].concat(options.init)
            : vuexInit;
          _init.call(this, options);
        };
      }
    
      /**
       * Vuex init hook, injected into each instances init hooks list.
      */
    
      function vuexInit () {
        // 让子组件的$store必定能拿到根组件上的$store。因为在根组件上new Vue({$store:vue仓库})
        var options = this.$options;
        // store injection
        if (options.store) {
          //这个预期是根组件的`new Vue({store:vue仓库})`来执行的。
          this.$store = typeof options.store === 'function'
            ? options.store()
            : options.store;
        } else if (options.parent && options.parent.$store) {
          //这个预期是非根组件的OptionsAPI选项对象来执行的。一层一层通过父组件来找,因为是beforeCreate生命周期,父组件的实例对象必定创建好了,一直找下去,一定能找到根组件的$store。
          this.$store = options.parent.$store;
        }
      }
    }
    
    1. 禁止Vue.use()调用Vuex多次。
    2. 通过applyMixin(Vue)执行全局混入。
  3. 根据Vue的版本号,通过类似于Vue.mixin({ beforeCreate: vuexInit })的方法,执行全局混入。让所有Vue组件beforeCreate生命周期都执行vuexInit()这个mixin()函数

    function vuexInit () {
      // 让子组件的$store必定能拿到根组件上的$store。因为在根组件上new Vue({$store:vue仓库})
      var options = this.$options;
      // store injection
      if (options.store) {
        //这个预期是根组件的`new Vue({store:vue仓库})`来执行的。
        this.$store = typeof options.store === 'function'
          ? options.store()
          : options.store;
      } else if (options.parent && options.parent.$store) {
        //这个预期是非根组件的OptionsAPI选项对象来执行的。一层一层通过父组件来找,因为是beforeCreate生命周期,父组件的实例对象必定创建好了,一直找下去,一定能找到根组件的$store。
        this.$store = options.parent.$store;
      }
    }
    

Vue.mixin()

  • Vue.mixin(OptionsAPI) 全局混入

    • 在mixin中写入的内容,会混入到每一个组件中。
  • 在真实项目中,所有组件或大部分组件都需要统一做的事情,可以基于mixin混入。

  • 但是可能存在,混入的内容和组件自己的内容有相同的,此时mixin有自己的合并策略:

    • data/methods/computed等-需要挂载到实例上的,如果发生冲突,以组件自己的为主。
    • 钩子函数与监听器等,如果发生冲突,则两个都会保留,触发执行的时候,先执行mixin混入的,再执行组件自己的!
  • 虽然混入操作,看起来可以让所有组件具备一些相同的行为,但是本身是一个坑,真实开发中,我们应该减少全局混入的使用。

    • 混用全局混入操作,会给开发带来很多不便利性。
    • 并且对于某些不需要这些统一操作的组件,带来了性能的浪费。
  • 我们可以用局部混入,来代替全局混入。

    const myMixin = {
      data() {
        return {
          //...
        };
      },
      created() {
        //....
      },
      methods: {
        //...
      },
    };
    
    • 在需要使用这些混入功能的对象中,基于mixins把其混入进来。

      <script>
      import myMixin from "./myMixin";//导入混入。
      export default {
        mixins: [myMixin],
      };
      </script>
      
      import myMixin from "./myMixin";//导入混入。
      export default {
        mixins: [myMixin],
      };
      

进阶参考