在日常项目开发过程中,往往离不开封装组件的过程。比如a页面有一个功能恰巧b页面和c页面也使用。这时候我们考虑到代码复用,会考虑将这个功能做成组件。这篇文章我们就来讲一讲开发vue组件时候的一些小技巧和一些黑(骚)科(操)技(作)。
我们会通过大量例子来一个个讲解
目录
- 常规功能组件小技巧
- 函数式组件
- 内联模板
- 递归组件
v-once
低开销静态组件- 全局函数调用组件
常规功能组件小技巧
首先我们用脚手架搭建一个项目,用来写我们的组件,我们用到的UI框架是Iview。功能组件的编写,离不开组件之间的通信
,这个相关的文章很多,本文就不详细阐述了,这里推荐一篇掘金的关于组件通信的文章,写的十分全面感兴趣的小伙伴可以去看看。
slot插槽
slot插槽在我们平常写组件的时候出场率应该很高。将 <slot>
元素作为承载分发内容的出口,我们可以在插槽内包含任何模板代码,这使得我们的组件更加的灵活。我们来讲一个比较经典的例子对话框组件,先看代码。
<template>
<div>
<div>头部</div>
<div>
<slot>默认内容</slot>
</div>
<div>
<slot name="footer"> <Button>确认</Button> </slot>
</div>
</div>
</template>
<script>
export default { name: "model" };
</script>
model标签中除slot为footer
的代码,其他代码都将作为对话框的主体内容显示,这就是作用域插槽
的用法。而slot为footer
的代码将在底部操作按钮部分显示,对应<slot name="footer">
这就是具名插槽
的用法。
属性透传
属性透传的出现的场景不多,我们以UI组件的二次封装为例。假如你的项目中频繁使用一个UI组件,而且这个UI组件每次都要设置很多类似的属性,那么我们就可以进行二次封装,调用起来更加的方便,甚至可以在原有组件上扩展功能。我们来看一段二次封装Iview的Table组件的代码。
<script>
import { Table } from "iview";
export default {
name: "customizeTable",
props: { ...Table.props, test: { type: [String], default: "扩展属性" } },
data() {
return { defaultProps: { border: true, stripe: true } };
},
render(h) {
var props = Object.assign({ ...this._props }, this.defaultProps);
return h(Table, { props: props });
}
};
</script>
//方法二
<template>
<div>
<Table v-bind="{...mergeProps}"></Table>
</div>
</template>
<script>
import { Table } from "iview";
export default {
name: "customizeTable",
props: {
...Table.props,
test: {
type: [String],
default: "扩展属性"
}
},
computed: {
mergeProps: function() {
return Object.assign({ ...this._props }, this.defaultProps);
}
},
data() {
return {
defaultProps: {
border: true,
stripe: true
}
};
}
};
</script>
首先我们通过Table组件的props属性获取的Table所有默认参数,然后通过this._props
实现属性透传,同时我们还可以在props中扩展你需要的属性如test
。方法一中绑定属性我们使用了render方法,这个我们在函数式组件中再细说。方法二中我们通过v-bind实现绑定对象中所有的属性。
函数式组件
我们先引用一下官网对于函数式组件的介绍
函数式组件,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。
组件需要的一切都是通过 context
参数传递,它是一个包括如下字段的对象:
props
:提供所有 prop 的对象children
: VNode 子节点的数组slots
: 一个函数,返回了包含所有插槽的对象scopedSlots
: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为createElement
的第二个参数传入组件parent
:对父组件的引用listeners
: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on
的一个别名。injections
: (2.3.0+) 如果使用了inject
选项,则该对象包含了应当被注入的属性。
在添加 functional: true
之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context
参数,并将 this.$slots.default
更新为 context.children
,然后将 this.level
更新为 context.props.level
。
为什么要用函数式组件?
由于函数式组件没有生命周期、无状态、没有实例,那就意味着它渲染速度更快,开销更低,同时因为函数式组件只是函数,我们可以在函数中实现一些我们需要的逻辑。
什么时候使用函数式组件?
在我看来函数式组件更像是一个中间件的角色,所以它比较常见的应用场景就是作为包装组件。举例来说我们有一个组件可能渲染多个组件中的一个,需要根据传递的参数来决定渲染那个组件,我们来看一段代码。
export default {
functional: true,
render: function (createElement, context) {
return createElement(context.props.domType, context.data, context.children);
}
}
<template>
<div>
<functionnalScript domType="model" style="width:100%"></functionnalScript>
</div>
</template>
<script>
import customizeTable from "../components/customizeTable.vue";
import model from "../components/model.vue";
import functionnalScript from "../components/functionnal.js";
export default {
name: "pageA",
components: {
customizeTable,
model,
functionnalScript: functionnalScript
}
};
</script>
<template functional>
<div>
<button v-bind="data.attrs" v-on="listeners">
<slot />
</button>
</div>
</template>
正常调用的时候父页面我们有customizeTable组件和model组件我们可以通过在一个属性domType
指定functionnalScript显示对应的组件,而且可以在多处复用。
还记得我们前面是不是讲了属性透传
,其实通过函数式组件也可以实现属性透传,我们来看模板的函数式组件functional组件就实现了完全透传任何 attribute
、事件监听器
、子节点
等。
内联模板
组件的模板一般都是在template选项内定义,而内联模板在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当作模板,而不是把它当内容分发,这让模板更灵活。但是这也导致了一些问题,我们先看代码。
<script>
export default {
name: "inlineChildren",
data() {
return { tip: "我是子组件的数据" };
}
};
</script>
<template>
<div>
<inlineChildren inline-template>
<div>
<h3>{{tip}}</h3>
</div>
</inlineChildren>
</div>
</template>
<script>
import inlineChildren from "../components/inlineChildren.vue";
export default {
name: "pageA",
components: { inlineChildren },
data() {
return { tip: "我是父组件的数据" };
}
};
</script>
inline-template的存在让inlineChildren标签内部内容不再作为slot(分发内容),而是作为inlineChildren组件的template
,并且这部分内容所在的上下文,是子组件的,并不是父组件的。所以除非特殊场景(我是没遇到过~~),其它适合正如官网所说的,inline-template
会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template
选项或 .vue
文件里的一个 <template>
元素来定义模板。
递归组件
什么是递归组件?
在组件自己的模板中调用自身就是我们所谓的递归组件。(注意需要通过组件的name
调用)
使用场景有哪些?
递归组件在我们日常开发中的发挥空间还是很大的,比如我们的菜单,评论等有层级的功能都可以使用到它。我们来看一个菜单的例子:
<template>
<div>
<div v-for="item in menuList">
<div class="hasChildren">{{item.name}}
<Icon v-if="item.children" type="ios-arrow-down" />
</div>
<side-menu v-if="item.children" :menuList="item.children"></side-menu>
</div>
</div>
</template>
<script>
export default {
name: "SideMenu",
props: {
menuList: {
type: Array,
default() {
return [];
}
}
}
};
</script>
<style>
.hasChildren {
background: #999;
color: #fff;
height: 40px;
line-height: 40px;
}
</style>
<template>
<div>
<sideMenu :menuList="menuList"></sideMenu>
</div>
</template>
<script>
import sideMenu from "../components//side-menu.vue";
export default {
name: "pageA",
components: { sideMenu },
data() {
return {
menuList: [
{ name: "side1", children: [{ name: "side1-1" }, { name: "side1-2" }] },
{
name: "side2",
children: [
{ name: "side2-1" },
{ name: "side2-2", children: [{ name: "side2-2-1" }] }
]
}
]
};
}
};
</script>
我们可以看到在sideMenu组件内部调用了它本身,这就是递归组件的基本使用方式。在更加复杂一点的情况就是有2个组件A和B,他们之间互有调用关系。这时会出现一个问题,到底是先有鸡还是先有蛋。
模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件,解决方法有2个。
//方法一 在A组件
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./B.vue').default
}
//方法二 异步加载B组件
components: {
B: () => import('./B.vue')
}
v-once低开销静态组件
如果你的组件中出现了大量静态内容,那么它们可能导致你的组件渲染速度变慢,这时候你可以在根元素上添加 v-once
attribute 以确保这些内容只计算一次然后缓存起来。
<template>
<div v-once> {{hugeData}} </div>
</template>
<script>
export default {
name: "vOnce",
data() {
return { hugeData: "huge Data..." };
},
mounted() {
setTimeout(() => {
this.hugeData = "change Data...";
}, 100);
}
};
</script>
你会发现hugeData
一直是huge Data...没有变,因为v-once
导致这个模板渲染后数据发生变化无法正常更新,所以考虑到其他开发者对于这个属性并不一定了解,如果不是特别需要尽量不要使用。
全局函数调用组件
正常的组件调用的过程:
<testModule></testModule>
import TestModule from "./TestModule "
export default { components: { testModule }};
如果你需要在多处使用这个组件,那么意味着每次调用你都需要重复上面的步骤,那么有没有更加方便的调用方式呢?console.log
、alert
不陌生吧,有时候我们也希望能通过这种方式调用一个组件,不在需要把组件写入到节点中。例如一个提示框组件,可能需要在各种时候唤醒它,并且提示对应内容,我们通过代码来实现这个tip组件。
//extend.js
import Vue from 'vue'import Tip from './tip.vue';
//使用基础 Vue 构造器,创建一个包含tip组件的“子类”
Tip.newInstance = (props) => {
const tip = Vue.extend(Tip);
const messageInstance = new tip({ propsData: props });
const div = document.createElement('div');
document.body.appendChild(div); messageInstance.$mount(div)
}
export default Tip.newInstance
//main.js
import tip from 'extend.js'Vue.prototype.$tip=tip;
//调用
this.$tip({});
这样我们就能实现通过调用一个函数this.$tip({})
来调用Tip组件了。你可以看到我们的核心思想就是创建一个包含我们需要组件的vue实例,然后将这个实例挂载到一个新创建的div上。如果我们可能多次调用这个函数,那么页面中将会被添加入多个div,这并不是我们所希望,所以我们还可以再优化一下。
<template>
<div>
<div v-for="item in tips">
<tip v-bind="item"></tip>
</div>
</div>
</template>
<script>
let i = 0;
const now = new Date().getTime();
function getUid() {
return "tip" + now + "-" + i++;
}
import tip from "./tip.vue";
export default {
name: "tipList",
data() {
return { tips: [] };
},
components: { tip },
methods: {
add(options) {
let name = getUid;
let _options = Object.assign({ name: name }, options);
this.tips.push(_options);
}
}
};
</script>
import Vue from 'vue'import Tip from './tipList.vue';
let tip, initialization;
//使用基础 Vue 构造器,创建一个包含tip组件的“子类”
Tip.newInstance = () => {
tip = Vue.extend(Tip);
const messageInstance = new tip();
const div = document.createElement('div');
document.body.appendChild(div);
messageInstance.$mount(div)
return {
prompt(props) {
messageInstance.add(props);
}
}
}
initialization=function(){
if(!tip){
tip=Tip.newInstance()
}
return tip
}
export default initialization
//main.js
import tip from 'extend.js'
Vue.prototype.$tip=tip;
//调用
this.$tip.prompt({});
我们通过一个组件tipList.vue
的prompt
方法实现多次调用的tip组件的需求,每次调用tip组件等于向tips数组中push一个对象这样就会多渲染出一个tip组件。
总结
希望看完之后,你也能在你的组件中使用上它们,让你的组件可以减少一些不必要的逻辑。作者可能有一些理解不对的地方希望大佬可以再评论里提出来,我们一起学习一起进步。
其他文章传送门: