vue组件最佳实践

写在前面

  • 知识就是力量。 ——安琪拉
  • 知识就是力量,但更重要的是运用知识的技能。 —— 培根
  • 本文主要介绍vue组件间常见的通信方式,边界情况及插槽的使用。

何为组件化?

vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界 面都可以抽象为一个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护 性,便于多人协同开发。

通常一个应用会以一棵嵌套的组件树的形式来组织:例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。如下图所示:


组件树

1.组件常用通信方式

  1. 父传子:props
  2. 子传父:自定义事件 event
  3. 跨层级通信:事件总线 Bus
  4. 跨层级通信:专为 Vue.js 应用程序开发的状态管理模式 vuex

1.1 props

// 父组件
<Child1 msg="some message from parent"></Child1>

// Child1组件
<template>
    <div class="border">
        我是子组件1
        <div>{{msg}}</div>
    </div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      default: ''
    }
  },
  data () {
    return {}
  }
}
</script>

结果:
props 父传子

1.2 event

// 父组件
<template>
    <div>
        <h2>组件通信</h2>
        <!-- props 父传子 -->
        <Child1 msg="some message from parent"></Child1>
        <!-- 自定义事件 event 子传父 -->
        <Child2 @clickSon="getMsg"></Child2>
        <p>Child2传过来的值是: {{child2Info}}</p>
    </div>
</template>

<script>
import Child1 from './Child1'
import Child2 from './Child2'

export default {
  components: {Child1, Child2},
  data () {
    return {
      child2Info: ''
    }
  },
  methods: {
    getMsg (info) {
      this.child2Info = info
    }
  }
}
</script>


// Child2组件
<template>
    <div @click="$emit('clickSon', 'msg from child2')" class="border">
        我是子组件2
    </div>
</template>

<script>
export default {
  data () {
    return {}
  },
  mounted () {
    this.$bus.$on('send', (msg) => {
      console.log('Child1 点击了, 信息是:' + msg)
    })
  }
}
</script>

结果:


自定义事件

1.3 Bus

任意两个组件之间传值可以用事件总线的方式。事件总线其实利用的就是设计模式中观察者模式。其核心实现方式如下:

class Bus {
  constructor () {
    this.callbacks = {}
  }

  $on (name, fn) {
    this.callbacks[name] = this.callbacks[name] || []
    this.callbacks[name].push(fn)
  }

  $emit (name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach(cb => cb(args))
    }
  }
}

export default Bus

使用方式:

1.先在main.js中注册$bus

import Vue from 'vue'
import App from './App'
import router from './router'
import Bus from './bus'
Vue.prototype.$bus = new Bus()
// 实际上Vue已经实现了事件总线,我们使用的时候不用自定义Bus,直接使用如下方式引入即可
// Vue.prototype.$bus = new Vue()

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

2.在需要接受值的地方注册监听事件,这里已 Child1 与 Child2 之间的传值为例:

Child2

<template>
    <div @click="$emit('clickSon', 'msg from child2')" class="border">
        我是子组件2
    </div>
</template>

<script>
export default {
  data () {
    return {}
  },
  mounted () {
    // 组件挂载时即对send事件进行监听,监听到后打印信息
    this.$bus.$on('send', (msg) => {
      console.log('Child1 点击了, 信息是:' + msg)
    })
  }
}
</script>

Child1

<template>
    <div class="border">
        我是子组件1
        <button @click="$bus.$emit('send', '子组件1通过bus传来的值')">触发事件总线</button>
    </div>
</template>

结果:
点击Child1的button后,Child2打印:'Child1 点击了, 信息是: 子组件1通过bus传来的值'

1.4 vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。这里就不做举例啦~下期做个完成剖析给大家。
想学习的移步官网:vuex中文官方网站

2.边界情况

大多数情况下,并不推荐使用边界情况,而是使用更优的替代方案去代替边界情况。但是了解边界情况可以在理解源码封装组件化仓库中得到很大的便利。正如官网所说:在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

2.1 $root——访问根实例

在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问。

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

// 所有的子组件都可以将这个实例作为一个全局 store 来访问或使用:
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()

2.2 $parent——访问父级组件实例

兄弟组件之间通信可通过共同祖辈搭桥

// brother1
this.$parent.$on('foo', handle) 
// brother2
this.$parent.$emit('foo')

2.3 $children——访问所有子组件实例或子元素

父组件可以通过children访问子组件实现父子通信。注意:children不能保证子元素顺序。

  // parent 
  this.$children[0].xx = 'xxx'

2.4 $refs——访问子组件实例或子元素

获取子节点引用

// parent
<HelloWorld ref="hw"/>

mounted() { 
    this.$refs.hw.xx = 'xxx'
}

2.5 provide/inject——依赖注入

能够实现祖先和后代之间传值

// 祖先元素
provide() {
    return {foo: 'foo'}
}

// 后代
<template>
    <div class="border">
        <p>{{foo}}</p>
    </div>
</template>

<script>
export default {
  inject: ['foo']
}
</script>
// 也可以在后代元素中设置别名,以防命名冲突
 inject: {
    bar: {
      from: 'foo'
    }
 }
访问的时候: <p>{{bar}}</p>

3.非prop特性

用于隔代透传,只能传一代。

3.1 $attrs

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当一个组件没有 声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过 v- bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

// 祖先组件
 <Child2 msg="some message from parent"></Child2>
 
// Child2组件
<template>
    <div>
      <Grandson v-bind="$attrs"/>
    </div>
</template>

<script>
import Grandson from './Grandson'
export default {
  components: {Grandson},
  data () {
    return {}
  }
}
</script>

// 孙子组件
<template>
    <div class="border">
        <div>孙子组件</div>
        <div>父辈通过$attr透传过来的信息:{{msg}}</div>
    </div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String
    }
  }
}
</script>

结果:
在这里插入图片描述

3.2 $listeners

透传事件

// 祖先组件
 <Child2 @clickSon="getMsg"></Child2>
  methods: {
   getMsg (info) {
     console.log(info)
   }
 }
 
// Child2组件
<template>
    <div>
      <Grandson v-on="$listeners"/>
    </div>
</template>

<script>
import Grandson from './Grandson'
export default {
  components: {Grandson},
  data () {
    return {}
  }
}
</script>

// 孙子组件
<template>
    <div class="border" @click="$emit('clickSon', 'msg from grandSon')">
        <div>孙子组件</div>
    </div>
</template>

4.插槽

插槽语法是Vue实现的内容分发 API,用于复合组件开发。在通用组件库开发中有大量应用。

4.1 匿名插槽

// Layout
<div class="body">
  <slot></slot>
</div>

// parent
<Layout>
    <!-- 匿名插槽 -->
    <template>种一棵树最好是时间是十年前,而后是现在!</template>
</Layout>

4.2 具名插槽

// Layout
<div class="header">
  <slot name="header"></slot>
</div>

// parent
<Layout>
    <template v-slot:header>欢迎来到捡代码的小女孩的vue世界</template>
</Layout>

4.3 作用域插槽

// Layout
<div class="footer">
  <slot name="footer" :childValue="footerContent"></slot>
</div>

// parent
<Layout>
    <template v-slot:footer="value">{{value.childValue}}</template>
    <!-- 对象解构 -->
    <!-- <template v-slot:footer="{childValue}">{{childValue}}</template> -->
</Layout>

完整代码如下:

父组件

<template>
  <div>
    <h2>插槽</h2>
    <!-- 插槽 -->
    <Layout>
      <!-- 具名插槽 -->
      <template v-slot:header>欢迎来到捡代码的小女孩的vue世界</template>
      <!-- 匿名插槽 -->
      <template>种一棵树最好是时间是十年前,而后是现在!</template>
      <!-- 作用域插槽 -->
      <template v-slot:footer="value">{{value.childValue}}</template>
       <!-- 对象解构 -->
       <!-- <template v-slot:footer="{childValue}">{{childValue}}</template> -->
    </Layout>
  </div>
</template>

<script>
import Layout from './Layout.vue'
export default {
  name: 'Slot',
  components: {
    Layout
  }
}
</script>

Layout组件

<template>
  <div>
    <div class="header">
      <slot name="header"></slot>
    </div>
    <div class="body">
      <slot></slot>
    </div>
    <div class="footer">
      <slot name="footer" :childValue="footerContent"></slot>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      footerContent: '学习的敌人是自己的满足,要认真学习一点东西,必须从不自满开始'
    }
  }
}
</script>

<style scoped>
.header {
  background-color: rgb(252, 175, 175);
}
.body {
  display: flex;
  background-color: rgb(144, 250, 134);
  min-height: 100px;
  align-items: center;
  justify-content: center;
}
.footer {
  background-color: rgb(114, 116, 255);
}
</style>

效果如下:


插槽效果

这些就是组件化开发的必备知识啦 (〃'▽'〃)
get之后和我一起手写小组件吖~


The end~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容