Skip to content

Vue3组件化开发

组件化开发1

组件拆分

前面我们是将所有的逻辑放到一个 App.vue 中:

  • 在之前的案例中,我们只是 创建了一个组件 App
  • 如果我们一个应用程序 将所有的逻辑都放在一个组件 中,那么这个组件就会变成 常的臃肿和难以维护
  • 所以组件化的核心思想应该是 对组件进行拆分,拆分成 一个个小的组件
  • 将这些组件组合嵌套在一起,最终形成 我们的应用程序

image-20220707093057489

按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可

image-20220707093219386

组件通信

  • 可开启也可以不开启自动引入组件

image-20220707094757751

  • App 组件是 Header、Main、Footer 组件的父组件
  • Main 组件是 Banner、ProductList 组件的父组件

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

  • 比如 App 可能使用了多个 Header,每个地方的 Header展示的内容不同,那么我们就需要使用者 传递给 Header 一些数据,让其进行展示
  • 又比如我们在 Main 中一次性 请求了 Banner 数据和 ProductList 数据,那么就需要传递给它们来进行展示
  • 也可能是 子组件中发生了事件,需要 由父组件来完成某些操作,那就需要 子组件向父组件传递事件

父子组件之间如何进行通信:

  • 父组件传递给子组件:通过 props 属性
  • 子组件传递给父组件:通过 $emit 触发事件

image-20220707100425405

什么是 props

  • props 是你可以在组件上 注册一些自定义的 attribute
  • 父组件给 这些 attribute 赋值,子组件通过 attribute 的名称获取到对应的值

Props 用法

  • 方式一:字符串数组,数组中的字符串就是 attribute 的名称
  • 方式二:对象类型,对象类型我们可以在指定 attribute 名称的同时,指定它需要传递的类型、是否是必须的、默认值等等

数组用法

js
export default {
  props: ['title', 'content']
}

对象用法

  • 数组用法中我们 只能说明传入的 attribute 的名称,并 不能对其进行任何形式的限制

当使用对象语法的时候,我们可以对传入的内容限制更多:

  • 比如指定传入的 attribute 的类型
  • 比如指定传入的 attribute 是否是必传的
  • 比如指定没有传入时,attribute 的默认值
js
export default {
  props: {
    title: String,
    content: {
      type: String,
      required: true,
      default: '123'
    }
  }
}

Props 细节补充

type 类型

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

对象类型其他写法

js
export default {
  props: {
    // 基础的类型检查(null 和 undefined 会通过任何类型验证)
    propA: Number,
    // 多个可能得类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      default() {
        // 对象或数组默认值必须从一个工厂函数获取
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数——这是一个用作默认值的函数
      default() {
        return 'Default Function'
      }
    }
  }
}

prop 大小写命名

  • HTML 中的 attribute 名是大小写不敏感 的,所以 浏览器会把所有大写字符解释为小写字符
  • 这意味着当你使用 DOM 中的模板 时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名
html
<show-message  messageInfo="哈哈"></show-message>
<show-message  message-info="哈哈"></show-message>

非 prop 的 attribute

  • 当我们 传递一个组件某个属性,但是 该属性并没有定义对应的 props 或者 emits 时,就称为 非 Prop 的 Attribute
  • 常见的包括 class、style、id 属性

Attribute 继承

  • 当组件有单个根节点时,非 Prop 的 Attribute 将自动添加到根节点的 Attribute 中

image-20220707104006014

禁用 Attribute 继承

  • 如果我们 不希望组件的根元素继承 attribute,可以在组件中设置 inheritAttrs: false

    禁用 attribute 继承的常见情况是 需要将 attribute应用于根元素之外的其他元素

    js
    export default {
      inheritAttrs: false
    }

我们可以通过 $attrs 来访问所有的非 props 的 attribute

  • 多个根节点的 attribute 如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上
html
<template>
  <div :class="$attrs.class">NotPropAttribute组件1</div>
  <div>NotPropAttribute组件2</div>
</template>

子组件传递给父组件

什么情况下子组件需要传递内容到父组件呢?

  • 子组件有一些事件发生 的时候,比如在组件中发生了点击,父组件需要切换内容
  • 子组件 有一些内容想要传递给父组件 的时候

如何进行操作?

  • 首先,我们需要在 子组件中定义好在某些情况下触发的事件名称
  • 其次,在 父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中
  • 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件
js
export default {
  emits: ["add", "sub", "addN"],
  methods: {
    increment() {
      this.$emit('add')
    },
    decrement() {
      this.$emit('sub')
    }
  }
}

在 vue3 当中,我们可以对传递的参数进行验证

js
export default {
  emits: {
    add: null,
    sub: null,
    addN: (num, name, age) => {
      console.log(num, name, age)
      if (num > 10) {
        return true
      }
      return false
    }
  },
  methods: {
    incrementN() {
      this.$emit('addN', this.num, 'why', 18)
    }
  }
}

组件化开发2

Provide/Inject

非父子组件的通信

  • Provide/Inject
  • Mitt 全局事件总线

Provide/Inject 用于非父子组件之间共享数据:

  • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
  • 在这种情况下,如果我们仍然将 props 沿着组件链逐级传递下去,就会非常的麻烦

对于这种情况下,我们可以使用 Provide 和 Inject

  • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者
  • 父组件有一个 provide 选项来提供数据
  • 子组件有一个 inject 选项来开始使用这些数据

结构:App.vue -> Home.vue -> HomeComtent.vue

image-20220707143132642

如果 Provide 提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取

  • 这时候会报错
js
export default {
  // 报错
  provide: {
    length: this.names.length
  },
  // 正确用法
  provide() {
    return {
      length: this.names.length
    }
  }
}

这时如果我们修改了 this.names 的内容,会发现子组件中是没有反应的

  • 如果想让数据编程响应式的,可以使用响应式的一些 API 来完成这些功能,比如 computed 函数
  • 我们在使用 length 的时候需要获取其中的 value,这时因为 computed 返回的是一个 ref 对象,需要取出其中的 value 来使用

image-20220707144418896

Mitt 全局事件总线

Vue3 从实例中移除了 off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:

bash
npm install mitt

我们可以封装一个工具 eventbus.js:

js
import mitt from 'mitt'

const emitter = mitt()

export default emitter

使用事件总线工具

  • 在 Home.vue 中监听事件
json
import emitter from './utils/eventbus'

export default {
  created() {
    emitter.on('why', info => {
      console.log('why:', info)
    })
    emitter.on('kobe', info => {
      console.log('kobe:', info)
    })
    // 监听所有事件
    emitter.on('*', (type, info) => {
      console.log('* listener:', type, info)
    })
  }
}
  • 在 App.vue 中触发事件
js
import emitter from './utils/eventbus'

export default {
  methods: {
    btnClick() {
      emitter.emit('why', { name: 'why', age: 18 })
    }
  }
}

在某些情况我们可能希望 取消掉之前注册的函数监听

js
// 取消emitter中所有的监听
emitter.all.clear()

// 定义一个函数
function onFoo() {}
emitter.on('foo', onFoo) // 监听
emitter.off('foo', onFoo) // 取消监听

Slot

在开发中,我们会经常封装一个个可复用的组件:

  • 前面我们会 通过 props 传递 给组件一些数据,让组件来进行展示
  • 但是为了让这个组件具备 更强的通用性,我们 不能将组件中的内容限制为固定的 div、span 等等这些元素
  • 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片
  • 我们应该让使用者可以决定某一块区域到底存放什么内容和元素

举个栗子:假如我们定制一个通用的导航组件 NavBar

  • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定
  • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示
  • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等
  • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示

image-20220707155740313

这个时候我们就可以来定义插槽 slot:

  • 插槽的使用过程其实是抽取共性、预留不同
  • 我们会将共同的元素、内容依然在组件内进行封装
  • 同时会将不同的元素使用 slot 作为占位,让外部决定到底显示什么样的元素

如何使用 slot 呢?

  • Vue 中将 <slot> 元素作为承载分发内容的出口
  • 在封装组件中,使用特殊的元素 <slot> 就可以为封装组件开启一个插槽
  • 该插槽插入什么内容取决于父组件如何使用

Slot 基本使用

我们在 App.vue 中使用它们:我们可以插入普通的内容、html 元素、组件元素,都可以是可以的

image-20220707160449156

有时候我们希望在使用插槽时,如果没有插入对应的内容,那么我们需要显示一个默认的内容:

image-20220707160426579

如果一个组件中含有多个插槽,我们插入多个内容时是什么效果?

image-20220707160538743

具名插槽使用

  • 具名插槽顾名思义就是给插槽起一个名字,<slot> 元素有一个特殊的 attribute:name
  • 一个不带 name 的 slot,会带有隐含的名字 default
html
<nav-bar :name="name">
  <template v-slot:left>
    <button>左边的按钮</button>
  </template>
  <template v-slot:center>
    <h2>我是标题</h2>
  </template>
  <template v-slot:right>
    <i>右边的i元素</i>
  </template>
</nav-bar>

<!-- NavBar.vue -->
<div class="nav-bar">
  <div class="left">
    <slot name="left"></slot>
  </div>
  <div class="center">
    <slot name="center"></slot>
  </div>
  <div class="right">
    <slot name="right"></slot>
  </div>
</div>

动态插槽名

  • 我们可以通过 v-slot:[dynamicSlotName] 方式动态绑定一个名称
html
<nav-bar :name="name">
  <template v-slot:[name]>
    <i>why内容</i>
  </template>
</nav-bar>

<script>
export default {
  data() {
    return {
      name: 'why'
    }
  }
}
</script>

<!-- NavBar.vue -->
<div class="addition">
  <slot :name="name"></slot>
</div>
<script>
export default {
  props: {
    name: String
  }
}
</script>

具名插槽的缩写

  • 跟 v-on 和 v-bind 一样,v-slot 也有缩写
  • 即把参数之前的所有内容 (v-slot:) 替换为字符 #
html
<template #left>
  <button>左边的按钮</button>
</template>

作用域插槽

  • 父级模板里的所有内容都是在父级作用域中编译的
  • 子模板里的所有内容都是在子作用域中编译的

image-20220707163944329

有时候我们希望插槽 可以访问子组件中的内容

  • 当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容

案例

  1. 在 App.vue 中定义好数据
  2. 传递给 ShowNames 组件中
  3. ShowNames 组件中遍历 names 数据
  4. 定义插槽的 prop
  5. 通过 v-slot:default 的方式获取到 slot 的 props
  6. 使用 slotProps 中的 item 和 index

image-20220707164720487

html
<!-- ShowNames.vue -->
<template>
  <template v-for="(item, index) in names" :key="item">
    <slot :item="item" :index="index"></slot>
  </template>
</template>

<script>
export default {
  props: {
    names: {
      type: Array,
      default: () => []
    }
  }
}
</script>

<!-- App.vue -->
<show-names :names="names">
  <template v-slot="slotProps">
    <strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
  </template>
</show-names>
<script>
import ShowNames from './ShowNames.vue'

export default {
  components: {
    ShowNames
  },
  data() {
    return {
      names: ['why', 'kobe', 'james', 'curry']
    }
  }
}
</script>

默认插槽 default,那么在使用的时候 v-slot:default="slotProps" 可以简写为 v-slot="slotProps"

  • 如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件上
html
<show-names :names="names">
  <template v-slot="slotProps">
    <strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
  </template>
</show-names>

<show-names :names="names" v-slot="slotProps">
  <strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
</show-names>

如果我们有默认插槽和具名插槽,那么按照完整的 template 来编写

html
<show-names :names="names">
  <template v-slot="coderwhy">
    <button>{{ coderwhy.item }}-{{ coderwhy.index }}</button>
  </template>

  <template v-slot:why>
    <h2>我是why的插入内容</h2>
  </template>
</show-names>

组件化开发3

动态组件

点击一个tab-bar,切换不同的组件显示

image-20220707172019437

  • 方式一:通过 v-if 来判断,显示不同的组件
html
<template v-if="currentTab === 'home'">
  <home></home>
</template>
<template v-else-if="currentTab === 'about'">
  <about></about>
</template>
<template v-else>
  <category></category>
</template>
  • 方式二:动态组件的方式

    可以是通过 component 函数注册的组件

    在一个组件对象的 components 对象中注册的组件

html
<component :is="currentTab"> </component>

动态组件传值只需要我们将属性和监听事件放到 component 上来使用

html
<component :is="currentTab" name="coderwhy" :age="18" @pageClick="pageClick"> </component>

keep-alive

在开发中某些情况我们希望继续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive

html
<keep-alive include="home,about">
  <component :is="currentTab" name="coderwhy" :age="18" @pageClick="pageClick"> </component>
</keep-alive>

keep-alive 有一些属性:

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存
  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存
  • max - number | string。最多可以缓存多少个组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁

include 和 exclude prop 允许组件有条件的缓存:

  • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
  • 匹配首先检查组件自身的 name 选项

image-20220708085341519

异步组件

默认的打包过程:

  • 默认情况下,在构建整个组件树的过程中,因为组件和组件之间是 通过模块化直接依赖 的,那么 webpack 在打包时就会将组件模块打包在一起(比如一个 app.js 文件中)
  • 这时候随着 项目不断庞大,app.js 文件的内容过大,会造成首屏渲染速度变慢

打包时,代码的分包:

  • 对于一些 不需要立即使用的组件,我们可以 单独对它们进行拆分,拆分成一些 小的代码块 chunk.js
  • 这些 chunk.js 会在 需要时从服务器加载下载,并且运行代码,显示对应的内容
  • 通过 import 函数导入的模块, 后续 webpack 对其进行打包的时候就会进行分包的操作

image-20220708091715746

defineasynccomponent

如果我们的项目过大了,对于某些组件我们希望通过异步的方式来(目的是可以对其进行分包处理),那么 Vue 中给我们提供了一个函数:defineAsyncComponent

defineAsyncComponent 接收两种类型的参数:

  • 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象
  • 类型二:接收一个对象类型,对异步函数进行配置
js
import { defineAsyncComponent } from 'vue'

// 写法一:参数为函数
const AsyncCategory = defineAsyncComponent(() => import('./AsyncCategory.vue'))

// 写法二:参数为对象
const AsyncCategoryObj = defineAsyncComponent({
  // 工厂函数
  loader: () => import('./Foo.vue'),
  // 加载异步组件时要使用的组件
  loadingComponent: LoadingComponent,
  // 加载失败时要使用的组件
  errorComponent: ErrorComponent,
  // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
  delay: 200,
  // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
  // 默认值:Infinity(即永不超时,单位 ms)
  timeout: 3000,
  // 定义组件是否可挂起 | 默认值:true
  suspensible: false,
})

Suspense

suspense

Suspense 是一个试验性的新特性,其 API 可能随时会发生变动

Suspense 是一个内置的全局组件,该组件有两个插槽:

  • default:如果 default 可以显示,那么显示 default 的内容
  • fallback:如果 default 无法显示,那么会显示 fallback 插槽的内容
html
<suspense>
  <template #default>
    <async-category></async-category>
  </template>
  <template #fallback>
    <loading></loading>
  </template>
</suspense>

$refs

我们在组件中想要直接获取到元素对象或者子组件实例:

  • 在 Vue 开发中我们是不推荐进行 DOM 操作的
  • 这个时候,我们可以给元素或者组件绑定一个 ref 的 attribute 属性

组件实例有一个 $refs 属性:

  • 它一个对象 Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例
html
<template>
  <!-- 绑定到一个元素上 -->
  <h2 ref="title">哈哈哈</h2>
  <!-- 绑定到一个组件实例上 -->
  <nav-bar ref="navBar"></nav-bar>
  <button @click="btnClick">获取元素</button>
</template>

<script>
export default {
  methods: {
    btnClick() {
      console.log(this.$refs.navBar.message)
      console.log(this.$refs.navBar.$el)
    }
  }
}
</script>

我们可以通过 $parent 来访问父元素:

  • 这里我们也可以通过 $root 来实现,因为 App 是我们的根组件

注意:在 Vue3 中已经移除了 $children 的属性,所以不可以使用了

html
<template>
  <button @click="getParentAndRoot">获取父组件和根组件</button>
</template>
<script>
export default {
  methods: {
    getParentAndRoot() {
      console.log(this.$parent)
      console.log(this.$root)
    }
  }
}
</script>

生命周期

什么是生命周期呢?

  • 每个组件都可能会经历从 创建、挂载、更新、卸载 等一系列的过程
  • 在这个过程中的 某一个阶段,用于可能会想要 添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据)

生命周期函数:

  • 生命周期函数是一些钩子函数,在某个时间会被 Vue 源码内部进行回调
  • 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段
  • 那么我们就可以在该生命周期中编写属于自己的逻辑代码了

image-20220708111410246

image-20220708111416649

对于缓存的组件来说,再次进入时,我们是不会执行 created 或者 mounted 等生命周期函数的:

  • 但是有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件
  • 这个时候我们可以使用 activated 和 deactivated 这两个生命周期钩子函数来监听;

组件的 v-model

为了我们的 MyInput 组件可以正常工作,这个组件内部的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 modelValue 的 prop
  • 在其 input 事件触发时,将新的值通过 自定义的 update:modelValue 事件 抛出

如果我们希望组件内部按照双向绑定的做法去完成,我们可以使用计算属性的 setter 和 getter 来完成

html
<template>
  <div>
    <!-- 1.通过input -->
    <!-- <input :value="modelValue" @input="inputChange" /> -->

    <!-- 2.直接绑定到 props 中是不对的 -->
    <!-- <input v-model="modelValue" /> -->

    <!-- 3.通过计算属性实现双向数据绑定 -->
    <input v-model="value" />
  </div>
</template>

<script>
export default {
  props: {
    modelValue: String
  },
  emits: ['update:modelValue'],
  computed: {
    value: {
      set(value) {
        this.$emit('update:modelValue', value)
      },
      get() {
        return this.modelValue
      }
    }
  },
  methods: {
    inputChange(event) {
      this.$emit('update:modelValue', event.target.value)
    }
  }
}
</script>

绑定多个属性

  • 默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue 的事件
  • 如果我们希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称
html
<hy-input v-model="message" v-model:title="title"></hy-input>

<script>
export default {
  props: {
    modelValue: String,
    title: String
  },
  emits: ['update:modelValue', 'update:title']
}
</script>

常备不懈,才能有备无患