文章目录
1.前言
由于在日常开发中会有一部分前端的开发任务,会涉及到Vue的项目的搭建、迭代、构建发布等操作,所以想系统的学习一下Vue相关的知识点,本专题会依照Vue的搭建、开发基础实践、进阶用法、打包部署的顺序进行记录。
主要内容与历史文章链接如下:
- Vue3搭建、路由配置与基本语法
- 响应式变量、双向绑定、计算属性、监听器
- 优雅使用VUE3 组件特性:组件定义、组件注册、事件监听、双向绑定
- 优雅使用VUE3 组件特性:组件插槽、属性透传、依赖注入、动态组件
- 环境变量、axios配置与打包部署
从前面两篇的内容中我们已经知道了Vue3的基本语法使用,以及响应式的核心特性,通过这些知识已经可以完成一个简单的页面了。但是在实际的开发中,我们的项目是由很多个页面组成的,每个页面中又可能会有特别多的功能需要实现。
当代码越写越多之后,可能就会发现,不同的页面中有一些功能是重复的,此时我们就可以考虑将这些重复的功能抽取出来,写成一个个的组件来进行复用。
组件除了可以复用代码的作用以外,还可以形成功能的封装,使项目中的一个个功能点由一盘散沙变得内聚,我们在后续的迭代过程中,这种内聚的组件可以大大的提高我们修改、拓展的效率,同时也能降低改出bug的概率。
组件相关的内容将会分为两篇来编写,本篇主要讲述以下内容:
- 组件的定义
- 组件的注册使用
- 父子组件通信
- 父组件如何传递属性给子组件
- 父组件如何监听子组件中的web事件
- 父子组件的双向绑定如何实现
2.组件定义与注册
2.1.什么是组件
组件的定义是很宽泛的,我们任意打开一个页面,看到的每一个视图元素都可能是一个组件,例如在一个管理系统中,标题栏是一个组件,菜单栏是一个组件,Tab、列表、分页、表单甚至是一个简单的input
输入框都可能是一个组件。
组件之间有父子关系、兄弟关系、祖孙关系等等,会像下图那样传递依赖:
我们需要做的,就是定义这一个一个的children
,并处理它们的依赖关系以及组件之间的参数传递(通信)。接下来,我们先尝试定义一个组件。
2.2.定义组件
如果创建一个和父组件没有通信关系的组件,其实就是创建一个普通.vue
文件,我们在前面两篇中就已经使用过了。例如我现在创建一个计数器组件,提供一个按钮和一个展示数字的块,当点击按钮的时候数字块上的数字就+1。
<!--Count.vue-->
<template>
<div>
<button @click="add">点击 + 1</button>
<div>{{ count }}</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const add = () => {
count.value++;
};
</script>
这样一个组件就写好了,接下来试试如何将这个组件注册到其他的组件中去。
2.3.注册组件
2.3.1.局部注册
局部注册组件有两个步骤:
- 在js中
import
组件,并给组件命名 - 在
<template>
中以组件的命名做为标签进行引入
<!--父模块引入Count组件-->
<template>
<Count />
<Count />
</template>
<script setup>
import Count from "../components/Count.vue";
</script>
在父组件上引入了两个Count
效果如上图所示,两个引入的组件互不影响,各记各的数。这样就实现了组键功能的复用,而不需要到处复制一模一样的代码。
实际的开发中,我们很少会这么去使用组件,在某个父组件中引入子组件的时候,往往会建立通信关系,也就是组件之间会有属性、事件、字段值的传递。
2.3.2.全局注册
全局注册就是在main.js
中注册组件,这样注册的组件在任何一个组件中都可以使用,不再需要从<script>
块中引入。
import Count from "@/components/Count.vue";
// 全局组件引入
app.component("Count", Count);
全局注册使用起来比较方便,但是没有明确的依赖关系,在日常的开发中更建议使用局部注册的方式。
3.父子组件通信
3.1.props传递属性
组件除了共性以外,还会有一定的特性,这种特性展示可以通过父组件以props
的形式传递到子组件中。还是以上面的计数器为例,我想给不同的计数器定义不同的名字。
要实现这个需求,可以在子组件中通过defineProps
定义一个需要接收的变量,然后在父组件中通过定义的key
将变量值传递到子组件中。
我们最子组件做一点小小的修改,在defineProps
加入一个label: String
,其中label
就是定义的需要从父组件中接收的变量key
,String
表示的是这个变量的类型。通过defineProps
定义的变量,可以在模板中直接使用{{ label }}
。
<template>
<div>
{{ label }}
<button @click="add">点击+1,计数值为:{{ count }}</button>
</div>
</template>
<script setup>
import { ref } from "vue";
defineProps({
label: String
});
const count = ref(0);
const add = () => {
count.value++;
};
</script>
在父组件的将label
这个key作为属性写在<Count/>
标签中,如下所示两种方式,一种是直接在模板中写死计数器1,另一种是通过变量的方式来定义计数器2。
<template>
<div class="hello">
<Count label="计数器1" />
<Count :label="countLabel" />
</div>
</template>
<script setup>
import Count from "../components/Count.vue";
import { ref } from "vue";
const countLabel = ref("计数器2");
</script>
需要注意的是,通过props
传入到子组件的变量值,不应该被子组件修改,这也是Vue的思想之一,在哪里定义的就在哪里修改,如果一定想要修改props
的值,一般是通过定义一个由子组件触发的事件监听,触发父组件中的函数进行修改。
3.2.事件监听
父子组件之间的时间监听就是定义一个事件,由子组件去触发,由父组件执行触发后的回调。通过事件监听,可以让子组件调用父组件中的函数,调用时可以传入形参,通过这种方式将子组件中的值传递到父组件中。如果说通过props
是属性值由父到子,事件监听就可以实现由子到父。
对上面的计数器例子做一点小修改,显示计数的块放到父组件中,在每个子组件中点击了计数按钮后,父组件的计数值+1。
首先是子组件的修改,通过defineEmits
定义并接收一个父组件传递过来的事件,例如就叫incrementCount
,并通过一个add
函数进行调用。
<template>
<div>
<button @click="add">点击 + 1</button>
</div>
</template>
<script setup>
const emit = defineEmits(["incrementCount"]);
const add = () => {
emit("incrementCount", 1);
};
</script>
在父组件引用的<Count>
标签中,通过@incrementCount="xxx"
传递函数,此处的xxx值的是定义在父组件中的函数,可以是一个在script
中显式定义的函数,也可以是一个匿名函数,如:@incrementCount="()=>xxx"
。
<template>
<div class="hello">
<Count @incrementCount="doIncr" />
<Count @incrementCount="doIncr" />
<div>父组件的计数:{{ count }}</div>
</div>
</template>
<script setup>
import Count from "../components/Count.vue";
import { ref } from "vue";
const count = ref(0);
const doIncr = () => {
count.value++;
};
</script>
此时不管点击哪一个,父组件的计数都会+1,大体流程图下图所示:
3.3.父子组件间的双向绑定
我们之前在单个组件中通过v-model
建立了数据与视图的双向绑定,而父子组件的双向绑定,就是在父组件中定义一个变量,将它与子组件中的某个模板元素完成双向绑定。
例如现在有这么一个例子,在父组件中引入一个input
子组件,将父组件中的inputValue
变量与子组件中的<input>
输入框建立双向绑定关系。
首先需要定义子组件MyInput
,通过defineProps
定义需要接收的属性,通过defineEmits
定义需要触发的函数。
<template>
<div>
<input type="text" v-model="inputValue">
</div>
</template>
<script setup>
import { computed, defineEmits } from "vue";
const props = defineProps({ "msg": String });
const emit = defineEmits(["update:msg"]);
const inputValue = computed({
get: () => props.msg,
set: (value) => emit("update:msg", value)
});
</script>
这里有两个注意点:
- 父组件中的
v-model:xxx
会提供一个@update:xxx
的事件传递到子组件中,这里的xxx
是一个标识,用于在一个子组件中有多个双向绑定的情况。 - 在子组件中一般不会直接使用父组件传递的
props
的值,如果在子组件中也需要通过v-model
建立双向绑定关系,可以通过computed
获取一个新的值。
这里的属性计算多了一个setter属性,即在属性发生变化时,就通过emit
触发父组件的事件。
在父组件中的使用就比较简单了,只需要添加v-model:xxx
属性,并绑定一个变量就可以了。
<template>
<div class="hello">
<MyInput v-model:msg="inputValue" />
<div>{{ inputValue }}</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import MyInput from "../components/MyInput.vue";
const inputValue = ref("挥之以墨");
</script>
这样就实现了父子组件的双向绑定。
3.4.综合使用的例子
通过一个实际的项目需求来综合使用一下上面提到的语法。
例如现在有一个消息系统的表单,这个表单中有一个文本域用以填写需要发送的短信模板,由于短信模板需要遵守一定的规则,所以不能让用户简单的自由填写,这个时候就可以通过子组件选择短信的开头签名与结尾,在选择后通过触发事件将短信模板的内容填充到父组件中。
这里我引入了Element Plus组件,不太清楚这个组件的可以通过《Element Plus安装文档》安装配置。
下面是实现代码:
- 子组件封装:
<template> <el-dialog v-model="visible" title="短信模板生成"> <el-form :model="form"> <el-form-item label="短信签名" :label-width="formLabelWidth"> <el-select v-model="form.signature" placeholder="选择短信签名"> <el-option label="【挥之以墨】" value="【挥之以墨】" /> <el-option label="【CSDN】" value="【CSDN】" /> </el-select> </el-form-item> <el-form-item label="营销短信" :label-width="formLabelWidth"> <div class="mb-2 flex items-center text-sm"> <el-radio-group v-model="form.isMarketing" class="ml-4"> <el-radio :label="1" size="large">是</el-radio> <el-radio :label="0" size="large">否</el-radio> </el-radio-group> </div> </el-form-item> <el-form-item label="模板内容" :label-width="formLabelWidth"> <el-input type="textarea" :rows="2" v-model="form.templateContent" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="generateTemplateContent()">确定</el-button> <el-button @click="closeDialog()">取消</el-button> </span> </template> </el-dialog> </template> <script setup> import { computed, defineProps, reactive } from "vue"; // 表单默认值 const form = reactive({ signature: "", isMarketing: 0, templateContent: "" }); const props = defineProps({ "dialogFormVisible": Boolean }); const emits = defineEmits(["update:dialogFormVisible", "updateTemplate"]); // 弹窗的显示与隐藏 const visible = computed({ get: () => props.dialogFormVisible, set: (value) => emits("update:dialogFormVisible", value) } ); // 生成模板内容 const generateTemplateContent = () => { let contentValue = form.signature + form.templateContent + (form.isMarketing ? "回T退订" : ""); emits("updateTemplate", contentValue); closeDialog(); }; // 弹窗关闭 const closeDialog = () => { emits("update:dialogFormVisible", false); }; const formLabelWidth = "140px"; </script>
- 父组件引用:
<template> <div class="hello"> <el-input type="textarea" :rows="2" v-model="templateContent" /> <MsgTemplate disabled="true" v-model:dialogFormVisible="dialogFormVisible" @updateTemplate="updateTemplate" /> <el-button @click="showDialog()"> 模板填写 </el-button> </div> </template> <script setup> import { ref } from "vue"; import MsgTemplate from "../components/MsgTemplate.vue"; const templateContent = ref(""); const dialogFormVisible = ref(false); // 打开弹框 const showDialog = () => { dialogFormVisible.value = true; }; // 更新内容 const updateTemplate = (contentValue) => { templateContent.value = contentValue; }; </script>
在弹窗子组件中安装操作流程进行操作:
点击确定之后,就可以在父组件中生成符合规范的短信模板了:
4.总结
本篇主要讲述的是组件的通用功能抽取以及在其他组件中的引入使用,需要注意以下的细节点:
- 定义组件:
- 编写单独的
.vue
文件 - 在这个单独的文件中定义需要接收的属性、函数、事件等,类似于形参
- 编写单独的
- 组件注册:
- 注册方式:
- 全局注册:通过顶层的
APP
对象进行引入,全局注册的组件,在组件中都可以直接使用 - 局部注册:在需要使用到的组件中
import
,只有当前组件可以使用
- 全局注册:通过顶层的
- 父组件引入组件后,在
html
标签中传入实际的属性、函数、事件等,类似于实参
- 注册方式:
- 属性传递
- 子组件通过
defineProps
定义需要接收的参数名及参数类型 - 父组件通过
:参数名="属性值"
的方式传递到子组件中
- 子组件通过
- 事件监听传递
- 子组件通过
defineEmits
定义需要接收的事件名,并通过$emit
或函数调用的方式触发 - 父组件通过
@事件名="函数"
的方式将实际需要执行的函数传递到子组件中
- 子组件通过
- 双向绑定:
- 子组件需要同时使用
defineProps(['xxx'])
和defineEmits(['update:xxx'])
定义需要接收的双向绑定的值。 - 子组件还需要定义一个
computed
生成一个新的属性,通过v-model
将这个新的属性与DOM绑定,在computed
的getter方法中,获取props
的值,在setter方法中,通过emit
触发事件的调用 - 父组件在引入子组件的标签中,直接使用
v-model:xxx="待绑定属性"
的方式,将自己的属性与子组件的DOM进行双向绑定。
- 子组件需要同时使用