Vue 创建自己的组件(一)
经过一顿复制粘贴之后,UI 框架提供的控件已经不能满足你了。这时你可能会尝试着自己封装一下需要的控件,但如果在入门的时候没有认真地学习每个知识点(就像我一样),运行起来就发现和预期的效果不一样。下面带着你一起来封装一个控件,顺带回顾一下知识点。
适合人群:
- 刚学完 Vue,想实践一下。
- 学了一段时间,但是自己封装的时候不熟悉。
- 想快速学习 Vue。
本文主要内容
- v-bind
- props
文章底部有完整代码
如果你更喜欢官方文档,可以看这里:组件注册。
组件的作用就是将一段多次重复用到代码提取出来,减轻后期维护的工作量。考虑到这里,那么首先就得明确有那些变量是可以提取出来的。
以封装一个简单的列表控件为例子,一步步讲解如何创建自己的组件。这里我用的是 Android RecyclerView 使用的思路,先创建一个 Item View,然后放到一个 List 中。
先看看最终效果图:
preview.png
let’s do it.
Item View
首先要做的就是创建一个 Item View,它就是列表中的每一项。这个 Item 并不复杂,只有四个元素:图片,标题,内容,标签。
确定了设计后,提取公共的部分。把它们拿出来,放到 props 中。
props: {
img: String,
title: String,
desc: String,
tag: String,
activeTag: {
type: Boolean,
default: false,
}
},
props 和 v-bind
Vue.js: Prop
props 可以理解为构造方法的参数,在实例化的时候传入。有两种写法:
props: ['img', 'title', 'desc'],
props: {
img: String,
title: String,
desc: String,
},
但是第二种写法包含了类型检查,通常使用第二种。这里需要注意一下类型的首字母是大写,是 Javascript 的类型。
定义好 props 后,就可以在模板里使用了。给出模版的代码:
<template>
<div>
<div class="itemContainer">
<img class="img" :src="img">
<div class="content">
<div class="contentPanel">
<div class="title">{{title}}</div>
<div class="desc">{{desc}}</div>
</div>
<div class="tagPanel">
<span class="tag" :class="computedTag">{{tag}}</span>
</div>
</div>
</div>
</div>
</template>
可以看到,在模板中使用了 Mustache 语法来插值,这是最简单的文本赋值方式。
关于 v-bind
v-bind 是一个绑定 HTML 属性的指令,它的作用就是可以把动态地把一个属性插入到标签中。自定义的组件里声明的 props 就是这个属性,官方文档中提及了 props 的使用方式:如果要传入一个表达式,需要使用 v-bind 指令告诉 Vue 这是一个表达式而不是一个字符串。
大多情况下,<img/>
的src
是动态传入的。所以要使用 v-bind 告诉 Vue 这里传入的是一个图片路径。
所以,写成(:src
是v-bind:src
的简写):
<img class="img" :src="img"/>
这里要注意在传入图片路径时区分相对路径和绝对路径。如果是相对路径,要使用require()
,否则会获取不到正确的图片路径。
可变样式
tag 被设计成两种背景颜色。为了实现这个需求,需要使用:class
,这是一个常用的变换样式的写法。它表示动态地在现有的 class 中插入另一个 class。在这里传入了一个对象:computedTag。这个对象的定义:
computed: {
computedTag() {
if (this.activeTag === true) {
return "tagActive";
}
return "tagInactive";
}
},
可以看到这是个计算属性,通过计算 props 中 activeTag 的值来返回一个 class。最终效果就是:把 tagActive 或 tagInactive 添加到 class 中。另外,这里注意到计算属性中可以使用 props 中的对象。换句话说,props 和 data 中的对象可以在计算属性和方法中使用,但是要注意不要修改 props。具体的原因在官网中:
Vue.js: 单向数据流
小结
封装的核心就是 props 和 v-bind。可以联想到 Javascript 中把函数作为参数的传入方式,如果没有在函数名后加上()
,则表示传入一个函数对象,如果加上了()
则表示传入一个函数的返回结果。
Item View 已经封装好了,下面把它们放到一个 List 中。
List
先给出 List 的部分代码:
<template>
<div>
<div v-for="(item,index) in data" :key="item.id">
<HelloItem
:title="item.title"
:desc="item.desc"
:img="item.img"
:activeTag="item.activeTag"
:tag="item.tag"
/>
<div class="divider" v-if="showDivider && index !== data.length-1"/>
</div>
</div>
</template>
先关注<HelloItem/>
,这是上一步封装好的 Item View。如果把 v-bind 去掉,像这样:
<HelloItem
title="item.title"
desc="item.desc"
img="item.img"
activeTag="item.activeTag"
tag="item.tag"
/>
运行一下,会看到页面变成了这样:
去掉v-bind.png
这也作证了 v-bind 的用途:如果你需要插入变量,v-bind 是必不可少的。
渲染列表
v-for 用于渲染一个列表。这里的问题不多,主要还是 :key
的问题。如果少了 key
,会导致一个页面不会刷新的问题。当然这里还是看实际情况,我在实践过程中就遇到过类似的问题。最后是通过后台取了 id 来触发 Vue 的刷新。如果你在实践中遇到了不会刷新的问题,检查接口返回的数据没有问题后,就要检查是否添加了 key
。
渲染分割线
最后是一个分割线的内容。这里用了 v-if 来判断是否为最后一个 Item View,如果是最后一个则不渲染分割线。官方教程中提到不要把 v-for 和 v-if 放在同一个标签中,注意一下就可以。
总结
封装一个控件需要注意的地方:
- props 设计合理
- 传值时注意是表达式还是字符串
最后贴上所有代码:
HelloItem.vue
<template>
<div>
<div class="itemContainer">
<img class="img" :src="img">
<div class="content">
<div class="contentPanel">
<div class="title">{{title}}</div>
<div class="desc">{{desc}}</div>
</div>
<div class="tagPanel">
<span class="tag" :class="computedTag">{{tag}}</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
img: String,
title: String,
desc: String,
tag: String,
activeTag: {
type: Boolean,
default: false,
}
},
data() {
return {};
},
computed: {
computedTag() {
if (this.activeTag === true) {
return "tagActive";
}
return "tagInactive";
}
},
created() {},
methods: {}
};
</script>
<style scoped>
.itemContainer {
padding: 16px;
display: flex;
}
.img {
width: 48px;
height: 48px;
}
.content {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.contentPanel {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 16px;
}
.tagPanel {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.tag {
padding: 1px 6px;
border-radius: 4px;
color: white;
text-align: center;
font-size: 11pt;
}
.tagActive {
background-color: #df6b5c;
}
.tagInactive {
background-color: #888888;
}
.title {
font-size: 18px;
font-weight: 900;
}
.desc {
font-size: 14px;
color: #888888;
}
</style>
HelloList.vue
<template>
<div>
<div v-for="(item,index) in data" :key="item.id">
<HelloItem
:title="item.title"
:desc="item.desc"
:img="item.img"
:activeTag="item.activeTag"
:tag="item.tag"
/>
<div class="divider" v-if="showDivider && index !== data.length-1"/>
</div>
</div>
</template>
<script>
import HelloItem from "@/components/HelloItem.vue";
export default {
props: {
data: Array,
showDivider: {
type: Boolean,
default: true
}
},
components: {
HelloItem
},
methods: {},
computed: {},
methods: {}
};
</script>
<style>
.divider {
border: 0.5px solid #dfdfdf;
margin-left: 74px;
margin-right: 16px;
}
</style>
Home.vue
<template>
<div class="home">
<HelloList :data="mockDatas"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloList from "@/components/HelloList.vue";
export default {
name: "home",
components: {
HelloList
},
mounted() {
for (let i = 0; i < 5; i++) {
let activeTag = false;
if (i % 2 === 0) {
activeTag = true;
}
this.mockDatas.push({
title: `Title${i}`,
desc: `desc${i}`,
img: require("../assets/logo.png"),
activeTag: activeTag,
tag: "Tag"
});
}
},
data() {
return {
mockDatas: []
};
},
methods: {}
};
</script>