Vue 3 defineExpose 企业级实用教程
Vue 3 defineExpose 企业级实用教程
一、defineExpose 概述
defineExpose是 Vue 3<script setup>语法糖中用于显式暴露组件公共 API的核心方法。在<script setup>中,变量和函数默认是私有的,defineExpose允许开发者明确指定哪些内容可被父组件访问,是实现组件间通信、确保类型安全的关键机制。
二、defineExpose 的核心价值
1.增强组件封装性
- 默认私有:<script setup>中未暴露的变量/函数对外不可见,避免内部实现细节泄露
- 最小暴露原则:仅暴露必要的公共接口,降低组件间耦合度
- 支持 TypeScript 接口定义,提供完整的类型推断和编译时检查
- 父组件访问子组件 API 时获得类型提示,减少“访问未定义属性”的运行时错误
- 显式声明组件对外提供的能力,便于团队协作(无需阅读内部代码即可了解可用方法)
- 替代 Options API 中的expose选项,更符合组合式 API 的灵活特性
2.类型安全与 TypeScript 集成
3.明确公共 API,提升可维护性
三、基础用法
3.1 子组件:定义并暴露公共接口
<!-- ChildComponent.vue -->
<template>
<div class="q-pa-md">
<p>计数器: {{ count }}</p>
<q-btn label="增加" @click="increment" color="primary" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 私有状态(默认不暴露)
const count = ref(0);
const internalState = ref('内部状态,不对外暴露');
// 公共方法
const increment = () => {
count.value++;
};
const reset = () => {
count.value = 0;
};
const getCount = () => {
return count.value;
};
// 显式暴露公共 API(仅以下内容可被父组件访问)
defineExpose({
increment, // 暴露增加计数方法
reset, // 暴露重置方法
getCount // 暴露获取当前值方法
// 不暴露 internalState 和 count,保持私有
});
</script>
3.2 父组件:访问子组件暴露的 API
通过ref获取子组件实例,调用暴露的方法:
<!-- ParentComponent.vue -->
<template>
<div class="q-pa-md">
<!-- 子组件,绑定 ref -->
<child-component ref="childRef" />
<!-- 父组件控制按钮 -->
<div class="q-mt-md">
<q-btn label="父组件触发增加" @click="incrementFromParent" />
<q-btn label="父组件触发重置" @click="resetFromParent" class="q-ml-sm" />
<q-btn label="获取当前值" @click="getCurrentValue" class="q-ml-sm" />
</div>
<div class="q-mt-md">
<p>父组件获取的值: {{ currentValue }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 子组件实例引用(通过 InstanceType 获取类型)
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null);
const currentValue = ref<number | null>(null);
// 调用子组件暴露的 increment 方法
const incrementFromParent = () => {
childRef.value?.increment(); // TypeScript 类型提示:仅暴露的方法可访问
};
// 调用子组件暴露的 reset 方法
const resetFromParent = () => {
childRef.value?.reset();
};
// 调用子组件暴露的 getCount 方法
const getCurrentValue = () => {
currentValue.value = childRef.value?.getCount() ?? null;
};
</script>
四、企业级实战示例
4.1 示例 1:复杂表单组件
封装可复用表单组件,暴露提交、重置、数据获取等核心方法。
子组件(表单组件)
<!-- EnterpriseForm.vue -->
<template>
<q-form ref="formRef" @submit.prevent="handleSubmit" class="q-gutter-md">
<q-input
v-model="formData.name"
label="姓名"
:rules="[val => !!val || '姓名不能为空']"
/>
<q-input
v-model="formData.email"
label="邮箱"
type="email"
:rules="[
val => !!val || '邮箱不能为空',
val => /.+@.+\..+/.test(val) || '邮箱格式不正确'
]"
/>
<div class="q-mt-md">
<q-btn label="提交" type="submit" color="primary" />
<q-btn label="重置" color="negative" class="q-ml-sm" @click="resetForm" />
</div>
</q-form>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
// 类型定义:表单数据结构
interface FormData {
name: string;
email: string;
}
// 类型定义:暴露的公共 API 接口
interface FormExpose {
submit: () => Promise<boolean>; // 提交表单
reset: () => void; // 重置表单
validate: () => Promise<boolean>; // 验证表单
getData: () => FormData; // 获取表单数据
setData: (data: Partial<FormData>) => void; // 设置表单数据
}
// 表单数据(私有)
const formData = reactive<FormData>({ name: '', email: '' });
// 表单引用(私有)
const formRef = ref();
// 提交表单(内部方法)
const handleSubmit = async () => {
if (await validate()) {
console.log('表单提交:', formData);
return true;
}
return false;
};
// 验证表单(内部方法)
const validate = async (): Promise<boolean> => {
if (!formRef.value) return false;
try {
await formRef.value.validate(); // 调用 Quasar 表单验证
return true;
} catch (error) {
console.error('验证失败:', error);
return false;
}
};
// 重置表单(内部方法)
const resetForm = () => {
Object.assign(formData, { name: '', email: '' });
};
// 获取表单数据(内部方法)
const getData = (): FormData => ({ ...formData }); // 返回副本,避免外部修改内部状态
// 设置表单数据(内部方法)
const setData = (data: Partial<FormData>) => {
Object.assign(formData, data);
};
// 暴露公共 API(严格遵循 FormExpose 接口)
defineExpose<FormExpose>({
submit: handleSubmit,
reset: resetForm,
validate,
getData,
setData
});
</script>
父组件(使用表单组件)
<!-- ParentFormContainer.vue -->
<template>
<div class="q-pa-md">
<enterprise-form ref="formRef" />
<div class="q-mt-md">
<q-btn label="外部提交" @click="submitForm" color="primary" />
<q-btn label="外部重置" @click="resetForm" color="negative" class="q-ml-sm" />
<q-btn label="加载数据" @click="loadData" color="info" class="q-ml-sm" />
<q-btn label="获取数据" @click="getFormData" color="secondary" class="q-ml-sm" />
</div>
<div v-if="formData" class="q-mt-md">
<h6>当前表单数据:</h6>
<pre>{{ formData }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import EnterpriseForm from './EnterpriseForm.vue';
import type { FormExpose } from './EnterpriseForm.vue'; // 导入暴露的 API 类型
// 表单组件引用(指定类型为 FormExpose)
const formRef = ref<FormExpose | null>(null);
const formData = ref<any>(null);
// 调用子组件的 submit 方法
const submitForm = async () => {
if (formRef.value) {
const success = await formRef.value.submit();
success && alert('提交成功!');
}
};
// 调用子组件的 reset 方法
const resetForm = () => {
formRef.value?.reset();
};
// 调用子组件的 setData 方法
const loadData = () => {
formRef.value?.setData({
name: '张三',
email: 'zhangsan@example.com'
});
};
// 调用子组件的 getData 方法
const getFormData = () => {
formData.value = formRef.value?.getData();
};
</script>
4.2 示例 2:可编辑数据表格组件
封装支持增删改查的数据表格,暴露数据操作 API。
子组件(表格组件)
<!-- EditableDataTable.vue -->
<template>
<div class="q-pa-md">
<q-table
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 10 }"
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="name">
<q-input
v-if="props.row.editMode"
v-model="props.row.name"
dense
borderless
/>
<span v-else>{{ props.row.name }}</span>
</q-td>
<q-td key="email">
<q-input
v-if="props.row.editMode"
v-model="props.row.email"
dense
borderless
/>
<span v-else>{{ props.row.email }}</span>
</q-td>
<q-td key="actions">
<q-btn
v-if="!props.row.editMode"
icon="edit"
size="sm"
@click="startEdit(props.row)"
/>
<div v-else>
<q-btn icon="check" size="sm" color="positive" @click="saveEdit(props.row)" />
<q-btn icon="close" size="sm" color="negative" @click="cancelEdit(props.row)" />
</div>
</q-td>
</q-tr>
</template>
</q-table>
<div class="q-mt-md">
<q-btn label="添加行" @click="addRow" color="primary" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 类型定义:表格行数据
interface TableRow {
id: number;
name: string;
email: string;
editMode?: boolean;
originalData?: Partial<TableRow>;
}
// 类型定义:暴露的 API 接口
interface TableExpose {
getData: () => TableRow[]; // 获取表格数据
setData: (data: TableRow[]) => void; // 设置表格数据
addRow: (row?: Partial<TableRow>) => void; // 添加行
validate: () => boolean; // 验证表格数据
exportData: (format: 'json' | 'csv') => string; // 导出数据
}
// 表格数据(私有)
const rows = ref<TableRow[]>([
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
]);
// 表格列配置(私有)
const columns = [
{ name: 'name', label: '姓名', field: 'name' },
{ name: 'email', label: '邮箱', field: 'email' },
{ name: 'actions', label: '操作', align: 'center' }
];
// 编辑相关方法(私有)
const startEdit = (row: TableRow) => {
row.originalData = { ...row };
row.editMode = true;
};
const saveEdit = (row: TableRow) => {
row.editMode = false;
delete row.originalData;
};
const cancelEdit = (row: TableRow) => {
if (row.originalData) Object.assign(row, row.originalData);
row.editMode = false;
delete row.originalData;
};
// 暴露的公共方法
const addRow = (rowData?: Partial<TableRow>) => {
rows.value.push({
id: Date.now(),
name: '',
email: '',
editMode: true,
...rowData
});
};
const getData = (): TableRow[] => rows.value.map(row => ({ ...row })); // 返回副本
const setData = (data: TableRow[]) => {
rows.value = data.map(item => ({ ...item }));
};
const validate = (): boolean => {
return rows.value.every(row =>
row.name.trim() && /.+@.+\..+/.test(row.email)
);
};
const exportData = (format: 'json' | 'csv' = 'json'): string => {
return format === 'json'
? JSON.stringify(getData(), null, 2)
: `姓名,邮箱\n${rows.value.map(row => `${row.name},${row.email}`).join('\n')}`;
};
// 暴露公共 API
defineExpose<TableExpose>({
getData,
setData,
addRow,
validate,
exportData
});
</script>
五、企业级最佳实践
1.严格类型定义,确保类型安全
为暴露的 API 定义 TypeScript 接口,明确入参和返回值类型:
// 定义接口
interface FormExpose {
submit: () => Promise<boolean>; // 返回提交结果
setData: (data: Partial<FormData>) => void; // 入参为部分表单数据
}
// 暴露时指定接口
defineExpose<FormExpose>({ submit, setData });
2.最小暴露原则,隐藏内部实现
仅暴露必要的公共方法,不暴露内部状态(如formData、loading):
// 推荐:只暴露方法,不暴露状态
defineExpose({ submit, reset, validate });
// 不推荐:暴露内部状态,导致外部直接修改
defineExpose({ formData, submit }); // ❌ 外部可直接修改 formData,破坏封装
3.文档化公共 API
为暴露的方法添加 JSDoc 注释,说明用途、参数和返回值:
/**
* 提交表单数据
* @returns {Promise<boolean>} 提交成功返回 true,失败返回 false
* @throws {Error} 验证失败时抛出错误,包含错误信息
*/
const submit = async (): Promise<boolean> => { /* 实现 */ };
4.返回数据副本,避免外部篡改
暴露数据获取方法时,返回副本而非原始引用,防止外部修改内部状态:
// 推荐:返回深拷贝/浅拷贝副本
const getData = () => ({ ...formData }); // 浅拷贝
// 或(深拷贝复杂对象)
const getData = () => JSON.parse(JSON.stringify(formData));
5.统一错误处理
暴露的方法应捕获内部错误并抛出,便于父组件统一处理:
const submit = async (): Promise<boolean> => {
try {
if (!await validate()) throw new Error('表单验证失败');
// 提交逻辑
return true;
} catch (error) {
console.error('提交失败:', error);
throw error; // 抛出错误,让父组件捕获
}
};
六、总结
defineExpose是 Vue 3 组件设计中实现可控通信的核心机制,通过显式暴露 API、结合 TypeScript 类型定义,可大幅提升组件的可维护性和安全性。企业级开发中,需遵循“最小暴露原则”,确保类型安全,并通过文档化和错误处理机制,构建健壮的组件生态。
核心价值:
- 封装与开放平衡:明确组件边界,既隐藏内部细节,又提供必要交互能力
- 类型驱动开发:与 TypeScript 深度集成,减少运行时错误
- 团队协作友好:公共 API 清晰可查,降低协作成本
掌握defineExpose的正确用法,是构建高质量 Vue 3 企业级应用的关键一步。
浙公网安备 33010602011771号