vue components 组件封装
原文链接:https://blog.csdn.net/qq_33988065/article/details/85124428
什么叫做组件化
vue.js 有两大法宝,一个是数据驱动,另一个就是组件化,那么问题来了,什么叫做组件化,为什么要组件化?接下来我就针对这两个问题一一解答,所谓组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JS、模板、图片等资源放在一起开发和维护。 因为组件是资源独立的,所以组件在系统内部可复用,组件和组件之间可以嵌套,如果项目比较复杂,可以极大简化代码量,并且对后期的需求变更和维护也更加友好。
如何进行组件化开发
先看下图:
这是 vue.js 中的一个报错,原因是使用了一个未经注册的组件 lx-xxx
,这个报错告诉我们一个道理:使用自定义组件之前必须注册。
那么如何注册一个组件呢? Vue.js 提供了 2 种组件的注册方式,全局注册和局部注册。
- 全局注册
在 vue.js 中我们可以使用 Vue.component(tagName, options) 进行全局注册,例如
Vue.component('my-component', {
// 选项
})
- 局部注册
Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 components 选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
区别:全局组件是挂载在 Vue.options.components 下,而局部组件是挂载在 vm.$options.components 下,这也是全局注册的组件能被任意使用的原因。
组件化开发必备知识
所谓工欲善其事,必先利其器,在正式开发一个组件之前,我们先要掌握一些必备的知识,这里我只会简单介绍一下,详情参考官网。
name
组件的名称,必填
<lx-niu/>
<lx-niu></lx-niu/>
name: 'lxNiu'
js 中使用驼峰式命令,HTML 使用kebab-case命名。
props
组件属性,用于父子组件通信,可通过this.msg访问
<div>{{msg}}</div>
props: {
msg: {
type: String,
default: ''
}
}
show: Boolean // 默认false
msg: [String, Boolean] // 多种类型
computed
处理data或者props中的属性,并返回一个新属性
<div>{{newMsg}}</div>
computed: {
newMsg() {
return 'hello ' + this.msg
}
},
注:因为props,data和computed在编译阶段都会作为vm的属性合并,所以不可重名
render
用render函数描述template
<lx-niu tag='button'>hello world</lx-niu>
<script type="text/javascript">
export default {
name: 'lxNiu',
props: {
tag: {
type: String,
default: 'div'
},
},
// h: createElement
render(h) {
return h(this.tag,
{class: 'demo'},
this.$slots.default)
}
}
</script>
render 中的 h 其实就是 createElement,它接受三个参数,返回一个 vnode
h 参数解释:
args1: {string | Function | Object} 用于提供DOM的html内容
args2: {Object} 设置DOM样式、属性、绑定事件之类
args3: {array} 用于设置分发的内容
注:vue编译顺序: template–> compile --> render --> vnode --> patch --> DOM
slot
分发内容,有传入时显示,无传入 DOM 时显示默认,分为无名和具名两种,this. solts.default默 认 指 向 无 名 插 槽 , 多 个 s l o t 时 用 法 this. solts.default默认指向无名插槽,多个slot时用法this.slots.name
<lx-niu>
<div slot='header'>header</div>
<div class="body" slot='body'>
<input type="text">
</div>
<div slot='footer'>footer</div>
<button class='btn'>button</button>
</lx-niu>
<template>
<div>
<slot name='header'></slot>
<slot name='body'></slot>
<slot name='footer'></slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'lxNiu',
mounted() {
this.$slots.header // 包含了slot="foo"的内容
this.$slots.default // 得到一个vnode,没有被包含在具名插槽中的节点,这里是button
}
}
</script>
class
定义子组件的类名
// 父组件
<lx-niu round type='big'/>
// 子组件
<div :class="[
type ? 'lx-niu__' + type : '',
{'is-round': round},
]">控制</div>
//真实DOM
<div class='lx-niu__big is-round'>hello</div>
其他属性
$attrs
v-bind="$attrs" 将除class和style外的属性添加到父组件上,如定义input:
<input v-bind="$attrs">
v-once
组件只渲染一次,后面即使数据发生变化也不会重新渲染,比如例子中val不会变成456
<template>
<div>
<button @click="show = !show">button</button>
<button @click="val = '456'">button</button>
<div v-once v-if="show">
<span>{{val}}</span>
</div>
</div>
</template>
<script>
export default {
data() {
return{
show: false,
val: '123'
}
},
};
</script>
mixins
// mixin.js
export default {
data() {
return{
msg: 'hello world'
}
},
methods: {
clickBtn() {
console.log(this.msg)
}
},
}
// index.vue
<button @click="clickBtn">button</button>
import actionMixin from "./mixin.js";
export default {
methods: {},
mixins: [actionMixin]
}
实例演示
比如我们要注册一个 lx-button 这样一个组件,那么目录和伪代码如下:
图片.png
index.vue
<template>
<button>lxButton</button>
</template>
<script>
export default {
name: 'lxButton'
}
</script>
index.js
import lxButton from './src/index'
lxButton.install = (Vue) => {
Vue.component(lxButton.name, lxButton)
}
export default lxButton
其中 install 是 Vue.js 提供了一个公开方法,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
MyPlugin.install = function (Vue, options){}
watch-弹窗实现原理
<button @click="dialogVisible = true">显示</button>
<lx-niu :visible.sync="dialogVisible"></lx-niu>
<script>
export default {
data() {
return {
dialogVisible: false
}
},
watch: {
dialogVisible(val) {
console.log('father change', val)
}
}
}
</script>
定义组件
<template>
<div v-show="visible">
<button @click="hide">关闭</button>
</div>
</template>
<script>
export default {
name: 'lxNiu',
props: {
visible: Boolean
},
watch: {
visible(val) {
console.log('child change:', val)
}
},
methods: {
hide() {
this.$emit('update:visible', false);
}
},
}
</script>
点击父组件中的显示按钮,改变传入子组件中的值,点击子组件中的关闭,改变父组件中值。
注:@click=“dialogVisible = true” 点击时将dialogVisible的值改为true
注::visible.sync: 双向数据绑定,配合update:visible使用,实现子组件修改父组件中的值
col组件实例
export default {
name: 'ElCol',
props: {
span: {
type: Number,
default: 24
},
tag: {
type: String,
default: 'div'
},
offset: Number,
pull: Number,
push: Number,
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (this[prop] || this[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${this[prop]}`
: `el-col-${this[prop]}`
);
}
});
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof this[size] === 'number') {
classList.push(`el-col-${size}-${this[size]}`);
} else if (typeof this[size] === 'object') {
let props = this[size];
Object.keys(props).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${props[prop]}`
: `el-col-${size}-${props[prop]}`
);
});
}
});
return h(this.tag, {
class: ['el-col', classList],
style
}, this.$slots.default);
}
};
col组件使用render函数,而不是template来实现组件,原因有两个:
1.该组件有大量的类判断,如果采用template代码比较冗余,使用js代码更加简洁
2.直接render描述性能更好
button组件实例
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
type: {
type: String,
default: 'default'
},
size: String,
icon: {
type: String,
default: ''
},
nativeType: {
type: String,
default: 'button'
},
loading: Boolean,
disabled: Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
icon组件
1.组件封装
图片.png
<template>
<svg
:class="getClassName"
:width="width"
:height="height"
aria-hidden="true">
<use :xlink:href="getName"></use>
</svg>
</template>
<script>
export default {
name: 'icon-svg',
props: {
name: {
type: String,
required: true
},
className: {
type: String
},
width: {
type: String
},
height: {
type: String
}
},
computed: {
getName () {
return `#icon-${this.name}`
},
getClassName () {
return [
'icon-svg',
`icon-svg__${this.name}`,
this.className && /\S/.test(this.className) ? `${this.className}` : ''
]
}
}
}
</script>
<style>
.icon-svg {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
</style>
2.全局注册使用
图片.png
svg文件夹下是静态svg图片
图片.png
/**
* 字体图标, 统一使用SVG Sprite矢量图标(http://www.iconfont.cn/)
*
* 使用:
* 1. 在阿里矢量图标站创建一个项目, 并添加图标(这一步非必须, 创建方便项目图标管理)
* 2-1. 添加icon, 选中新增的icon图标, 复制代码 -> 下载 -> SVG下载 -> 粘贴代码(重命名)
* 2-2. 添加icons, 下载图标库对应[iconfont.js]文件, 替换项目[./iconfont.js]文件
* 3. 组件模版中使用 [<icon-svg name="canyin"></icon-svg>]
*
* 注意:
* 1. 通过2-2 添加icons, getNameList方法无法返回对应数据
*/
import Vue from 'vue'
import IconSvg from '@/components/icon-svg'
import './iconfont.js'
//全局注入
Vue.component('IconSvg', IconSvg)
const svgFiles = require.context('./svg', true, /\.svg$/)
const iconList = svgFiles.keys().map(item => svgFiles(item))
export default {
// 获取图标icon-(*).svg名称列表, 例如[shouye, xitong, zhedie, ...]
getNameList () {
return iconList.map(item => item.default.id.replace('icon-', ''))
}
}
在main.js 中引入
图片.png3.在页面中使用
name的值,为svg下面的静态svg图片名称
<icon-svg name="zhedie"></icon-svg>