原则
减少重复代码:封装复用逻辑
原则:通过封装复用逻辑,避免在多个地方重复编写相同的代码片段。
实现示例:针对常用的输入框组件,可以将其通用功能(如验证、双向绑定)提取为单一的自定义指令或混合函数,以提升代码复用性。 ###组件开发原则:保持单一职责 原则:每个组件应专注于完成一种特定功能,避免添加与组件主要功能无关的逻辑。
实现示例:封装按钮组件时,只需关注样式和事件绑定,而将复杂的表单逻辑交由专门的表单组件处理。
提供清晰的接口:规范 props 和事件
原则:定义合理的 props 和 events,避免过多参数,保持接口的简洁性和易用性。
实现示例:组件的所有配置项应通过 props 提供,而状态变化或交互操作则通过 $emit 通知父组件。例如,按钮组件可以通过 @click 事件传递点击事件,而不暴露内部逻辑。
支持扩展性:提供灵活的接口设计
原则:为封装组件提供插槽(slot)、动态样式或回调接口,以便开发者根据需求扩展功能。
实现示例:通过插槽和自定义 props,使组件能够适应不同场景。例如,表格组件可以提供一个 custom-column 插槽,允许用户自定义特定列的显示内容。
遵循团队规范:统一命名和代码风格
原则:在封装组件时,应严格遵守团队的命名、代码风格和功能设计规范,以确保代码的一致性和可维护性。
实现示例:在 Vue 组件中,props 使用驼峰命名(如 modelName),而事件则遵循类似 update:modelValue 的命名约定,确保与主流框架和社区最佳实践保持一致。
总结
通过遵循单一职责、减少重复代码、提供清晰接口、支持扩展性以及统一规范的原则,我们可以显著提升组件的质量、可维护性和复用性,从而构建高效且易于协作的微前端应用系统。
方法
-
继承属性:使用
v-bind="$attrs"
来接收和传递父组件传递的属性。常用于向原组件传递未声明的属性。 -
继承事件:使用
emits
配置声明事件,或者通过$emit
进行事件的触发和传播。 -
继承方法:使用
ref
来暴露内部方法给父组件,通过defineExpose
明确暴露。 -
继承插槽:使用
slots
对象来接收和转发插槽内容。 -
vue2与vue3的区别:在 vue3 中,取消了listeners这个组件实例的属性,将其事件的监听都整合到了attrs上,因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传
-
在 vue2 中,需要用到
$slots
(插槽) 和$scopedSlots
(作用域插槽) -
在 vue3 中,取消了作用域插槽
$scopedSlots
,将所有插槽都统一在 `$slots
v-model
大家应该都知道v-model
只是一个语法糖,实际就是给组件定义了modelValue
属性和监听update:modelValue
事件,所以我们以前要实现数据双向绑定需要给子组件定义一个modelValue
属性,并且在子组件内要更新modelValue
值时需要emit
出去一个update:modelValue
事件,将新的值作为第二个字段传出去。
原因是因为从vue2
开始就已经是单向数据流,在子组件中是不能直接修改props
中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给props
的变量。如果这里我们给input
输入框直接加一个v-model="props.modelValue"
,那么其实是在子组件内直接修改props
中的modelValue
。
如果父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。
第一种方法:将 v-model 拆开,通过 emit 让父组件去修改数据
第二种方法:使用计算属性的 get set 方法
但是如果子组件中有多个表单项,不管是上面哪种方法,都要写很多重复的代码
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="name"></el-input>
<el-input v-model="text"></el-input>
<el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const name = computed(() => {
get() {
return props.modelValue.name
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
name: val
})
}
})
const text = computed(() => {
get() {
return props.modelValue.text
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
text: val
})
}
})
const password = computed(() => {
get() {
return props.modelValue.password
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
password: val
})
}
})
</script>
上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx
,才会触发 set,而 modelList.value.name = xxx
,无法触发。这个时候,Proxy
代理对象可以完美的解决这个问题:
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return new Proxy(props.modelValue, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
emit('update:modelValue',{
...target,
[key]: value
})
return true
}
})
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts
,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的。
useVModel
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = useVModel(props, 'modelValue', emit)
</script>
使用defineModel实现数据双向绑定
defineModel是一个宏,所以不需要从vue中import导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用
<template>
<CommonInput v-model="inputValue" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputValue = ref();
</script>
<template>
<input v-model="model" />
</template>
<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>
在上面的例子中我们直接将defineModel
的返回值使用v-model
绑定到input输入框上面,无需定义 modelValue
属性和监听 update:modelValue
事件,代码更加简洁。defineModel
的返回值是一个ref
,我们可以在子组件中修改model
变量的值,并且父组件中的inputValue
变量的值也会同步更新,这样就可以实现双向绑定。
原理:defineModel其实就是在子组件内定义了一个叫model的ref变量和modelValue的props,并且watch了props中的modelValue。当props中的modelValue的值改变后会同步更新model变量的值。并且当在子组件内改变model变量的值后会抛出update:modelValue事件,父组件收到这个事件后就会更新父组件中对应的变量值。
例子
<template>
<my-input v-model="inputValue" ref="myInputRef" :name="'lyw'" :year="18" @input="onInput">
<!-- 动态插槽定义 -->
<template #prefix>
<span>Prefix Content</span>
</template>
<template #suffix="{ year }">
<span>END: </span>
</template>
<template #default="{ name }">
<p>Default Content: </p>
</template>
</my-input>
<div>当前输入的值是:</div>
</template>
<script lang="ts" setup>
import MyInput from '@/components/MyInput.vue'
import { onMounted } from 'vue'
import { ref } from 'vue'
const inputValue = ref('')
const myInputRef = ref()
function onInput(value: string) {
console.log('Input value:', value)
}
onMounted(() => {
// 光标聚焦
// 避免ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort()
// 前提:子组件把方法暴露
myInputRef.value.focus()
})
</script>
<template>
<!-- 简化:直接使用 computed 绑定到 v-bind -->
<el-input v-model="localValue" v-bind="$attrs" ref="inputRef">
<!-- 动态插槽 -->
<template v-for="(_slot, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
</el-input>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { computed, ref } from 'vue'
// 定义 props 和 emits
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
// 使用 computed 来同步 props 和 emits
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const inputRef = ref('')
let exposeObj: Record<string, any> = {}
const getMethod = () => {
const entries = Object.entries(inputRef.value)
for (const [method, fn] of entries) {
exposeObj[method] = fn
}
}
onMounted(getMethod)
defineExpose(exposeObj)
</script>
一、组件封装五大核心原则
一、组件封装五大核心原则
1. 单一职责原则
技术要点:每个组件应聚焦单一功能点,避免功能耦合
示例场景:
-
✅ 推荐做法:按钮组件仅处理样式/交互,不承载表单验证逻辑
-
❌ 反例:在按钮组件中实现axios请求封装
2. DRY原则(减少重复代码)
技术要点:通过高阶组件或组合式API抽象通用逻辑
实现方案:
// 通用输入框封装示例
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="custom-input"
>
`
}
3. 接口清晰原则
技术规范:
-
Props使用camelCase命名(如
inputValue
) -
事件命名遵循Vue官方规范(如
update:modelValue
) -
为props提供TypeScript类型定义和默认值
4. 可扩展性原则
技术方案:
-
提供具名插槽(named slots)
-
支持CSS自定义属性(CSS Variables)
-
暴露组件实例方法(通过defineExpose)
5. 团队规范适配原则
实施建议:
-
建立团队组件文档(推荐使用Storybook)
-
统一组件前缀(如
BaseButton
、BizTable
) -
制定Props命名白皮书
二、组件继承关键技术(Vue2/Vue3对比实现)
1. 属性透传方案
功能 | Vue2实现 | Vue3实现 |
---|---|---|
属性透传 | v-bind="$attrs" |
v-bind="$attrs" |
事件透传 | v-on="$listeners" |
已整合到$attrs |
插槽透传 | $scopedSlots |
统一使用$slots |
2. 方法暴露方案
// Vue3 Composition API
const formMethods = {
validate() { /*...*/ },
reset() { /*...*/ }
}
defineExpose(formMethods)
// Vue2 Options API
export default {
methods: { ...formMethods }
}
3. 动态插槽实践
<!-- 封装可扩展的布局组件 -->
<template>
<div class="container">
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>
</template>
运行 HTML
三、版本适配指南
Vue2开发者注意事项
-
作用域插槽必须使用
$scopedSlots
访问 -
事件监听器需通过
$listeners
单独处理 -
使用
.sync
修饰符实现双向绑定
Vue3优化特性
-
组合式API实现逻辑复用(推荐使用
<script setup>
) -
v-model
支持多个绑定源 -
样式穿透改用
:deep()
选择器
优化说明:
-
增加版本对比表格,提升技术方案的可比性
-
补充TypeScript类型定义等现代开发实践
-
使用代码注释说明关键配置项
-
添加版本适配专章,明确不同版本的技术实现
-
优化技术术语表述的准确性(如DRY原则的规范表述)
-
增加推荐代码格式(添加template标签包裹示例)
建议后续可补充:
-
组件单元测试示例
-
实际项目中的目录结构示例
-
结合VueUse的工具函数应用案例
+ 保持单一职责 原则:每个组件应专注于完成一种特定功能,不要添加与组件主要功能无关的逻辑。 举例:封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑。 + 尽量减少重复代码 原则:通过封装复用逻辑,减少多处使用同样代码的情况。 举例:可以对常用的输入框组件进行封装,将其通用功能提取为单一组件。 + 提供清晰的接口 原则:定义合理的 props 和 events,避免过多的参数,保持易用性和清晰度。 举例:组件需要的所有配置项应通过 props 提供,事件通过 $emit 通知父组件。 + 支持扩展性 原则:为封装组件提供插槽、动态样式或回调接口,让开发者可以扩展功能。 举例:提供 slot 和 props,使组件可以适应不同场景。 + 遵循团队规范 原则:封装组件时应符合团队的命名、代码风格和功能设计规范。 举例:Vue 组件的 props 使用驼峰命名,事件使用 update:modelValue 风格
好的,我需要帮助用户优化他们提供的Vue组件封装原则部分。首先,我需要回顾用户之前提供的原始内容和优化后的版本,了解之前的改动方向。用户现在给出了另一个需要优化的原则列表,看起来是原始内容的一部分。我的任务是根据之前的优化策略,进一步提升这段内容的文案质量。
首先,分析用户提供的原始原则部分。每个原则目前用列表项呈现,每个条目下有原则和举例。结构比较清晰,但可能可以进一步优化技术准确性、可读性和结构层次。
接下来,回顾之前的优化版本,用户曾将原则分成核心原则和技术方案,使用更明确的标题,添加代码示例,版本对比等。现在需要将同样的优化策略应用到当前的原则部分。
首先检查每个原则的技术表述是否正确。例如,“保持单一职责”是否准确,是否有更专业的术语。可能需要强调“单一职责原则”作为正式术语,而不仅仅是描述。同时,举例部分是否具体,能否用更典型的例子。
然后考虑结构清晰度。当前的结构是列表项,每个包含原则和举例。优化后可能使用更分明的标题,例如使用小标题将每个原则分开,增加技术要点和示例的区分,使用代码块或更明显的视觉元素。
可读性方面,拆分长句,使用项目符号或更简洁的语言。例如,原举例中的“封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑”可以更简洁,或分成多个点。
SEO优化方面,可能需要合理增加关键词,如“Vue组件封装原则”、“最佳实践”等,但用户当前内容可能更偏向技术指南,SEO可能不是重点,但可以适当考虑。
代码示例规范方面,用户之前的优化版本中添加了代码示例,这里是否需要在原则部分添加代码?例如,在“提供清晰的接口”原则中,可以添加一个props和$emit的代码示例,增强可操作性。
版本适配性方面,是否需要指出某些原则在Vue2和Vue3中的差异?例如,在“遵循团队规范”中提到的事件命名风格,是否适用于Vue3的更新,比如update:modelValue是Vue3的语法,可能需要说明。
交互元素方面,是否可以建议添加图示或Codepen链接?但当前内容可能不需要,但用户之前的优化版本有提到,可能需要保持一致性。
现在,根据这些分析,我需要逐一优化每个原则。
1. 保持单一职责:
- 原原则表述正确,但可以更正式地称为“单一职责原则”,并强调SOLID原则中的单一职责。
- 示例中的按钮组件例子可以更具体,比如提到基础组件与业务组件的区分,或添加代码示例展示正确的按钮组件结构。
2. 尽量减少重复代码:
- 原原则提到DRY原则,但用户之前的优化版本已使用DRY作为标题,这里可能需要统一术语。
- 示例中的输入框封装可以添加代码示例,如展示如何提取通用输入框组件,使用props和v-model。
3. 提供清晰的接口:
- 强调接口设计的重要性,props命名规范,事件命名规范。
- 添加代码示例,展示props定义和事件触发的代码片段,可能包括TypeScript类型。
4. 支持扩展性:
- 提到插槽、作用域插槽、CSS变量、暴露方法等具体技术手段。
- 示例可以展示插槽的使用,或者如何通过props传递回调函数。
5. 遵循团队规范:
- 强调团队协作的重要性,可能添加工具推荐,如ESLint配置,或命名规范示例。
- 示例中的事件命名可以对比Vue2和Vue3的不同,如Vue3使用update:modelValue,而Vue2使用.sync修饰符。
结构方面,可以将每个原则作为子标题,每个下面分技术要点、实现方案、示例代码、注意事项等部分,增强层次感。
技术准确性方面,检查术语是否正确,例如“$emit”是否正确使用,Vue3中是否有变化,是否需要区分版本。
版本适配方面,在涉及Vue特定语法时,如事件命名,说明适用的版本,或对比Vue2和Vue3的不同处理方式。
代码规范方面,确保示例代码格式正确,使用正确的语法高亮,添加注释解释关键点。
最后,确保优化后的内容与用户之前的优化版本风格一致,使用类似的标题结构、代码示例格式,以及版本对比的说明。
以下是优化后的技术原则部分,采用分层结构并增强技术指导性:
<button :class=”variant” @click=”$emit(‘click’)”>
XD </button> </template>
## 2\. DRY原则 (避免重复)
### 复用方案
+ 逻辑复用:组合式函数封装(Vue3 Composition API)
+ 模板复用:Renderless组件设计模式
+ 配置驱动:通过JSON Schema生成重复组件
### 最佳实践
// useFormInput.js export function useFormInput(initialValue) { const value = ref(initialValue) const setValue = (newVal) => value.value = newVal return { value, setValue } }
// 在组件中复用 import { useFormInput } from ‘./hooks’ const { value: inputVal } = useFormInput(‘’)
## 3\. 接口清晰化原则
### 设计规范
| 接口类型 | 命名规范 | 类型声明 |
| --- | --- | --- |
| Props | camelCase | defineProps+TS |
| Events | kebab-case | defineEmits+TS |
| Slots | 语义化命名 | defineSlots+TS |
### 类型安全示例
defineProps<{ modelValue: string maxLength?: number }>()
defineEmits<{ (e: ‘update:modelValue’, value: string): void (e: ‘submit’): void }>() ```
4. 可扩展性原则
扩展机制
-
插槽系统:
<template> <div class="card"> <slot name="header"></slot> <slot :data="innerData"></slot> </div> </template>
-
CSS扩展:
.card { padding: var(--card-padding, 16px); }
-
方法暴露:
defineExpose({ reset: () => { /*...*/ } })
5. 团队规范统一原则
标准化方案
-
命名体系:
# 基础组件 BaseButton.vue BaseIcon.vue # 业务组件 AuthLoginForm.vue UserProfileCard.vue
-
文档驱动:
## Button Props | 属性名 | 类型 | 默认值 | |----------|-------|---------| | variant | enum | primary |
-
工具约束:
// .eslintrc { "vue/component-name-in-template-casing": ["error", "PascalCase"] }