文章目录
前言
本篇文章旨在回顾vue组件的内容,查漏补缺,以便更快、更好的在开发过程中,解决所遇到的问题。
一、自定义指令
1. 自定义指令的基本用法
// Vue.directive("demo", {})
// 当我们在directive中定义了demo这个指令后,我们就可以在template和JSX
<template>
<div id="app">
<div v-demo></div>
</div>
</template>
export default{
name: "appp",
render() {
return (
<div id="app">
<div v-demo></div>
</div>
);
},
}
2. 什么时候需要自定义指令?
当我们的methods中存在操作DOM/BOM的逻辑的时候,就该思考是否可以抽象成一个自定义指令。
以便于我们的逻辑、DOM操作解耦,方便单元测试。
Vue.directive("demo", {
// 只调用一次,指令第一次绑定到元素时调用。
// 在这里可以进行一次性的初始化设置。
bind: function(el, binding, vnode) {},
// 被绑定元素插入父节点时调用
// (仅保证父节点存在,但不一定已被插入文档中)
// 在bind中el.parentNode为null;
// 在inserted中可以通过el.parentNode访问当前节点的父节点
inserted: function(el, binding, vnode) {},
// 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。
// 指令的值可能发生了改变,也可能没有,但是可以通过比较更新前后的值(vnode和oldVnode)来忽略不必要的模板更新,在一定程度上提高组件性能。
update: function(el, binding, vnode, oldVnode) {},
// 指令所在组件的VNode及其子VNode全部更新后调用。
componentUpdate: function(el, binding, vnode, oldVnode) {},
// 只调用一次,指令与元素解绑时调用。
unbind: function(el, binding, vnode) {},
})
// el: 指令所绑定的元素,可以用来直接操作DOM
// binding: 一个对象,包含以下属性
{
name, // 指令名,不包括 v- 前缀
value, // 指令的绑定值
oldValue, // 指令绑定的前一个值
expression, // 字符串形式的指令表达式
arg, // 传给指令的参数,可选
modifiers, // 一个包含修饰符的对象
}
// vnode: Vue 编译生成的虚拟节点
// oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdate 钩子中可用
// 建议除了 el 之外,其他参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
3. 如何创建自定义指令?
// 需求
<template>
<!-- 1. v-resize 指令, 监听浏览器窗口大小改变的时候, 通过监听函数 onResize 响应-->
<!-- <div v-resize="onResize">window width is: {{ length }}</div> -->
<!-- 2. 可通过 direction,控制监听页面高度 或者 宽度的变化 -->
<div v-resize:[direction].quiet="onResize">window Height is: {{ length }}</div>
<!-- 3. 可通过 修饰符 .quiet 来控制是否在 指令初始化的时候 响应onResize函数 -->
<!-- <div v-resize.quiet="onResize">window width is: {{ length }}</div> -->
<!-- <div v-resize="onResize">window width is: {{ length }}</div> -->
</template>
<script>
export default {
data() {
return {
direction: "vertical",
length: 0
};
},
methods: {
onResize(length) {
this.length = length;
}
}
};
</script>
// main.js中,全局注册实现
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
Vue.directive("resize", {
inserted(el, binding) {
const callback = binding.value;
const direction = binding.arg;
const modifiers = binding.modifiers;
const result = () => {
return direction === "vertical" ? window.innerHeight : window.innerWidth;
};
const onResize = () => callback(result());
window.addEventListener("resize", onResize);
if (!modifiers || !modifiers.quiet) {
onResize();
}
el._onResize = onResize;
},
unbind(el) {
if (!el._onResize) return;
window.removeEventListener("resize", el._onResize);
delete el._onResize;
}
});
new Vue({
render: h => h(App)
}).$mount("#app");
二、双向绑定
1. 语法糖v-model
v-mode用于在表单元素<input>、<textarea>、及<select>上创建双向数据绑定的语法糖。
在上一篇文章中介绍了 .sync 修饰符的双向绑定(通过v-bind:msg和v-on:update:msg这种范式实现)
<template>
<div>
<div>
<h3>Text</h3>
{{text}}
<br />
<input type="text" v-model="text" />
<br />
<!-- 等价于 -->
<input type="text" :value="text" @input="text = $event.target.value" />
<br />
<!-- 绑定change(改变)事件 -->
<input type="text" v-model.lazy="text" />
<br />
<!-- 清楚两端空格 -->
<textarea v-model.trim="text"></textarea>
<br />
<!-- 格式化绑定类型 -->
{{typeof(num)}}
<input type="number" v-model.number="num" />
</div>
</div>
</template>
<script>
export default{
data() {
return {
text: "",
};
},
}
</script>
<template>
<div>
<div>
<h3>Radio & checkbox & select</h3>
<p>
以下那个不是甲类传染病?
<lable v-for="i in list" :key="i.value">
<input type="radio" v-model="selected" :value="i.value" />
{{i.name}}
</lable>
<br />
您选择了:{{ selected }}
</p>
<p>
以下那个不是甲类传染病?
<lable v-for="i in list" :key="i.value">
<input type="checkbox" v-model="checked" :value="i.value" />
{{i.name}}
</lable>
<br />
您选择了:{{ checked.join(',') }}
</p>
<p>
选择你希望人类最先消除的下列哪个恶疾?
<select v-model="select">
<option v-for="{name, value} in list" :value="value" :key="value" />
{{name}}
</option>
</select>
<br />
您选择了:{{ select }}
</p>
<p>
选择你希望人类最先消除的下列哪个恶疾?
<!-- multiple下拉框多选 -->
<select v-model="mutliSelected" multiple>
<option v-for="{name, value} in list" :value="value" :key="value" />
{{name}}
</option>
</select>
<br />
您选择了:{{ mutliSelected.join(',') }}
</p>
</div>
</div>
</template>
<script>
export default{
data() {
return {
selected : "A",
checked: [],
list: [
{
name: "霍乱",
value: "A"
},
{
name: "鼠疫",
value: "B"
},
{
name: "甲流",
value: "C"
},
],
select: "",
mutliSelected: [],
};
},
}
</script>
2. 自定义组件双向绑定(v-model)
// SCustomSelect组件
<template>
<div>
<div class="top" @click="showBottom = !showBottom">{{ selected.name }}</div>
<div class="bottom" v-if="showBottom">
<div v-for="i in list" :key="i.value" @click="select(i)>{ i.name }</div>
</div>
</div>
</template>
<script>
export default{
model: {
prop: "selected",
event: "change"
}
props: ["list", "selected"],
data() {
return {
showBottom: false,
};
},
methods: {
select(i) {
this.$emit("change", i);
this.showBottom = false;
}
}
}
</script>
<style scoped>
.top {
border: 1px solid #999;
padding: 2px;
}
.bottom {
position: relative;
border: 1px solid #999;
top: -1px;
}
</style>
// 引入SCustomSelect组件
<template>
<div>
<h3>Custom</h3>
<s-custom-select v-model="selected" :list="list"></s-custom-select>
您选择了:{{ selected.name }}
</div>
</template>
<script>
import SCustomSelect from "./SCustomSelect";
export default{
components: {
SCustomSelect
},
data() {
let list= [
{
name: "霍乱",
value: "A"
},
{
name: "鼠疫",
value: "B"
},
{
name: "甲流",
value: "C"
},
],
return {
selected : list[0],
};
},
}
</script>
三、组件设计
1. 业务工程中组件设计的大致思路
思路:提取Nav和Footer组件,差异部分依据依赖注入原则,在使用时动态的分配内容;
在Vue中,我们可以使用插槽来解决这个问题,插槽这个 API 的设计思想源自 Web Components 规范草案。
我们的页面由header、default、footer三部分组成,
例如这个代码,右边是我们的子模板组件s-index-layout, 左边是使用了这个模板的父组件,
在父模板中我们通过v-slot指令来指定对应子模板中的slot,
如这里的header,default和slot,在子模板中我们通过slot标签上的name属性定义slot的具体名字。在slot标签中的内容是缺省的信息。
将静态的文案修改成了动态的插值语法。
这其实和我们普通组件一样,我们通过一个插值表达式将当前组件的content内容塞到这个slot里面去显示,并且这个content内容对于我们子模板组件slot-layout是不可见的。
这是因为父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
如果需要在父模板中,使用子模板中的数据,替换子模板中的内容。
比如这个例子,同样的,左边是我们的父组件,右边是子组件,父组件使用了子组件,并通过slot:header去定义在header里面插入的东西。
我们可以看到默认的子组件里面显示的是用户名,就好像我们在很多官方网站上看到的一样。
那如果这个时候策划提出需要在某种tob的情况下要实现用户的email而不是user该怎么做呢。
有一种方式是通过变量提升,我们把user信息提到父组件里面去了,由父组件来控制。
但这从设计上来说并不优雅,因为子组件很有可能已经被很多其他组件使用,那么动一发而牵全身肯定是不合适,
并且我们也希望子组件能够尽量的能保持其独立的完整性,以保证他在任何地方都是可以拿来随插随用的。
那要怎么做呢?
这里vue给我们留了一个后门(从2.6.0的版本开始),
我们可以将 user 作为 <slot> 元素的一个 attribute 绑定到slot上面去,绑定在 <slot> 元素上的 attribute 被称为插槽 prop,
现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字,
这里我们通过对象的解构将user取出来,然后将user,email插值到页面上面去。
2. vue中slot组件的使用
实现“loadding加载中”的一个组件
// 子组件SLoad
<template>
<div>
<div v-if="loadding">加载中。。。</div>
<slot v-else :data="data"></slot>
</div>
</template>
<script>
export default {
props: ["url"],
data() {
return {
loadding: true,
data: {}
};
},
created() {
setTimeout(() => {
this.loadding = false;
this.data = { name: "LiLei" };
}, 1000);
}
};
</script>
// 父组件SLay
<template>
<s-load url="http://xxx.com/api" #default="{data}">
<div>锣鼓喧天,旌旗蔽空地欢迎 {{ data.name }}</div>
</s-load>
</template>
<script>
import SLoad from "./SLoad";
export default {
components: {
SLoad
}
};
</script>
四、组件通信
1. 组件跨层级访问
在子组件中去访问外层组件, 严格按照单项数据流的规范来说,直接从子组件去修改父组件是不被允许的,但我们可以通过emit事件来通知父级组件进行相应的修改。
当我们需要跨越多层级时,就会发现,这个过程很是枯燥和乏味,而且一旦传错,排查起来也不方便。
针对上面的问题,vue在每个实例上都提供了$root 和 $parent 这俩个属性。
可以通过$root访问当前单页应用的根组件,通过$parent来访问当前组件的父组件,相应的也就可以直接修改根组件或父组件的属性或是调用根组件或父组件上的方法。
子组件可以通过$parent $root 访问父组件, 那么父组件怎么访问子组件呢?
答案就是$ref,需要注意的是:
1. $ref 只能在 mounted 生命周期钩子函数被调用之后才能使用。
2. $parent 和 $root 在各个生命周期钩子函数中都可以使用。
3. 虽然使用$root 和 $parent 可以很省力地去修改内容,但这不可避免了造成了这个组件和相应根组件或者父组件之间的强耦合。
(强耦合通俗地说就是,这个组件如果它里面用了$parent那就必须和相应的父组件成对使用,缺一不可了。)
2. 依赖注入
由上面的组件跨层级访问,我们可以了解到,使用$root 和 $parent 会造成组件和相应根组件或者父组件之间的强耦合。
为了解决这个问题,vue里面提供依赖注入的方式来声明当前组件依赖的父组件们(直系祖宗)的外部prop有哪些。
1. provide 允许我们指定我们想要提供给后代组件的数据/方法;
2. 在任何后代组件里,都可以使用 inject 来接收provide 指定的我们想要添加在这个实例上的属性;
优点:
*祖先组件不需要知道哪些后代组件使用它提供的属性;
*祖先组件不需要知道哪些后代组件使用它提供的属性。
缺点:
*组件间的耦合较为紧密,不易重构;
*供依赖注入所提供的属性是非响应式的 (vuex是响应式的) 。
vue里面的依赖注入是如何实现的。
下面是vue2.0实现依赖注入的源码(源码地址:https://github.com/vuejs/vue/blob/main/src/core/instance/inject.ts)。
大家注意这里,inject也是通过$parent, 依次往父组件上去寻找声明了的provide对象。
export function resolveInject(
inject: any,
vm: Component
): Record<string, any> | undefined | null {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
}
return result
}
}
3. 组件二次封装
由于一些奇怪的原因(这里的奇怪指的是作为程序员你所不能理解的老板或者设计说这里就要这样),你需要对一些现有的组件或者三方的组件库做一些定制化的需求,比方说样式修改等 =》 转到代码
// 二次封装element的ei-input组件SCustomInput
// 通过 v-bind=“$attr” 来传递父组件上的prop class 和 style;
// 通过 v-on=“$listenser” 来传递父组件上的事件监听器和事件修饰符;
<template>
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
</template>
<style scoped>
// 由于我们声明了scoped属性,我们在浏览器后台样式查看器中可以看到el-input是添加了唯一标识字符串的,而el-input__inner没有,vue中规定可以通过 >>> 来定位到el-input__inner类名,给其添加样式
.el-input >>> .el-input__inner {
border-top: none;
border-left: none;
border-right: none;
}
</style>
// 在APP.vue中使用封装的SCustomInput组件
<template>
<div id="app">
<s-custom-input/>
</div>
</template>
<script>
import SCustomInput from "./components/SCustomInput";
export default {
name: "App",
components: {
SCustomInput
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
五、插件
1. Mixin模式(vue中扩展插件)
在js中,我们可以通过原型继承或者引用拷贝去灵活地进行共享属性和方法。
vue 提供了全局和局部注册mixin的 mixin函数。
Vue.mixin 会在全局注册一个混入,会影响所有创建的Vue 实例。
<template>
<div>smixin</div>
</template>
<script>
/* eslint-disable no-console */
import Vue from "vue";
const mixin = {
created: function() {
console.log("mixin created");
},
methods: {
foo: function() {
console.log("foo");
},
conflicting: function() {
console.log("from mixin");
}
}
};
Vue.mixin(mixin);
export default {
created() {
console.log("component created");
this.conflicting();
},
methods: {
conflicting: function() {
console.log("from components");
}
}
};
</script>
*同名钩子函数将合并为一个数组,混入对象的钩子将在组件自身钩子之前调用(上图代码打印结果:先 mixin crested,再component created)。
*二者的 methods、components 和 directives,将被合并为同一个对象。若对象键名冲突时,取组件对象的键值对(上图代码打印结果:from components,未打印from mixin)。
* Vue.mixin 在全局注册一个混入,会影响所有的vue组件,所以需要特别地注意,谨慎地使用vue.mixin这种混入方式(局部注册使用时只对局部组件有影响)。
2. 插件
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
1. 添加全局方法或者 property。如:vue-custom-element
2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
3. 通过全局混入来添加一些组件选项。如 vue-router
4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
通过全局方法 Vue.use() 使用插件:
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
该方法需要在调用 new Vue() 之前被调用。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。
开发插件:文档地址:https://cn.vuejs.org/v2/guide/plugins.html#%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6
六、组件复用
1. Mixin模式
组件中的逻辑复用,比较常见的一个例子,就是表单。
如下图中,我们的一个常用表单中,可能存在着 input , select, timepicker, switch, checkbox, radio 等多个组件,他们的变现形式各异,但是,当我们点击立即创建按钮的时候,一个比较通用的场景就是需要对每一份内容进行校验。
只有当所有组件中的内容都校验通过了, 表单才会提交。
最直接的想法是给每个表单组件里面写上一个validate()方法,但是这非常的枯燥,我们可以通过mixin将一个公用的函数同步到每一个组件中去。
在这里我们就可以用到mixin局部注册了。
举一个例子,当input组件失焦的时候,可以做一次校验。
由此我们可以约定一定的校验规则,按照设计模式中的策略模式来匹配校验,除了这个组件之外,其他的组件也可以复用这个mixin,从而复用逻辑。
但是我们也会发现,如果对于一个以前没有维护这个组件, 临时接收这个组件的开发人员来说,失焦的时候调用的validate函数在当前组件里面是没有声明的,这很大程度上制造了些困扰。
mixin缺陷:
*打破了原有组件的封装;
*增加组件复杂度;
*可能会出现命名冲突的问题(多人协作,复用多个mixin时);
*仅仅只是对逻辑的复用,模板不能复用;
2. HOC高阶组件
HOC, higher order component, 高阶组件,这是一种在react社区使用比较多的结构型模式,本质上是对高阶函数的一种引申,也可以认为是装饰者模式的一种实现。
通俗地说,hoc就是一个函数接收一个组件作为参数,并返回一个新组件; 并且将可复用的逻辑在函数中实现。通过复用这个装饰者函数来实现业务逻辑的复用。
我们使用Hoc的方式来实现一个先前同样的校验逻辑。
在这个hoc中,我们可以看到他是接收一个组件并且返回一个新的组件,返回的新组件和原先组件从本质上是一个父子组件的关系,那么父子组件之间传递信息,同样使用我们已经熟悉的prop和$emit来传递。
在父层组件中,我们可以抽象一个统一的校验逻辑。除此之外,我们会发现在父层组件我们可以抽象出统一的模板内容,比方说这里的校验内容。
HOC相比较Mixin的优点:
*模板可复用;
*不会出现命名冲突(本质上是一个HOC是套了一层父组件);
HOC的不足:
*组件复杂度高,多层嵌套,调试会很痛苦
3. RenderLess组件
renderless组件是目前在vue社区使用比较多的一种复用业务逻辑的模式。
这种模式是伴随着slot插槽机制而产生的。它可以帮助我们:
*复用的逻辑沉淀在包含slot插槽的组件;
*接口由插槽Prop来暴露;
上面的代码同样是校验组件,
首先定义一个svalidate组件,
然后通过插槽prop将组件中的validate函数暴露出来,
然后当具体的子组件,input组件在失焦的时候调用这个校验validate函数。
RenderLess组件的优点:
*模板可复用;
*不会出现命名冲突;
* 符合依赖倒置原则;
*复用的接口来源清晰;
总结
革命尚未成功,同志仍需努力。