Vue 创建自己的组件(一)

2019-04-02  本文已影响0人  你在补考名单上

转载请注明出处和原作者:你在补考名单上

经过一顿复制粘贴之后,UI 框架提供的控件已经不能满足你了。这时你可能会尝试着自己封装一下需要的控件,但如果在入门的时候没有认真地学习每个知识点(就像我一样),运行起来就发现和预期的效果不一样。下面带着你一起来封装一个控件,顺带回顾一下知识点。

适合人群

  1. 刚学完 Vue,想实践一下。
  2. 学了一段时间,但是自己封装的时候不熟悉。
  3. 想快速学习 Vue。

本文主要内容

文章底部有完整代码


如果你更喜欢官方文档,可以看这里:组件注册

组件的作用就是将一段多次重复用到代码提取出来,减轻后期维护的工作量。考虑到这里,那么首先就得明确有那些变量是可以提取出来的。

以封装一个简单的列表控件为例子,一步步讲解如何创建自己的组件。这里我用的是 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 这里传入的是一个图片路径。

所以,写成(:srcv-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 放在同一个标签中,注意一下就可以。

总结

封装一个控件需要注意的地方:

  1. props 设计合理
  2. 传值时注意是表达式还是字符串

最后贴上所有代码:

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>

上一篇 下一篇

猜你喜欢

热点阅读