day-104-one-hundred-and-four-20230703-组件封装-Vue.extend()语法-创建一个使用函数来调用的组件-组件和插件的问题-vuex-Vue.mixin()
组件封装
-
技术角度:
-
视图复杂使用jsx语法。
-
视图简单使用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>
-
-
-
组件调用:
-
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>
-
-
使用函数的方式去调用。
-
创建一个使用函数来调用的组件
-
在Vue2中,想要渲染一个组件,我们只需要创建此类的一个实例即可。
-
基于ES6Module导入的组件,不是一个类,只是这个组件的描述
import Toast from '@/components/Toast.vue' console.log(Toast)- 如果我们打算
new Toast()会报错;
- 如果我们打算
-
步骤:
-
导入组件的OptionsAPI。
-
基于函数执行渲染组件,得到一个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类的实例。 ==> 这就是动态渲染组件。
-
-
在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); };-
具体步骤:
- 调用Vue组件构造函数,创建一个Vue实例,让Vue实例挂载一些数据。
- 使用Vue组件实例. m o u n t ( ) ,将视图进行渲染,即让 v m . mount(),将视图进行渲染,即让vm. mount(),将视图进行渲染,即让vm.el有对应的DOM节点。即把Vue组件实例类编译为虚拟DOM,并把编译出来的虚拟DOM渲染成真实的DOM。并且如果不指定容器,渲染的真实DOM并不会出现在页面中。
- 把挂载后的页面插入到指定的容器中。
-
-
参考于element-ui的$message事件样式
- 在
/node_modules/element-ui/packages/message/src/main.js中就是对于函数执行封装组件的控制。 - 在
/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;-
所以看源码,可以看到vuex中可以导出并解构出Store这个类。
-
并可以直接基于Vue.use()来使用Vuex这个插件,进而调用vuex中install中的这个方法。
-
可以看到vuex中有四个辅助函数。
-
可以看到vuex中自带了createLogger这个vuex的官方插件。
- 当前看的版本为v3.6.2。
-
-
在
/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"; -
通过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; } } }- 禁止Vue.use()调用Vuex多次。
- 通过applyMixin(Vue)执行全局混入。
-
根据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], };
-