Vue组件化开发全面详解【Vue系列】

728 阅读13分钟

目录

一. 认识组件化

1.什么是组件化?

  • 引入: 人在面对复杂问题处理方式?

    • 下拉查看(理解)
    • 当面对一个非常复杂的问题时, 我们不太可能一次性搞定全部内容
    • 但是, 我们有一种天生的能力, 就是将问题进行拆解
    • 如果将一个复杂的问题, 拆分成很多个可以处理的小问题, 再将其放在整体当中, 你会发现大的问题也会迎刃而解
  • 组件化也是类似的思想:

    • 下拉查看(理解)
    • 如果我们将一个页面中所有的处理逻辑全部放在一起, 处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展
    • 但如果, 我们将一个页面拆分成一个个小的功能块, 每个功能块完成属于自己这部分独立的功能, 那么之后整个页面的管理和维护就变得非常容易了
image-20200514155749713
  • 我们将一个完成的页面分成多个组件
  • 每个组件都用于实现页面的一个功能
  • 而每一个组件又可以进行细分

2.Vue组件化思想

  • 组件是Vue.js​中重要思想
    • 它提供了一种抽象, 我们可以开发出一个独立可复用的小组件来构造我们的应用
    • 组件可以扩展 HTML 元素,封装可重用的代码
    • 组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树
  • 组件化思想应用
    • 有了组件化的思想, 我们之后开发中就要充分的利用它
    • 尽可能将页面拆分成一个个小的, 可复用的组件
    • 这样让我们代码更方便组织和管理, 并且扩展性也强



二. 注册组件

1.全局组件注册

  • 组件注册分为三个步骤

    1. 创建组件构造器对象 Vue.extend({ })

    2. 注册组件Vue.component('tagName',options)

    3. 使用组件 <my-cpn/>

  • 当我们通过调用Vue.component()注册组件时, 组件的注册是全局的

    • 该组件可以在任意Vue实例下使用
image-20200514164613042

2.局部组件注册

  • 如果我们注册的组件是挂载某个实例中, 那么就是一个局部组件
    • 只能在当前Vue实例管理下使用
  • 在Vue实例 options 中定义, components 带s
<div id="app">
 <my-component></my-component>
</div>
<script>
    // 注册
    var Child = {
     template'<div>A custom component!</div>'
    };
    // 创建根实例
    new Vue({
     el'#app',
     components: {
     // <my-component> 将只在父模板可用
     'my-component'Child
     }
    })
</script>

3.父组件和子组件

  • 前面我们看到了组件树
    • 组件和组件之间存在层级关系
    • 而其中一种非常重要的关系就是父子组件的关系
  • 父子组件的错误用法:
    • 当子组件注册到父组件的components时, Vue会编译好父组件的模块
    • 该模块的内容已经决定了父组件要渲染的HTML(相当于父组件已经有了子组件的内容了)
    • 子组件只能在父组件中被识别的

4.注册组件语法糖

  • 主要省去了Vue.extend()步骤, 而是可以用一个对象来替代
  1. 全局注册组件语法糖

    <cpn2></cpn2>
    // 语法糖写法
    Vue.component('cpn2', {
      template: `
        <div>
          <h2>我是标题22</h2>
          <div>我是内容,哈哈哈哈</div>
          <div>我是内容,呵呵呵呵</div>
        </div>
      `
    })
    
  2. 局部组件注册语法糖

    components: {
      'cpn3':{
        template: `
          <div>
            <h2>我是标题33</h2>
            <div>我是内容,哈哈哈哈</div>
            <div>我是内容,呵呵呵呵</div>
          </div>
      `
    })}
    

5.模板的分离写法

  • template中的HTML分离出来, 然后挂载到对应的组件, 结构会非常清晰
  • Vue提供了两种方案来定义HTML模板内容
    • 使用script: <script type="text/x-template" id="cpn1"></script>
    • 使用template: <template id="cpn2"></template>
    • template填写: 对应模板的id值

1.script模板示例
image-20200710212944143

2.template模板示例
image-20200710213942046



三. 组件中数据的存放

1.组件中数据的访问

  • 组件是一个单独功能模块的封装

    • 这个模块有属于自己的HTML模板, 也应该有自己的数据data
  • 组件中的可以访问顶层Vue实例中的数据吗?

    • 不能访问Vue实例中的数据, 即使能访问, 所有数据放在Vue实例中, Vue实例会变得非常臃肿

2.组件数据的存放

  • 组件的数据放在组件对象的data属性当中
    • 只是这个data属性必须是一个函数
    • 而且这个函数要返回一个对象
Vue.component('cpn', {
  template'#cpn',
  // 组件数据的存放
  data() {
    return {
      counter0
    }
  }
})

3.组件中data为什么必须是函数?

  • 如果在组件中data是一个对象会发生什么?❌
    • 多个组件会共用相同的数据, 数据发生改变后会相互影响
    • 最重要的是在组件中data是一个对象会报错, 可以返回一个对象的引用 ( 就是声明一个对象在data中引用 ,来实现数据互通 )
  • 如果在组件中data是一个函数👌
    • 因为组件可能被多处使用,但它们的data是私有的,所以每个组件都要 return 一个新的data对象,如果共享data,修改其中一个会影响其他组件
    • 组件之间的数据都是相互独立存在的, 数据改变后, 不会相互影响



四. 组件的通信

1.父子组件的通信

  • 在上面, 我们提到了子组件是不能引用父组件或Vue实例的数据的
  • 但是, 开发中, 往往一些数据确实需要从上层传递到下层
    • 比如在一个页面中, 我们从服务器请求到了很多数据
    • 其中一部分数据, 并非是我们整个页面大的组件来展示的, 而是需要下面的子组件来进行展示的
    • 这个时候, 不会让子组件再放一次网络请求, 而是让大组件(父组件)将数据传递给小组件(子组件)
  • 如何进行父子组件之间的通信呢?
    • 通过props向子组件传递数据
    • 通过自定义事件向父组件发送消息
    • 引用官网的一句话:父子组件的关系可以总结为 props 向下传递,事件向上传递

2.非父子组件通信(事件总线)

  • 有时候两个组件也需要通信(非父子关系)
  • 在简单的场景下,可 以使用一个空的Vue实例作为中央事件总线:
let $bus new Vue()

// 触发组件 A 中的事件
$bus.$emit('event'1)

// 在组件 B 创建的钩子中监听事件
$bus.$on('event',id => {
    // ...
})

3.祖先和子孙组件通信(provide/inject)

  • provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide提供的数据或方法
// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo'bar'
  },
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo// => "bar"
  }
}

4.组件间的通信(图示)

组件间的通信
组件间的通信



五. 父组件向子组件传值

1.props基本用法

在子组件中, 使用选项props用于接收来自父组件的数据

  • 父组件如何向子组件传递数据?
    • 1.在子组件定义props属性, 值可以是 数组 | 对象
    • 2.在使用子组件时, 用v-bind绑定属性将父组件的值传递给 子组件props 定义的属性
  • props的值有两种方式:
    • 方式一: 字符串数组, 数组中的字符串就是传递来的值, 使用时的名称
    • 方式二: 对象, 对象可以类型检测, 自定义验证, 默认值等.

2.props数据校验

属性 描述
type 检测是否是给定的类型, 否则抛出警告, 支持StringNumberBooleanArrayObjectDateFunctionSymbol、任何自定义构造函数、或上述内容组成的数组
default:any 为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。
required:Boolean 定义该 prop 是否是必填项
validator:Function 自定义验证函数会将该 prop 的值作为唯一的参数代入
const cpn = {
  template'#cpn',
  data() { return {} },
  // props: ['cmovies', 'cmessage']
  props: {
    //1.可以类型限制
     heightNumber,
    //2.提供默认值,以及必传值
    cmessage: {
      typeString,
      default'aaa',
      requiredtrue
    },
    //3.类型是对象或者数组时,定义默认值必须是一个函数
    cmovies: {
      typeArray, 
      default() {
        return ['a','b']
      },
     //4.自定义验证
      age: {
        validatorfunction (value) {
          return value >= 0
        }
     }
  }
}
  • 注意: 在props定义的属性中使用驼峰形式,模板中需要使用短横线的形式, 使用组件时驼峰标识的前用 - 代替



六. 子组件向父组件传值

1. 自定义事件

props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。

我们应该如何处理呢?这个时候,我们需要使用**自定义事件**来完成。

  • 什么时候需要自定义事件?
    • 当子组件需要向父组件传递数据时, 我们就要用到自定义事件了
    • 之前学习的v-on不仅可以监听DOM事件, 也可以用于组件间的自定义事件
  • 自定义事件流程
    1. 子组件中, 将数据传递给父组件, 通过**this.$emit('eventName', [...args])** 触发当前实例上的事件, 附加参数都会传给监听器回调

    2. 在使用子组件时, 监听在$emit参数中定义的事件, 值是定义在实例中定义的事件, 触发后实例方法中接收子组件的值

      • <cpn @eventNamek="cpnClick"></cpn>
  • 注意: $emit参数中的事件名不能是驼峰命名
<!-- 2.在使用组件时,监听定义的事件,并在父组件中定义接收 -->
    <cpn @item-Click="cpnClick"></cpn>

  <template id="cpn">
    <div>
      <button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
    </div>
  </template>
<script>
    const cpn = {
      template'#cpn',
      data() {
        return {
          categories: [
            { id'aaa'name'热门推荐' },
            { id'bbb'name'手机数码' },
            { id'ccc'name'家用家电' },
            { id'ddd'name'电脑办公' },
          ]
        }
      },
      methods: {
        btnClick(item) {
    /// 1.在子组件中点击触发获得到的值,发射/触发事件事件
          this.$emit('item-click', item)
        }
      },
    }
    
    new Vue({
      el'#app',
      components: {
        cpn
      },
      methods: {
        //  3.在父组件中方法接收数据
        cpnClick(item) {
          console.log(item);
        }
      }
    })
  </script>

2. sync修饰符

  • 使用 .sync修饰符 场景: 子组件想修改父组件传递的数据时
    • 在使用改子组件时, 使用 .sync 修饰符
    • 使用该组件时不需要在父组件中监听事件, 改变数据
<!-- 父组件 -->
<demo :money="money" @update:money="fn"></demo>
<!-- 作用相等,使用: .sync 修饰符,省略了定义事件,由子组件来修改数据(语法糖) -->
<demo :money.sync="money"></demo>


<!-- demo子组件-->
<script>
export default {
props: {
money: Number
},
methods: {
changeMoney() {
this.$emit('update:money',200)
}
}
}
</script>.

<!-- demo子组件--> <script> export default { props: { money: Number }, methods: { changeMoney() { this.$emit('update:money',200) } } } </script>.




七. 父子组件的访问

1.父访问子: $children

有时候我们需要父组件直接访问子组件对象, 或子组件直接访问父组件, 或子组之间访问根组件

  • 父访问子组件: 使用$children$refs (reference 引用)
  • 子组件访问父组件: 使用$parent
  • $children的访问(不常用):
    • this.children获取的是一个数组类型, 它包括所有子组件对象
methods: {
    btnClick() {
      // 1.$children: 获取所有子组件对象
       console.log(this.$children);
       console.log(this.$children[2].name);
       this.$children[2].cmessage();
       // 遍历"所有子组件"
       for (const item of this.$children) {
         console.log(item.name);
         item.cmessage()
       }
    }
  }

2.父访问子: $refs

  • $children的缺陷:

    • 通过$children访问子组件时, 是一个数组类型, 访问其中的子组件必须通过索引
    • 当子组件过多, 或在子组件前新添加的了组件, 往往不能确认拿到目标子组件的索引值
    • 有时候, 想获取明确的其中一个特定的组件时, 可以用$refs
  • **$refs**的使用(常用):

    • $refs 和 ref指令通常是一起 使用的

      1. 首先给, 目标子组件添加ref绑定一个id

      2. 其次在父组件中, 通过this.$refs.id就可以访问到该组件了

<child-cpn1 ref="child1"></child-cpnl>
<child-cpn2 ref="child2"></child-cpn2>
<button @click="showRefscpn">通过refs访问子组件</button>
/* Vue的methods中 */
showRefscpn(){
    // 获取子组件中的message的值
    console.log(this.Srefs.child1.message);
    console.log(this.Srefs.child2.message);
}

3.子访父: $parent

  • 在子组件中访问父组件: $parent (尽量避免使用)
  • 在子组件中访问根组件: $root (尽量避免使用)
  • 注意事项
    • 尽管 Vue 在开发中, 允许我们通过 $parent 来访问父组件, 但开发中尽量不要这么做
    • 子组件尽量避免直接访问父组件的数据, 因为这样耦合度太高
    • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性, 往往会引起问题。
    • 另外, 更不好做的是通过$parent直接修改父组件的状态, 那么父组件中的状态将变得飘忽不定, 很不利于我的调试和维护



八. 插槽slot

1.为什么使用插槽

  • slot翻译为插槽:
    • 插槽的目的是让我们原来的设备具备更多的扩展性
    • 比如电脑的USB接口我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
  • 组件的插槽:
    • 组件的插槽也是为了让我们封装的组件更加具有扩展性
    • 让使用者可以决定组件内部的一些内容到底展示什么。
  • 举个🌰: 移动网站中的导航栏
    • 移动开发中, 几乎每个页面都有导航栏
    • 导航栏我们必然会封装成一个插件, 比如nav-bar组件。
    • 一旦有了这个组件, 我们就可以在多个页面中复用了。
  • 总结: Slot 通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot 位置)

2.如何封装这类组件

如何封装合适呢? 抽取共性,保留不同。 最好的封装方式就是将共性抽取到组件中, 将不同内容的地方暴露为插槽。

一旦我们预留了插槽, 就可以让使用者根据自己的需求, 决定插槽中插入什么内容。 是搜索框, 还是文字, 还是菜单。由调用者自己来决定。

3. slot 基本使用

  • 基本使用slot
    • 在子组件中, 定义一个<slot></slot>就可以为组件开启一个插槽
    • 该插槽插入什么内容取决于父组件如何使用 👇👇👇
image-20200522214331596

4. slot 插槽默认值

有时候我们需要给插槽设置一个具体的默认内容,它只会在没有提供内容的时候就会被渲染

<template id="cpn">
    <div>
      <!-- 当使用子组件没有提供内容时候,就会渲染默认内容:按钮 -->
      <slot><button>按钮</button></slot>
    </div>
</template>

4.具名插槽 slot

  • 当子组件有多个插槽时, 想替换掉指定的插槽怎么办?
    • 对于这样的情况, <slot>元素有一个特殊的 attribute: name
    • 在向具名插槽提供内容的时候, 我们在template元素使用v-slot指令, 并以v-slot的参数形式提供其名称, v-slot:header
<cpn>
  <!-- 向具名插槽提供内容, 我们在template元素上使用 v-slot 指令 -->
  <template v-slot:header>
    <h1>Here might a page title</h1>
  </template>

  <!-- 默认会填没有具名插槽的坑 -->
  <template v-slot:default>
    <p>我是填的主要内容元素的坑</p>
    <p>主要内容</p>
  </template>

  <template v-slot:footer>
      <p>这里是页脚</p>
  </template>
</cpn>

<!-- 模板内容👇👇👇 -->
<template id="cpn">
    <div>
      <header>
        <!-- 我们希望页头放在这里 -->
        <slot name="header"></slot>
      </header>
      <main>
        <!-- 我们希望主要内容放在这里 -->
        <!-- 不带 name 的 slot 默认name包含 "default" -->
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
        <!-- 我们希望页脚放在这里 -->
      </footer>
    </div>
</template>
  • 一个不带name<slot>出口会带有隐含(默认)的名字 "default"

5.编译作用域

准则: 父组件模板的所有东西都会在父级作用域内编译; 子组件模板的所有东西都在子级作用域内编译

展开查看案例 编译作用域

6.作用域插槽

  • 使用场景

    • 在父组件中展示子组件的内容, 对子组件内容展示不满意
    • 在其插槽中获取子组件展示的数据, 进行重新修改内容重新展示
  • 如何使用

    1. 在子组件插槽中绑定自定义属性用来要展示的数据(供父作用域访问的数据) 在定义子组件中:
    <template id="cpn">
        <div>
          <span>
            <!-- 为了让 user 在父级插槽中可用, 我们将 user 作为 <slot> 元素的一个属性绑定 -->
            <slot :user="user">{{user.lastName}}</slot>
          </span>
        </div>
    </template>
    
    1. 在使用子组件中, 获取传递过来的数据, 进行修改展示 使用子组件中:
      <!-- 在使用子组件中, 对组件展示的内容不满意, 在父作用域对子组件展示的内容进行修改 -->
        <current-user>
          <!-- 在这里想访问子组件中的 user 数据  -->
          <template v-slot:default="slotProps">
            {{slotProps.user.firstName}}
          </template>
        </current-user>
    
    • v-slot的使用范围: v-slot只能在组件中 或 template元素中使用

    • 备注
    • 1.v-slot 指令自Vue 2.6.0起被引入,提供更好的支持v-slotslot-scope attribute的 API 替代方案

    • 2.在接下来所有的2.x版本中v-slotslot-scope attribute仍会被支持,但已经被官方废弃且不会出现在Vue3中
  • 总结

    • 有时让插槽内容能够访问子组件中才有的数据是很有用的
    • 父组件替换插槽的标签,但是内容由子组件来提供

独占默认插槽

当提供的内容只有默认插槽时, 组件的标签才可以被插槽的模板来使用

这样我们就可以把v-slot直接写在组件上

  • 不带参数的v-slot对应默认插槽
<current-user v-slot="slotProps">
  {{slotProps.user.lastName}}
</current-user>
  • 注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确

解构插槽prop

slot的值可以是任何能够作为函数形参数的 JavaScript 表达式, 可以使用 ES6 解构来传入具体的插槽

<!-- 1."解构"传递来的对象 -->
<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

<!-- 2.重命名(起别名), 将 user 重命名为 person  -->
<urrent-user v-slot="{ user: person }">
  {{person.firstName}}
  {{person.firstName}}
</urrent-user>

7.动态插槽名

动态指令参数也可以用在v-slot上,来定义动态的插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

8.具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写, 即把参数之前的所有内容(v-slot:)替换为字符 #例如 v-slot:header 可以被重写为 #header

<cpn>
<!-- 向具名插槽提供内容, 使用 # 简写形式(代替: v-slot )  -->
  <template #header>
    <h1>Here might a page title</h1>
  </template>
<!-- 如果你希望使用缩写的话,你必须始终以明确插槽名取而代之: --> 
  <template #default="{ user }">
    {{ user.firstName }}
  </template>
</cpn>