Vue.js组件化开发

238 阅读10分钟

Vue.js组件

组件是可复用的 Vue 实例。组件用于封装页面的部分功能,将功能的结构、样式、逻辑代码封装为整体。这提高了功能的复用性及可维护性,使我们更好的专注于业务逻辑。

组件使用时为自定义的HTML标签形式,通过组件名作为自定义标签名。

<div id="app">
  <!-- 普通 p 标签 -->
  <p>这是p标签</p>
  <!-- vue.js 组件 -->
  <my-component></my-component>
</div>

官方文档

组件注册

即组件创建。

全局注册

全局注册的组件可以用于任意的vue实例或组件中。

// 全局组件注册,必须放在根实例创建之前
Vue.component('my-component', { 
  template: '<div>这是我们全局注册的组件</div>'
})

// 根实例
new Vue({
  el: '#app',
  data: {
  }
})

使用

<!-- 这里可以改成app2等其他任意vue实例 -->
<div id="app">
  <p>这是p标签</p>
  <!-- vue.js 组件 -->
  <my-component></my-component>
</div>

组件基础

组件本质上就是可复用的vue实例,所以它们可与 new Vue 接收相同的选项,例如data、methods以及生命周期钩子等。但 el 这样的选项是根实例特有的,组件无法接收 el 选项。

组件命名规则

第一种(短横线分割),kebab-case: 'my-component'

第二种(首字母大写,驼峰命名),PascalCase: 'MyComponent'

// kebab-case 进行注册
Vue.component('my-com-a', {
  template: '<div>这是a组件的内容</div>'
});
// PascalCase 进行注册
Vue.component('MyComB', {
  template: '<div>这是b组件的内容</div>'
});

但无论采用哪种书写方式,在DOM中只有kebab-case方式可以使用。

<!-- 组件使用 -->
<my-com-a></my-com-a>
<my-com-b></my-com-b>

template 选项

template 选项用于设置组件的结构,最终被引入根实例或者其他组件中。

Vue.component('MyComA', {
  template: `
    <div>
      这是组件 A 的内容: {{ 1 + 2 * 3 }}
    </div>
  `
})

data 选项

data 选项用于存储组件的数据,与根实例不同,组件的data 选项必须是函数,数据设置在函数返回值中。这种实现方式是为了确保每个组件实例都可以维护各自一份返回对象的拷贝,不会互相影响。

Vue.component('MyComA', {
  template: `
    <div>
      <h3>{{ title }}</h3>
      <p>{{ content }}</p>
    </div>
  `,
  data () {
    return {
      title: '这是组件标题',
      content: '这是组件内容'
    }
  }
})

局部注册

局部注册的组件只能在当前实例中使用。

new Vue({
  el: '#app',
  data: {
  },
  components: {
    'my-com-a': {
      template: `
        <div>
          <h3>{{ title }}</h3>
          <p>{{ content }}</p>
        </div>
      `,
      data () {
        return {
          title: '组件 A 标题',
          content: '组件 A 内容'
        }
      }
    },
  }
});

使用

<div id="app">
  <my-com-a></my-com-a>
</div>

单独配置组件的选项对象

如果局部组件多了起来,当要修改组件选项内容时,则不是很方便。这时候,我们将组件选项对象单独提取出来进行配置就方便很多,有助于提高可维护性。

// 组件 A 的选项对象
var MyComponentA = {
  template: `
    <div>
      <h3>{{ title }}</h3>
      <p>{{ content }}</p>
    </div>
  `,
  data () {
    return {
      title: '组件 A 标题',
      content: '组件 A 内容'
    }
  }
};
// 组件 B 的选项对象
var MyComponentB = {
  template: `
    <div>
      <h3>{{ title }}</h3>
      <p>{{ content }}</p>
    </div>
  `,
  data () {
    return {
      title: '组件 B',
      content: '组件 B 内容'
    }
  }
}

new Vue({
  el: '#app',
  data: {
  },
  components: {
    'my-component-a': MyComponentA,
    MyComponentB  //ES 语法表示 MyComponentB: MyComponentB
  }
})

组件通信

组件通信,是指在组件间进行传递数据的操作。

父组件向子组件传值

通过子组件的 props 选项接收父组件的传值。

// 创建子组件
Vue.component('my-component-a', {
  // props 选项不要与data 选项有同名属性,否则会出现覆盖问题
  props: ['title', 'content'],
  template: `
    <div>
      <h3>{{ title }}</h3>
      <p>{{ content }}</p>
    </div>
  `
})

new Vue({
  el: '#app',
  data: {
    item: {
      title: '这是示例标题',
      content: '这是示例内容'
    }
  }
})

使用props 选项接收数据

<!-- 静态属性设置 -->
<my-component-a 
  title="这是静态的标题"
  content="这是静态的内容"
></my-component-a>

<!-- 动态属性绑定:常用操作 -->
<my-component-a
  :title="item.title"
  :content="item.content"
></my-component-a>

Props命名规则: 建议 prop 命名使用 camelCase(首字母小写,驼峰命名),父组件绑定时使用 kebab-case(短横线 - 分割)

官方文档

单向数据流

父子组件间的所有 prop 都是单向下行绑定的,这称为组件prop的单向数据流特性。即父组件值改变则子组件相应prop值改变,反之,子组件相应prop值改变不会影响父组件值。

如果子组件要处理 prop 数据,应当存储在 data 中再操作,避免直接对 prop 数据进行处理。

Vue.component('my-component', {
  props: ['innerTitle'],
  template: `
    <div>
      <h3>{{ title }}</h3>
    </div>
  `,
  data () { 
    return { 
      title: this.innerTitle 
    } 
  }
})

注意:如果 prop 为数组或对象时,子组件对其操作会影响到父组件的状态。因为数组或对象由父组件传入时,传递的是地址,由于 prop 单向数据流特性,我们虽然不能更改数组或对象的地址引用,却可以更改其内部的属性值,这导致父组件也受到影响。

有两种方案杜绝这种影响:

  • 在传入数组或对象时,将其拷贝存储在 data 中,然后再操作
  • 避免传入数组,直接传入子组件需要的值

props 类型

在声明 props 时,还能设置可接收值的类型,假如传入值不符合设置的类型,Vue就会报错。

props: {
  parStr: String,
  parNum: Number,
  parArr: Array,
  parObj: Object,
  parAny: undefined, // null
  parData: [String, Boolean]
},

props 验证

当 prop 需要设置多种规则时,可以将 prop 的值设置为选项对象。

props: {
  // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  propA: Number,
  // 多个可能的类型
  propB: [String, Number],
  // 必填的字符串
  propC: {
    type: String,
    required: true
  },
  // 带有默认值的数字
  propD: {
    type: Number,
    default: 100
  },
  // 带有默认值的对象
  propE: {
    type: Object,
    // 对象或数组默认值必须从一个工厂函数获取
    default: function () {
      return { message: 'hello' }
    }
  },
  // 自定义验证函数
  propF: {
    validator: function (value) { 
      // 返回值为false时,vue会提示报错
      // 这个值必须匹配下列字符串中的一个
      return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
  }
},

注意:验证函数中,无法使用实例的data、methods等功能。因为验证过程在实例创建完成之前。

非 props 属性

当父组件给子组件设置了属性,但此属性在 props 中不存在,这时会自动绑定到子组件的根元素上。

如果组件根元素已经存在对应属性,则会替换组件内部的值。class 与 style 属性是例外,当内外都设置时,属性会自动合并。

如果不希望继承父组件设置的属性,可以设置 inheritAttrs: false,但只适用于普通属性,class 与 style 不受影响。

Vue.component('MyComponent', {
  inheritAttrs: false,
  template: `
  <div data-index="6"
       title="旧的title"
       class="abc" 
       style="width: 200px;">
    <p>这是组件的内容</p>
  </div>
  `
});

传值

<div id="app">
  <my-component
    data-index="3"
    :title="'示例标题内容'"
    style="height: 200px;"
    class="colorRed"
  ></my-component>
</div>

子组件向父组件传值

当父组件传值后,子组件经过处理,可能还需要将处理后的结果返回给父组件。比如,商品为子组件,购物车为父组件,父组件需要获取商品个数,就需要子组件在个数变化时传值给父组件。

子组件向父组件传值需要通过 $emit() 触发自定义事件,父组件通过监听自定义事件来获取值。自定义事件命名方式建议采用 kebab-case(短横线分割)。

子组件

Vue.component('ProductItem', {
  props: ['title'],
  template: `
    <div>
      <span>商品名称: {{ title }}, 商品个数: {{ count }}</span>
      <button @click="countIns1">+1</button>
      <button @click="countIns5">+5</button>
    </div>
  `,
  data () {
    return {
      count: 0
    }
  },
  methods: {
    countIns1 () {
      // 1 为 需要传的值
      this.$emit('count-change', 1);
      this.count++;
    },
    countIns5 () {
      // 5 为 需要传的值
      this.$emit('count-change', 5);
      this.count += 5;
    }
  }
});

父组件

new Vue({
  el: '#app',
  data: {
    products: [
      {
        id: 1,
        title: '苹果一斤'
      },
      {
        id: 2,
        title: '香蕉一根'
      },
      {
        id: 3,
        title: '橙子一个'
      }
    ],
    totalCount: 0
  },
  methods: {
    onCountChange (productCount) {
      this.totalCount += productCount;
    }
  }
});

传值

<div id="app">
  <h3>购物车</h3>
  <product-item
    v-for="product in products"
    :key="product.id"
    :title="product.title"
    
    @count-change="onCountChange"
  ></product-item>
  <p>商品总个数为:{{ totalCount }}</p>
</div>

组件 与 v-model

v-model 用于组件时,需要通过props与自定义函数实现。

子组件

// 子组件
var ComInput = {
  props: ['value'],
  template: `
    <input
      type="text"
      :value="value"
      @input="onInput"
    >
  `, // @input="$emit('input', $event.target.value)"
  methods: {
    onInput (event) {
      this.$emit('input', event.target.value)
    }
  }
}

父组件

new Vue({
  el: '#app',
  data: {
    iptValue: ''
  },
  components: {
    ComInput
  }
});

传值

<div id="app">
  <p>输入框内容为:{{ iptValue }}</p>
  <com-input v-model="iptValue"></com-input>
</div>

非父子组件传值

第一种方法是通过父组件中转数据:

兄弟组件B->父组件A->兄弟组件C

但当组件嵌套关系复杂时,这种中转传值方式就较为繁琐。

第二种方法,EventBus,也叫事件总线

EventBus

EventBus(事件总线)是一个独立的事件中心,用于管理不同组件间的传值操作。

EventBus 通过一个新的 Vue 实例来管理组件传值操作,组件通过给实例注册事件、调用事件来实现数据传递。发送数据的组件触发bus事件,接收的组件给bus注册对应事件。

新建EventBus.js文件

// EventBus.js 文件内容
const bus = new Vue()

引入EventBus.js

<script src="lib/vue.js"></script>
<!-- EventBus.js放在vue.js引入之后 -->
<script src="EventBus.js"></script>

组件A

// 商品组件
Vue.component('ProductItem', {
  template: `
    <div>
      <span>商品名称:苹果,商品个数:{{ count }}</spa
      <button
        @click="countIns"
      >+1</button>
    </div>
  `,
  data () {
    return {
      count: 0
    }
  },
  methods: {
    countIns () {
      // 给bus触发自定义事件,传递数据
      bus.$emit('countChange', 1);
      this.count++;
    }
  }
});

组件B

// 计数组件
Vue.component('ProductTotal', {
  template: `
    <p>总个数为: {{ totalCount }}</p>
  `,
  data () {
    return {
      totalCount: 0
    }
  },
  created () {
    // 给 bus 注册事件,并接收数据
    bus.$on('countChange', (productCount) => {
      // 实例创建完毕,可以使用 data 等功能
      this.totalCount += productCount;
    });
  }
})

根实例

// 根实例
new Vue({
  el: '#app',
  data: {
  }
});

传值,EventBus可以实现任意组件间的传值,这里只是用兄弟组件做示例

<div id="app">
  <h3>购物车</h3>
  <product-item></product-item>
  <product-total></product-total>
</div>

其他通信方式

有两种可以直接操作其他组件的方式:

  • $root

    用于访问当前组件树根实例,也可用于组件间传值。

    不建议平常使用,一般只用于小的测试组件中。

    官方文档

    另外$parent$children$root类似,用于便捷访问父子组件。也不建议平常使用。

  • $refs

    ref属性,为HTML标签或组件赋予一个 ID 引用,用于在 JavaScript 里直接访问。

    $refs,页面渲染后用于获取设置了ref属性的HTML标签或子组件。

    官方文档

    组件

    var ComA = {
      template: `<div>组件A的内容:{{ value }}</div>`,
      data () {
        return {
          value: '示例内容'
        }
      }
    }
    
    var vm = new Vue({
      el: '#app',
      methods: {
        fn () {
          this.$refs.comA.value = '新的内容'
        }
      },
      components: {
        ComA
      },
      // 页面渲染后执行
      mounted () {
        console.log(this.$refs)
        this.$refs.comA.value = "修改后的内容"
      },
    })
    

    HTML

    <div id="app">
      <!-- 给子组件设置 ref 属性 -->
      <com-a ref="comA"></com-a>
      <button @click="fn">按钮</button>
    </div>
    

组件插槽

组件插槽可以便捷的设置组件内容。

<!-- 没有组件插槽的组件无法像普通HTML标签那样设置内容 -->
<com-a></com-a>

<!-- 有组件插槽的组件可以便捷的设置组件内容 -->
<com-b>这是有插槽的组件</com-b>

单个插槽

需要使用<slot></slot>进行插槽设置。

Vue.component('ComA', {
  template: `
    <div>
      <h3>组件标题</h3>
      <slot>
        这是插槽的默认内容
      </slot>
    </div>
  `,
  data () {
    return {
      value: '子组件的数据'
    }
  }
});

设置组件内容

<div id="app">
  <com-a>这是组件的内容</com-a>
  <com-a>
    这是第二个组件的内容:
    <span>这是span的内容</span>
  </com-a>
  <com-a>
    这里是父组件的视图模板,只能使用父组件的数据:
    {{ parValue }}
  </com-a>
  <com-a></com-a>
</div>

具名插槽

如果组建中有多个位置需要设置插槽,根据需要给<slot>设置name,这种有名称的插槽称为具名插槽。

如果<slot>未设置name,则默认name为default。

// 子组件 
Vue.component('ComA', {
  template: `
    <div>
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
  `
});

设置组件内容

<div id="app">
  <com-a>
    <template v-slot:header>
      <h3>组件的头部内容</h3>
    </template>
    <!-- <template v-slot:default>
      <p>组件的主体内容1</p>
      <p>组件的主体内容2</p>
    </template> -->
    <!-- 等同于 上面注释部分 -->
    <p>组件的主体内容1</p>
    <p>组件的主体内容2</p>
    <!-- v-slot: 可简写为 # -->
    <template #footer>
      <p>组件底部内容</p>
    </template>
  </com-a>
</div>

作用域插槽

一般插槽只能使用父组件数据,而作用域插槽可以使用子组件数据。

组件将需要被插槽使用的数据通过v-bind绑定给<slot>,这种用于给插槽传递数据的属性称为插槽prop。

// 子组件B的选项对象
var ComB = {
  template: `
    <div>
      <p>组件B的内容: </p>
      <slot
        :value="value"
        :num="num"
      ></slot>
    </div>
  `,
  data () {
    return {
      value: '这是组件B内部的数据',
      num: 200
    }
  }
}

// 子组件A的选项对象
var ComA = {
  template: `
    <div>
      <p>组件A的内容: </p>
      <slot
        v-bind:value="value"
        :num="num"
      ></slot>
      <slot name="footer"
        :value="value"
      ></slot>
    </div>
  `,
  data () {
    return {
      value: '这是组件A内部的数据',
      num: 100
    }
  }
}

设置组件内容

<div id="app">
  <!-- 多个插槽的作用域插槽书写方式 -->
  <com-a>
    <template v-slot:default="dataObj">
      {{ dataObj.value }}
      {{ dataObj.num }}
    </template>
    
    <template v-slot:footer="dataObj">
      {{ dataObj.value }}
    </template>
  </com-a>
  <!-- 只具有默认插槽的作用域插槽书写方式 -->
  <!-- <com-b v-slot="dataObj"> -->
  <com-b #default="dataObj">
    {{ dataObj }}
  </com-b>
  <!-- 通过 ES6 的解构操作接收作用域插槽的数据 -->
  <com-b v-slot="{ value, num }">
    {{ value }}
    {{ num }}
  </com-b>
</div>

内置组件

动态组件

动态组件适用于多个组件频繁切换的处理。例如选项卡切换等。

<component>用于将一个‘元组件’渲染为动态组件,以is属性决定渲染哪个组件。

<!-- component 设置动态组件 -->
<component :is="currentCom"></component>

我们动态地改变currentCom指向的组件就可以实现组件切换了。

<div id="app">
  <!-- 按钮代表选项卡的标题功能 -->
  <button
    v-for="title in titles"
    :key="title"
    @click="currentCom = title"
  >
    {{ title }}
  </button>
  <!-- component 设置动态组件 -->
  <component :is="currentCom"></component>
</div>
<script src="lib/vue.js"></script>
<script>
  // 设置要切换的子组件选项对象
  var ComA = {
    template: `<p>这是组件A的内容:<input type="text"></p>
  };  
  var ComB = {
    template: `<p>这是组件B的内容:<input type="text"></p>
  };
  var ComC = {
    template: `<p>这是组件C的内容:<input type="text"></p>
  };
  new Vue({
    el: '#app',
    data: {
      // 所有组件名称
      titles: ['ComA', 'ComB', 'ComC'],
      // 当前组件名称
      currentCom: 'ComA'
    },
    components: {
      ComA, ComB, ComC
    }
  });
</script>

keep-alive组件

在前面使用动态组件切换时,会频繁的进行重新渲染操作且不保留组件状态,假如我们希望动态组件切换时,避免重新渲染或保留组件状态,我们就可以使用keep-alive组件。

<!-- 通过 include 设置哪些组件会被缓存 -->
<!-- 字符串形式的逗号分隔符间不能有空格 -->
<!-- <keep-alive include="ComA,ComB,ComC"> -->
<!-- <keep-alive :include="['ComA', 'ComB', 'ComC']"> -->
<!-- <keep-alive :include="/Com[ABC]/"> -->
<!-- 通过 exclude 设置哪些组件不会被缓存 -->
<!-- <keep-alive exclude="ComD"> -->
<!-- <keep-alive :exclude="['ComD']"> -->
<!-- <keep-alive :exclude="/ComD/"> -->
   
<!-- 设置最大缓存组件数 -->
<keep-alive max="2">
  <!-- 动态组件 -->
  <component :is="currentCom"></component>
</keep-alive>

过渡组件

用于在Vue插入、更新或者移除DOM时,提供多种不同方式的应用过渡、动画效果。 主要有:

  • transition组件
  • 自定义过渡类名
  • transition-group组件

transition组件

用于给元素和组件提供进入或离开过渡:

  • 条件渲染(v-if)
  • 条件展示(v-show)
  • 动态组件
  • 组件根节点
<transition>
  <p v-if="show">hello world</p>
</transition>

组件提供了6个class,用于设置过渡的具体效果。

<style>
  /* 用于设置出场的最终状态 */
  .v-leave-to {
    opacity: 0;
  }
  /* 用于设置过渡的执行过程 */
  .v-leave-active {
    transition: opacity 1s;
  }
  /* 用于设置入场的初始状态 */
  .v-enter {
    opacity: 0;
  }
  /* 用于设置入场的最终状态 */
  .v-enter-to {
    opacity: 0.5;
  }
  /* 用于设置入场的过程 */
  .v-enter-active {
    transition: all 1s;
  }
</style>

transition组件相关属性

  • 给组件设置name属性,可用于给多个元素、组件设置不同的过渡效果,这时需要将v-更改为对应name-的形式。

    例如:

    <transition name="demo">的对应类名前缀为,demo-enter等。

  • 通过设置appear属性,可以让组件在初始渲染时实现过渡。 <transition appear">

自定义过渡类名

自定义类名比普通类名优先级更高,在使用第三方CSS动画库时非常有用。

用于设置自定义过渡类名的属性如下:

  • enter-class
  • enter-active-class
  • enter-to-class
  • leave-class
  • leave-active-class
  • leave-to-class

用于设置初始过渡类名的属性如下:

  • appear-class
  • appear-to-class
  • appear-active-class
<transition
  enter-active-class="test"
  leave-active-class="test"
>
  <p v-if="show">这是 p 标签</p>
</transition>

Animate.css

Animate.css 是一个第三方CSS动画库,通过设置类名来给元素添加各种动画效果。

Animate.css官网

CDN引用

<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<!-- 不需要添加 animate__ 前缀的兼容版本,但是官方建议使用上面的完整版本 -->
<!-- "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.0.0/animate.compat.css" -->

使用

<!-- 通过自定义过渡类名设置,给组件添加第三方动画库的类名效果 -->
<transition
  enter-active-class="animate__bounceInDown"
  leave-active-class="animate__bounceOutDown"
>
  <!-- 必须给要使用动画的元素设置基础类名 animate__animated -->
  <p 
    v-if="show"
    class="animate__animated"  
  >hello world</p>
</transition>

注意事项:

  • animate__前缀版本 与 compat版本的选择问题,推荐选择前缀版本
  • 基础类名 animate__animated 不能忘

transition-group组件

<transition-group>用于给系列元素设置过渡动画。

  • tag属性用于设置容器元素,默认为<span>
  • 过渡会应用于内部元素,而不是容器
  • 子节点须有独立的key,动画才能正常工作
<transition-group
  tag="ul"
>
  <li
    v-for="item in items"
    :key="item.id"
  >
    {{ item.title }}
  </li>
</transition-group>

当列表元素变更导致元素位移,可通过 .v-move 类名设置移动时的效果

.v-move {
  transition: all .5s;
}

其他过渡样式的设置和transition组件一样。