省流
- 策略模式渲染(需要二次封装)
- 参数注入,状态下沉
- 不能处理模板差异大的场景
传统方案的痛点
在前端开发中,我们经常遇到这样的场景:同一个页面需要支持新增、编辑和预览三种不同的状态。 在没有统一封装的情况下,开发者通常采用两种思路:
方案一:多套代码分离
为每种状态创建独立的页面组件
问题:
- 代码重复严重:表单字段、布局逻辑大量重复
- 维护成本高:字段变更需要同时修改多个文件
- 一致性难保证:容易出现页面间展示不一致的问题
方案二:单组件参数控制
在一个组件内通过参数控制不同状态:
问题:
- 组件臃肿:大量条件判断使组件变得复杂难读
- 可维护性差:字段增多时条件判断呈指数级增长
核心解决思路
我们的解决方案基于状态下沉 和组件自治的设计理念:
1. 状态管理下沉到子组件
不再在父组件中集中处理所有的状态判断逻辑,而是将页面模式通过依赖注入的方式传递给子组件,让每个子组件根据当前模式自主决定如何渲染和交互。
2. 组件职责分离
- 容器组件:负责提供全局状态(模式)和通用逻辑(数据加载、提交等)
- 表单组件:专注于自身的渲染逻辑,根据注入的模式状态自动适配
- 业务页面:只需关注表单结构,无需处理状态管理细节
3. 三种模式抽象
将页面状态抽象为三种模式:
add
:新增模式,显示空白可编辑表单edit
:编辑模式,显示预填充的可编辑表单view
:预览模式,显示只读内容
具体实现方案
1. 基础表单组件封装
以输入框为例,创建一个表单组件my-input:
vue
<template>
<!-- 预览状态默认展示span -->
<span v-if="mode === 'view'">
{{ $attrs.modelValue }}
</span>
<el-input v-else v-bind="$attrs" />
</template>
<script setup>
// input没有内部逻辑,封装为了处理预览状态
import { inject } from 'vue';
const mode = inject('mode') || 'add';
</script>
2. 页面容器
二级页面逻辑统一,业务处理放在模板中;同时页面不作为组件,为了方便处理如按钮文案不同这类的业务场景; 依靠useAddPage来复用逻辑。
如果新增功能是个弹窗,只要把这个组件换成dialog就可以了,这里不多展示
vue
<template>
<div class="common-layout">
<el-card>
<el-container>
<el-main :view-mode="mode">
<tempForm
ref="tempFormRef"
@finish="closeAndRefresh"
:mode="mode"
:extra="extra" />
</el-main>
<el-footer>
<el-row justify="end">
<template v-if="mode === 'view'">
<el-button type="primary" @click="close">
确定
</el-button>
</template>
<template v-else>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submit">
提交
</el-button>
</template>
</el-row>
</el-footer>
</el-container>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue';
import tempForm from './tempForm.vue';
import useAddPage from '@/hooks/useAddPage';
const tempFormRef = ref(null);
const {
extra,
mode,
submit,
close,
closeAndRefresh
} = useAddPage(tempFormRef);
</script>
useAddPage,核心是接收路由参数和provide注入mode属性
javascript
import { ref, provide } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import { closeTab, closeAndRefreshTab } from '@/utils/route';
/**
* @description: 页面级新增的处理
*/
export default function (tempFormRef) {
const route = useRoute();
const query = route.query;
const mode = query.mode;
// 提供 mode 给组件处理预览
provide('mode', mode);
const extra = ref({
...query
});
const title = query.title;
const store = useStore();
// 根据路由参数设置页面标题
if (title) {
store.dispatch('tab/changeTitle', {
name: route.name,
title
});
}
const submit = () => {
tempFormRef.value.submit();
};
const close = () => {
closeTab(`${ route.name }-${ route.meta.id }-{}-{}`);
};
const closeAndRefresh = () => {
closeAndRefreshTab(`${ route.name }-${ route.meta.id }-{}-{}`);
};
return {
extra,
mode,
submit,
close,
closeAndRefresh
};
}
3. 业务页面使用
在具体的业务页面中,只需要关注表单结构,无需重复处理状态逻辑; 从props处理透传的mode和extra参数,保留submit函数供外部调用
vue
<template>
<!-- 主体表单 -->
<div class="temp-form-normal">
<el-form
:model="formData"
ref="formRef"
v-loading="loading"
label-width="100px"
:inline="true"
:rules="rules">
<h3>| 基础信息</h3>
<el-form-item label="A" prop="A">
<my-input v-model="formData.A" />
</el-form-item>
<el-form-item label="B" prop="B">
<my-select v-model="formData.B" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, defineExpose } from 'vue';
const props = defineProps({
// enum: add, edit, copy
mode: {
type: String,
default: 'add'
},
extra: {
type: Object,
default: () => ({})
}
});
const rules = {};
const dataForm = reactive({
});
const formData = reactive({
});
// 新增修改弹出框提交
const formRef = ref(null);
const validateForm = () => {
return new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (!valid) {
ElMessage.warning('请填写完整信息');
resolve(false);
} else {
resolve(true);
}
});
});
};
// 处理差异化参数
const handleDifferent = (params) => {
if (props.mode === 'edit') {
params.id = props.extra.id;
}
};
const addConfirm = (params) => {
addRdAPI(params).then((res) => {
close();
});
};
const editConfirm = (params) => {
updatedRdAPI(params).then((res) => {
close();
});
};
// 外部提交
const submit = async () => {
const valid = await validateForm();
if (!valid) {
return;
}
const params = {
...formData
};
// 处理差异化参数
handleDifferent(params);
if (props.mode === 'add' || props.mode === 'turn') {
addConfirm(params);
} else {
editConfirm(params);
}
};
defineExpose({
submit
});
const emit = defineEmits(['finish']);
// 外部关闭
const close = () => {
emit('finish');
};
const getDetail = () => {
getDetailApi({ id: props.extra.id }).then((res) => {
});
};
const addInit = () => {
};
const editInit = () => {
};
// 初始化
const init = () => {
if (props.mode === 'add') {
addInit();
} else if (props.mode === 'turn') {
//
} else {
editInit();
}
};
init();
</script>
真实目标
这套流程主要有几方面的考虑:
- 快速的搭建简单的增删改查页面(方便用AI,方便复制粘贴)
- 减少状态判断的心智负担,以及减少template中大段的if代码
- 规范常规流程代码,预留每个阶段的处理函数,控制开发成员自由发挥的空间
其余的细节:
查看时,可能需要一些样式处理,比如去掉表单必填属性的星号,这块考虑用属性选择器去处理,如:view-mode="mode"
less
[view-mode='view'] {
.el-form-item__label:before {
display: none;
}
}
总结
通过将状态管理责任下沉到子组件的设计方案,既避免了多套代码的重复问题,又解决了单组件参数控制的复杂性。只针对三种状态下,模板差异不大的场景。好处在于写一套代码可以报3次工时。