自定义组件,举个🌰:
1、封装自定义组件 CustomList.vue
<!-- src/components/CustomList.vue -->
<script lang="ts">
import type { IDataItem } from "../type/customlist";
// IProps 不能直接从外部导入,当前会报错,以后可能会支持(有人提了issue)
export interface IProps {
title?: string;
data: Array<IDataItem>;
}
</script>
<script setup lang="ts">
import { onMounted } from "vue";
// 声明一些属性,给父组件传值进来,并且给部分设置了默认值
const props = withDefaults(defineProps<IProps>(), {
title: "CustomList",
});
// 抛出事件,供父组件使用
// const emit = defineEmits(["click-item"]);
const emit = defineEmits<{
(e: "click-item", item: IDataItem, $event: Event): void;
}>();
const clickItem = function (item: IDataItem, e: Event) {
// 可以在函数里执行完一些操作
console.log("自定义组件点击了组件里面的item...", item, e);
// 再抛出宏定义的事件
emit("click-item", item, e);
};
onMounted(() => {
console.log("CustomList onMounted...", props);
});
</script>
<template>
<h1>{{ title }}</h1>
<ul @click="$emit('click-ul', $event, 1)">
<li @click="clickItem(item, $event)" v-for="item in data" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<style lang="less" scoped></style>
src
底下 type
文件夹中声明的 interface
接口文件
// src/type/customlist.d.ts
export interface IDataItem {
name: string;
id: number;
}
2、在 App.vue
中使用自定义组件 CustomList.vue
<!-- src/App.vue -->
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeMount } from "vue";
import CustomList from "./components/CustomList.vue";
import type { IDataItem } from "./type/customlist";
// 声明一个data,传到自定义组件CustomList
const data = reactive([] as Array<IDataItem>);
const customListRef = ref<HTMLElement | null>(null);
// 点击了自定义组件的item,执行了自定义组件抛出宏定义事件并接收其携带过来的参数
const clickItem = function (item: string, e: Event) {
console.log("clickItem...", item, e);
};
// 点击了自定义组件的ul在templete里面直接抛出的事件并接收其携带过来的参数
const clickUl = function (e: Event, value: number) {
console.log("clickUl...", e, value);
};
onBeforeMount(() => {
const list: Array<IDataItem> = [
{
name: "aaa",
id: 1,
},
{
name: "bbb",
id: 2,
},
];
data.push(...list);
});
onMounted(() => {
console.log("onMounted...", customListRef.value);
});
</script>
<template>
<div class="content">
<!-- 使用自定义组件CustomList -->
<CustomList
@click-ul="clickUl"
@click-item="clickItem"
:data="data"
ref="customListRef"
/>
</div>
</template>
<style scoped lang="less">
.content {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
color: var(--colorTextNormal);
padding: 50px;
}
</style>
一、组件注册
1、全局注册
在 main.ts
中使用 app.component('MyComponent', MyComponent)
全局注册一个组件,可以在app内的任何地方使用。
缺点:
-
无法在生产打包时被自动移除 (也叫
tree-shaking
),即使它并没有被实际使用,它仍然会出现在打包后的JS
文件中。 -
依赖关系不明显,出问题不易定位,用多了难维护
2、局部注册
在用到组件的地方 import
导入
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
3、总结:非必要不实用全局注册
二、属性
父组件通过给子组件传递不同的属性数据控制子组件最终展示状态。
1、定义一个 props
通过 defineProps
宏定义一个属性
const props = defineProps(['title'])
console.log(props.title)
2、给 props
添加类型并给定默认值
针对类型的 props/emit 声明
defineProps<IProps>()
可以给 props
设置类型,IProps
为 props
类型 为组件的 props 标注类型
withDefaults
的第二个参数支持给 props
设置默认值 使用类型声明时的默认 props 值
import type { IDataItem } from "../type/customlist";
// IProps 不能直接从外部导入,当前会报错,以后可能会支持(有人提了issue)
export interface IProps {
title?: string;
data: Array<IDataItem>;
}
const props = withDefaults(defineProps<IProps>(), {
title: "CustomList",
});
3、 props
只读,不可修改
组件内的 props
都是只读的,不能对其进行修改 单向数据流
<script setup lang="ts">
import { onMounted } from "vue";
// 声明一些属性,给父组件传值进来,并且给部分设置了默认值
const props = withDefaults(defineProps<IProps>(), {
title: "CustomList",
});
onMounted(() => {
// ❎ 不能这么干,单向数据流 props 不可修改
props.title = "改变了CustomList的title"
});
</script>
三、事件
子组件抛出内部事件并传递参数供父组件使用。
1、template
内使用 $emit
直接抛出一个事件
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件并抛出相关参数,$emit('抛出的事件名', 需要传到父组件的参数一, 参数二...)
。抛出事件名要用可以使用 camelCase
和 kebab-case
形式(建议 kebab-case
)。
<!-- MyComponent -->
<button @click="$emit('someEvent', $event, 1)">click me</button>
2、defineEmits()
宏来声明要触发的事件
在 <template>
中使用的 $emit
方法不能在组件的 <script setup>
部分中使用,相当于你不能对这个事件先做出一些处理然后再抛出,使用 defineEmits()
宏声明要触发的事件可以解决这个问题:
声明触发的事件
<script>
// 抛出事件,供父组件使用
const emit = defineEmits<{
// (e: "抛出的事件名", 抛出的参数一, 参数二...)
(e: "callback", $event: Event): void;
}>();
const callback = function (e: Event) {
emit("callback", e);
};
</script>
<template>
<button @click="callback($event, 111)">click me</button>
</template>
3、使用组件并监听子组件抛出的事件及参数
<script>
import MyComponent from "./components/MyComponent.vue";
const callback = function (e: Event, value) {
console.log("callback...", e, value);
}
</script>
<template>
<MyComponent @some-event="callback" />
</template>
四、插槽
可以在自定义组件内部指定的部位插入自定义内容,让组件更加灵活。
1、匿名插槽
组件内插入 <slot>
标签不指定 name
属性,默认只有一个,可以给插槽内添加默认值,父组件在使用组件时不传值的时候会展示默认内容,传值则会替换掉默认内容。
<!-- 子组件 SubmitButton -->
<button type="submit">
<slot>这里是默认内容文本</slot>
</button>
<!-- 使用 SubmitButton (不传值) ,按钮内部显示【这里是默认内容文本】-->
<SubmitButton />
<!-- 使用 SubmitButton (传值) ,按钮内部显示【Save】-->
<SubmitButton>Save</SubmitButton>
2、具名插槽
组件内插入 <slot>
标签时加上一个 name
属性,区分不同的插槽,可以有多个。
<slot name="header"></slot>
3、具名作用域插槽
感觉跟具名插槽就是同一个东西,就是可以从子组件带参数过来,只能在指定的插槽里面用。
<!-- 子组件 MyComponent -->
<template>
<slot name="header" message="hello header"></slot>
<slot message="hello default"></slot>
<slot name="footer" message="hello footer"></slot>
</template>
<!-- 父组件 -->
<!-- headerProps, defaultProps, footerProps 是一个对象 ,返回以对应 slot 上 { 属性: 属性值 } 形式的数据 -->
<MyComponent>
<template #header="headerProps">
{{ headerProps.message }}
</template>
<template #default="defaultProps">
{{ defaultProps.message }}
</template>
<template #footer="footerProps">
{{ footerProps.message }}
</template>
<!-- 解构取值的 🌰 -->
<template #footer="{ message }">
{{ message }}
</template>
</MyComponent>
五、封装一个组件注意
1、组件编码规范
-
使用
PascalCase
形式的组件名称,提高了模板的可读性,方便区分vue component
和原生HTML DOM
组件名格式 -
给事件命名可以使用
camelCase
和kebab-case
(短横线连字符) 形式,使用时用kebab-case
(短横线连字符) 形式,使用camelCase
并没有太多优势,推荐更贴近HTML
的kebab-case
书写风格。DOM 模板解析注意事项
传递 prop 的细节
2、可复用性
- 组件内部样式可覆盖,尽可能不要写死,提供参数去修改
- 组件内部页面展示更加灵活,在合适的地方提供插槽,让组件内布局可控、更加灵活