新手用Element UI踩坑记录 如何正确设计组件库API接口让调用者一看就懂
嗨,你好呀!我是Agnes,今天想跟你聊聊我在 Element UI 使用过程中踩过的那些坑,以及我在设计组件库 API 接口时总结出的一些心得。毕竟,好 API 的设计不仅能让自己少掉几根头发,也能让后续的使用者少骂两句。
一、初识 Element UI,满怀期待却频频碰壁
话说我第一次上手 Element UI,是抱着满腔热血和无限憧憬的。官网文档写得挺漂亮,各种组件看着也顺眼,想着这不就是前端开发的救星嘛!结果真正上手写项目的时候,才发现现实远比想象中骨感。
1.1 表单校验的诡异行为
我记得最清楚的就是表单校验这块。当时我想实现一个用户注册表单,包含用户名、邮箱、密码等字段,每个字段都有对应的校验规则。表面上看挺简单,不就是加个 rules 属性嘛:
<el-form :model="form" :rules="rules" ref="registerForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email"></el-input>
</el-form-item>
</el-form>
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
乍一看没啥问题吧?运行起来也确实能校验。但是!当我想动态修改校验规则时,麻烦就来了。比如用户输入的用户名如果已经存在,我需要动态添加一个异步校验规则来检查用户名是否重复。这时候我试着这样做:
// 错误示范:直接修改 rules 对象
this.rules.username.push({
validator: (rule, value, callback) => {
// 异步检查用户名是否重复
checkUsernameExists(value).then(exists => {
if (exists) {
callback(new Error('用户名已存在'));
} else {
callback();
}
});
},
trigger: 'change'
});
结果呢?完全没生效!页面一点反应都没有。我折腾了半天才发现,Element UI 的表单校验规则是在组件初始化时绑定的,动态修改 rules 对象并不会触发重新绑定。
1.2 解决方案:用 :rules 动态绑定
后来我换了种思路,不直接修改 rules 对象,而是通过计算属性或者方法动态生成规则:
computed: {
dynamicRules() {
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
};
// 根据条件动态添加校验规则
if (this.checkUsernameExists) {
rules.username.push({
validator: (rule, value, callback) => {
checkUsernameExists(value).then(exists => {
if (exists) {
callback(new Error('用户名已存在'));
} else {
callback();
}
});
},
trigger: 'change'
});
}
return rules;
}
}
然后模板里改成:
<el-form :model="form" :rules="dynamicRules" ref="registerForm">
这样就能动态控制校验规则了。这个坑让我明白了一件事:Element UI 虽然封装得很好,但它内部有很多”黑盒”机制,不懂原理的话很容易被坑。
二、表格组件的无限坑
表格组件是 Element UI 里功能最强大的组件之一,但同时也是坑最多的。我印象最深的是这几个问题:
2.1 列宽导致的布局错乱
有一次我做了一个后台管理系统,表格里有十几个列,包括文本、数字、日期、状态标签等。正常情况下页面显示得挺正常的,但当我调整浏览器窗口大小的时候,表格列宽就开始乱套了。
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="姓名" width="120"></el-table-column>
<el-table-column prop="age" label="年龄" width="80"></el-table-column>
<el-table-column prop="email" label="邮箱" width="200"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
{{ scope.row.status === 'active' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<!-- 还有很多其他列... -->
</el-table>
问题出在当列数太多、宽度之和超过容器宽度时,Element UI 的表格会自动压缩列宽,但压缩的逻辑并不总是符合预期。有时候某些列被压缩得完全看不清内容,有时候又会出现横向滚动条,体验很差。
2.2 解决方案:配合 show-overflow-tooltip 和自适应布局
经过一番研究和尝试,我总结了一套相对稳定的方案:
<el-table
:data="tableData"
border
style="width: 100%"
:header-cell-style="{background:'#f5f7fa',color:'#606266'}"
show-summary
:summary-method="getSummaries"
>
<!-- 固定列宽,但设置最小宽度 -->
<el-table-column prop="name" label="姓名" min-width="120"></el-table-column>
<el-table-column prop="age" label="年龄" width="80"></el-table-column>
<!-- 关键:使用 min-width 而非固定 width,让列能够自适应 -->
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip></el-table-column>
<!-- 操作列固定在最右侧 -->
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
关键要点:
- 优先使用
min-width而不是固定width,让表格能够根据内容自适应 - 操作列固定在最右侧,使用
fixed="right"属性 - 对可能过长的文本列启用
show-overflow-tooltip,鼠标悬停时显示完整内容 - 必要时启用横向滚动,让用户可以左右滑动查看全部内容
2.3 树形表格的性能陷阱
还有一次,我需要在表格里展示树形结构的数据,比如组织架构、商品分类等。Element UI 的 el-table 原生支持树形数据展示,通过 row-key 和 lazy 属性可以很好地实现:
<el-table
:data="treeData"
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
default-expand-all
>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="code" label="编码"></el-table-column>
<el-table-column prop="sort" label="排序"></el-table-column>
</el-table>
看起来很简单对吧?但是当数据量达到几百条甚至上千条的时候,问题就来了——页面渲染变得极其缓慢,甚至直接卡死。
我后来通过性能分析发现,Element UI 的树形表格在默认情况下会递归渲染所有节点,即使大部分节点是折叠状态。对于大数据量的树形结构,这是不可接受的。
2.4 懒加载+虚拟滚动才是正解
对于大数据量的树形表格,正确的做法是分两种情况处理:
情况一:数据量在几百条以内 可以使用懒加载,只有展开节点时才请求子节点数据:
<el-table
:data="treeData"
row-key="id"
:load="loadNode"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="code" label="编码"></el-table-column>
</el-table>
methods: {
loadNode(row, resolve) {
if (row.level === undefined) {
// 根节点
resolve([]);
} else {
// 加载子节点
fetchChildren(row.id).then(children => {
resolve(children);
});
}
}
}
情况二:数据量超过几百条
这时候光靠懒加载还不够,需要引入虚拟滚动。Element UI 本身不内置虚拟滚动,但可以通过第三方库如 vue-virtual-scroller 来实现:
<virtual-list
style="height: 500px; overflow: auto"
:data-key="'id'"
:data-source="flatData"
:data-component="VirtualRow"
:item-size="40"
:keeps="50"
/>
这样即使有几千条数据,页面也能流畅运行。
三、弹窗组件的隐藏坑
弹窗(el-dialog)是前端最常用的组件之一,但 Element UI 的弹窗也有一些让人头疼的地方。
3.1 弹窗遮罩导致的问题
有一次我遇到了一个很奇怪的问题:在某些页面上,点击按钮打开弹窗时,弹窗能正常显示,但是弹窗外面的页面内容却变成了灰色,点击弹窗外面的区域无法关闭弹窗。
排查后发现,问题出在同一个页面有多个弹窗,并且它们共享了同一个遮罩层。Element UI 的弹窗默认会有一个遮罩层(append-to-body 默认为 false),当多个弹窗叠加时,遮罩层会互相干扰。
<!-- 问题代码 -->
<el-dialog title="用户信息" :visible.sync="dialogVisible">
内容...
</el-dialog>
<el-dialog title="编辑表单" :visible.sync="editDialogVisible">
内容...
</el-dialog>
3.2 解决方案:使用 append-to-body
Element UI 提供了 append-to-body 属性,可以将弹窗挂载到 body 下,避免遮罩层的层叠问题:
<el-dialog
title="用户信息"
:visible.sync="dialogVisible"
append-to-body
>
内容...
</el-dialog>
<el-dialog
title="编辑表单"
:visible.sync="editDialogVisible"
append-to-body
>
内容...
</el-dialog>
这样每个弹窗都会独立挂载到 body 下,互不干扰。
3.3 弹窗滚动穿透问题
另一个经典问题是滚动穿透。当弹窗内容很长需要滚动时,如果弹窗底部的页面内容也在滚动区域内,会出现这种情况:弹窗滚动到顶部后,继续滚动会导致底层页面也开始滚动。
<el-dialog
title="长内容弹窗"
:visible.sync="dialogVisible"
append-to-body
:close-on-click-modal="false"
>
<div style="height: 2000px; padding: 20px;">
<!-- 很长的内容 -->
</div>
</el-dialog>
解决办法是在弹窗打开时禁用底层页面的滚动,弹窗关闭时恢复:
methods: {
openDialog() {
this.dialogVisible = true;
document.body.style.overflow = 'hidden';
},
closeDialog() {
this.dialogVisible = false;
document.body.style.overflow = '';
}
}
不过 Element UI 其实已经内置了这个功能,只需要在 el-dialog 上加 top 属性并确保弹窗内容有滚动条即可。更简单的做法是使用 el-dialog 的 destroy-on-close 属性,在弹窗关闭时销毁内容,下次打开时重新创建:
<el-dialog
title="长内容弹窗"
:visible.sync="dialogVisible"
append-to-body
destroy-on-close
>
<!-- 内容 -->
</el-dialog>
四、设计组件库 API 接口的心得
经历了上面那些坑之后,我开始思考一个问题:如果我要设计一套组件库,该如何设计 API 才能让调用者一看就懂,少踩坑?
4.1 命名一致性原则
这是我踩坑后得出的最重要的一条经验。回想 Element UI 里的一些 API,确实存在一些不一致的地方:
// 有的用 visible,有的用 open
<el-dialog :visible.sync="dialogVisible"> // Element UI 用 visible
<el-drawer :visible.sync="drawerVisible"> // Element UI 用 visible(一致)
<el-popover trigger="click" v-model="popoverVisible"> // 用 v-model
// 有的用 onChange,有的用 change
<el-select @change="handleChange"> // 事件用 change
<el-checkbox :checked="checked" @change="handleChange"> // 事件用 change
// 有的用 value,有的用 modelValue
<el-input v-model="inputValue"> // v-model 绑定 inputValue
<el-slider v-model="sliderValue"> // v-model 绑定 sliderValue
虽然 Element UI 整体还算一致,但确实存在一些让人困惑的地方。如果让我来设计,我会遵循以下原则:
原则一:状态用 isXxx 或 showXxx,不用 visible 和 open 混用
// 推荐的命名
const props = {
// 控制显示/隐藏的状态,统一用 showXxx
showHeader: { type: Boolean, default: true },
showFooter: { type: Boolean, default: true },
showDrawer: { type: Boolean, default: false },
// 控制选中状态,统一用 isXxx
isChecked: { type: Boolean, default: false },
isEnabled: { type: Boolean, default: true },
// 不要混用 visible 和 open
// 统一用 showXxx
};
原则二:事件名统一用 kebab-case,不使用 camelCase
// 不推荐
@onChange="handleChange"
@onInput="handleInput"
// 推荐
@change="handleChange"
@input="handleInput"
// 自定义事件也遵循这个原则
emit('success-change', data)
emit('item-click', item)
原则三:属性命名遵循 “动词+名词” 或 “形容词+名词” 的结构
// 好的命名
maxCount // 最大数量
minWidth // 最小宽度
isLoading // 是否加载中
hasError // 是否有错误
autoScroll // 是否自动滚动
showTooltip // 是否显示提示
// 不好的命名
countMax // 虽然能理解,但不如 maxCount 直观
widthMin // 同上
loading // 歧义,是动词还是形容词?
error // 同上
4.2 默认值的设计哲学
好的 API 默认值应该能让用户”不配置也能用”,这是我最想强调的一点。
举几个反例:
// 不好的设计:默认值不合理
const props = {
// 默认 pageSize 为 1,用户基本都要改,为什么默认不是更合理的值?
pageSize: { type: Number, default: 1 },
// 默认 theme 为 'default',但实际业务中几乎都用 'dark' 或 'light'
theme: { type: String, default: 'default' },
// 默认 disabled 为 true,这意味着用户几乎都要去掉禁用状态
disabled: { type: Boolean, default: true }
};
// 好的设计:默认值符合大多数场景
const props = {
pageSize: { type: Number, default: 20 }, // 20 是常见的分页大小
theme: { type: String, default: 'light' }, // light 是更通用的主题
disabled: { type: Boolean, default: false }, // 默认启用是更合理的
autoSave: { type: Boolean, default: true } // 自动保存对用户更友好
};
4.3 类型检查与 PropTypes
很多人会忽略类型检查的重要性,觉得这只是开发时的事情,生产环境不会用到。但实际上,好的类型检查能帮助调用者快速理解每个属性的用途和预期值。
// 推荐:详细的 prop 定义
const props = {
/**
* 数据源
* @type {Array}
* @required
* @example [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
*/
data: {
type: Array,
required: true,
validator: (value) => {
return Array.isArray(value) && value.every(item =>
item && typeof item === 'object'
);
}
},
/**
* 显示字段名
* @type {String}
* @default 'name'
*/
labelField: {
type: String,
default: 'name'
},
/**
* 值字段名
* @type {String}
* @default 'id'
*/
valueField: {
type: String,
default: 'id'
},
/**
* 是否显示搜索框
* @type {Boolean}
* @default false
*/
showSearch: {
type: Boolean,
default: false
},
/**
* 搜索回调
* @type {Function}
* @param {String} keyword - 搜索关键词
* @returns {Promise<Array>} - 返回搜索结果
*/
onSearch: {
type: Function,
default: null
},
/**
* 模式
* @type {'single' | 'multiple' | 'tree'}
* @default 'single'
*/
mode: {
type: String,
default: 'single',
validator: (value) => {
return ['single', 'multiple', 'tree'].includes(value);
}
}
};
4.4 事件设计的最佳实践
事件设计是 API 设计中容易被忽视的部分。一个好的事件设计应该让调用者清楚知道:什么情况下会触发、触发时传递什么参数、如何取消默认行为。
// 推荐的事件命名和参数设计
const emits = [
/**
* 选中项变化时触发
* @param {Array} selectedItems - 当前选中的项
* @param {Object} extraInfo - 额外信息,包含触发事件的来源等
*/
'select-change',
/**
* 搜索时触发
* @param {String} keyword - 搜索关键词
*/
'search',
/**
* 选项点击时触发
* @param {Object} item - 被点击的选项
* @param {Event} event - 原生事件对象
*/
'option-click',
/**
* 加载数据时触发
* @param {Number} page - 当前页码
* @param {Number} pageSize - 每页数量
*/
'load-data',
/**
* 加载完成后触发
* @param {Array} items - 加载的数据
* @param {Boolean} hasMore - 是否还有更多数据
*/
'load-complete'
];
4.5 插槽设计的清晰边界
插槽的设计应该让用户清楚知道每个插槽的用途和可以访问的数据。
<!-- 推荐的插槽设计示例 -->
<template>
<div class="custom-select">
<!-- 通用插槽:用于自定义触发器 -->
<slot name="trigger" :open="isDropdownOpen" :disabled="disabled">
<div class="select-trigger" @click="toggleDropdown">
<slot>请选择...</slot>
</div>
</slot>
<!-- 下拉面板:用于自定义下拉内容 -->
<slot name="dropdown" :options="filteredOptions" :selected="selected">
<div class="dropdown" v-show="isDropdownOpen && !disabled">
<div
v-for="option in filteredOptions"
:key="option.value"
class="option"
:class="{ selected: option.value === selectedValue }"
@click="handleSelect(option)"
>
{{ option[labelField] }}
</div>
</div>
</slot>
<!-- 底部插槽:用于添加额外操作 -->
<slot name="footer" :selected="selectedItems">
<!-- 默认底部内容 -->
</slot>
</div>
</template>
调用者可以这样使用:
<CustomSelect v-model="selectedValue" label-field="name" value-field="id">
<!-- 自定义触发器 -->
<template #trigger="{ open, disabled }">
<button :class="{ 'is-open': open }" :disabled="disabled">
自定义触发器
</button>
</template>
<!-- 自定义下拉内容 -->
<template #dropdown="{ options, selected }">
<div class="my-dropdown">
<div v-for="option in options" :key="option.id">
{{ option.name }}
<span v-if="selected.includes(option.id)">✓</span>
</div>
</div>
</template>
<!-- 自定义底部 -->
<template #footer="{ selected }">
<button @click="clearAll">清空</button>
<span>已选择 {{ selected.length }} 项</span>
</template>
</CustomSelect>
五、从踩坑到实践的完整案例
为了让大家更直观地理解,我来展示一个完整的组件设计案例。假设我们要设计一个”高级搜索面板”组件。
5.1 组件定义
// AdvancedSearch.vue
import { defineComponent, ref, computed, watch } from 'vue';
export default defineComponent({
name: 'AdvancedSearch',
props: {
/**
* 搜索字段配置
* @type {Array<{ field: String, label: String, type: String, options?: Array, placeholder?: String }>}
*/
fields: {
type: Array,
required: true,
validator: (value) => {
return Array.isArray(value) && value.every(field =>
field && typeof field.field === 'string' && typeof field.label === 'string'
);
}
},
/**
* 搜索参数对象
* @type {Object}
*/
modelValue: {
type: Object,
default: () => ({})
},
/**
* 按钮布局方式
* @type {'horizontal' | 'vertical'}
* @default 'horizontal'
*/
layout: {
type: String,
default: 'horizontal',
validator: (value) => ['horizontal', 'vertical'].includes(value)
},
/**
* 是否显示高级筛选按钮
* @type {Boolean}
* @default true
*/
showAdvancedBtn: {
type: Boolean,
default: true
},
/**
* 是否显示重置按钮
* @type {Boolean}
* @default true
*/
showResetBtn: {
type: Boolean,
default: true
},
/**
* 搜索按钮文本
* @type {String}
* @default '搜索'
*/
searchText: {
type: String,
default: '搜索'
},
/**
* 重置按钮文本
* @type {String}
* @default '重置'
*/
resetText: {
type: String,
default: '重置'
}
},
emits: ['search', 'reset', 'change', 'update:modelValue'],
setup(props, { emit, expose }) {
// 内部状态
const searchParams = ref({ ...props.modelValue });
const isAdvancedOpen = ref(false);
// 计算属性
const activeFields = computed(() => {
return props.fields.filter(field => {
const value = searchParams.value[field.field];
return value !== undefined && value !== null && value !== '';
});
});
const hasActiveFilters = computed(() => {
return activeFields.value.length > 0;
});
// 方法
const handleSearch = () => {
emit('update:modelValue', { ...searchParams.value });
emit('search', { ...searchParams.value });
};
const handleReset = () => {
searchParams.value = {};
emit('update:modelValue', {});
emit('reset', {});
};
const handleFieldChange = (field, value) => {
searchParams.value = { ...searchParams.value, [field.field]: value };
emit('change', { field: field.field, value });
};
const toggleAdvanced = () => {
isAdvancedOpen.value = !isAdvancedOpen.value;
};
// 监听外部变化
watch(() => props.modelValue, (newVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(searchParams.value)) {
searchParams.value = { ...newVal };
}
}, { deep: true });
// 暴露方法给外部调用
expose({
reset: handleReset,
search: handleSearch,
getParams: () => ({ ...searchParams.value })
});
return {
searchParams,
isAdvancedOpen,
activeFields,
hasActiveFilters,
handleSearch,
handleReset,
handleFieldChange,
toggleAdvanced
};
}
});
5.2 模板定义
<template>
<div class="advanced-search" :class="`layout-${layout}`">
<!-- 主搜索区 -->
<div class="search-main">
<div class="search-fields">
<!-- 基础字段 -->
<div
v-for="field in basicFields"
:key="field.field"
class="search-field"
>
<label class="field-label">{{ field.label }}</label>
<component
:is="getComponent(field.type)"
:model-value="searchParams[field.field]"
:options="field.options"
:placeholder="field.placeholder || `请输入${field.label}`"
@update:model-value="handleFieldChange(field, $event)"
/>
</div>
</div>
<!-- 操作按钮 -->
<div class="search-actions">
<button
class="btn btn-primary"
@click="handleSearch"
>
{{ searchText }}
</button>
<button
v-if="showResetBtn"
class="btn btn-default"
@click="handleReset"
>
{{ resetText }}
</button>
<button
v-if="showAdvancedBtn"
class="btn btn-link"
@click="toggleAdvanced"
>
{{ isAdvancedOpen ? '收起' : '高级筛选' }}
<span class="icon" :class="{ 'is-open': isAdvancedOpen }">▼</span>
</button>
</div>
</div>
<!-- 高级筛选区 -->
<div
v-if="isAdvancedOpen"
class="search-advanced"
>
<div class="advanced-fields">
<div
v-for="field in advancedFields"
:key="field.field"
class="search-field"
>
<label class="field-label">{{ field.label }}</label>
<component
:is="getComponent(field.type)"
:model-value="searchParams[field.field]"
:options="field.options"
:placeholder="field.placeholder || `请输入${field.label}`"
@update:model-value="handleFieldChange(field, $event)"
/>
</div>
</div>
<!-- 活跃筛选标签 -->
<div v-if="hasActiveFilters" class="active-filters">
<span
v-for="field in activeFields"
:key="field.field"
class="filter-tag"
>
{{ field.label }}: {{ searchParams[field.field] }}
<button class="tag-remove" @click="handleFieldChange(field, '')">×</button>
</span>
</div>
</div>
<!-- 自定义插槽 -->
<slot name="extra" :params="searchParams" :active-fields="activeFields" />
</div>
</template>
5.3 使用示例
<template>
<div class="page">
<AdvancedSearch
ref="searchRef"
:fields="searchFields"
v-model="searchParams"
layout="vertical"
@search="handleSearch"
@reset="handleReset"
>
<!-- 自定义额外区域 -->
<template #extra="{ params, activeFields }">
<div class="search-stats">
当前共 {{ total }} 条结果,已筛选 {{ activeFields.length }} 个条件
</div>
</template>
</AdvancedSearch>
<!-- 结果列表 -->
<div class="result-list">
<!-- ... -->
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import AdvancedSearch from '@/components/AdvancedSearch.vue';
const searchFields = [
{
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入关键词'
},
{
field: 'category',
label: '分类',
type: 'select',
options: [
{ value: 'tech', label: '技术类' },
{ value: 'design', label: '设计类' },
{ value: 'product', label: '产品类' }
]
},
{
field: 'dateRange',
label: '日期范围',
type: 'date-range'
},
{
field: 'status',
label: '状态',
type: 'select',
options: [
{ value: 'active', label: '启用' },
{ value: 'inactive', label: '禁用' }
]
},
{
field: 'priority',
label: '优先级',
type: 'rate'
}
];
const searchParams = ref({});
const total = ref(0);
const searchRef = ref(null);
const handleSearch = async (params) => {
console.log('搜索参数:', params);
// 调用搜索接口
const result = await fetchSearchResults(params);
total.value = result.total;
// 更新列表...
};
const handleReset = () => {
console.log('已重置搜索条件');
};
// 外部调用组件方法
const resetSearch = () => {
searchRef.value?.reset();
};
const getSearchParams = () => {
return searchRef.value?.getParams();
};
</script>
六、API 设计的 checklist
最后,让我总结一下设计 API 时的 checklist,方便你在实际项目中参考:
6.1 属性设计 checklist
- [ ] 每个属性都有清晰的类型定义
- [ ] 每个属性都有合理的默认值
- [ ] 必填属性都标记了
required: true - [ ] 可选属性都有
default值 - [ ] 枚举类型的属性都有
validator验证 - [ ] 属性的命名遵循一致性原则(不用
visible和open混用) - [ ] 属性的命名遵循”动词+名词”或”形容词+名词”结构
- [ ] 每个属性都有 JSDoc 注释说明用途
6.2 事件设计 checklist
- [ ] 事件命名使用 kebab-case
- [ ] 事件命名遵循”动词+名词”结构
- [ ] 每个事件都有参数说明
- [ ] 事件参数命名清晰易懂
- [ ] 避免使用
change这样过于泛化的事件名(除非确实适用) - [ ] 支持取消默认行为的事件应该提供
preventDefault机制
6.3 插槽设计 checklist
- [ ] 每个插槽都有明确的用途
- [ ] 作用域插槽传递的数据有明确说明
- [ ] 提供默认内容,避免调用者必须覆盖所有插槽
- [ ] 插槽命名清晰,不使用
default以外的通用名(除非确实是默认插槽)
6.4 文档设计 checklist
- [ ] 每个属性都有示例代码
- [ ] 每个事件都有触发时机说明
- [ ] 每个插槽都有使用示例
- [ ] 提供了完整的 API 表格
- [ ] 提供了常见使用场景的示例
- [ ] 提供了 TypeScript 类型定义
七、写在最后
从 Element UI 的踩坑经历中,我最大的收获就是:一个好的 API 设计不是想出来的,而是用出来的。只有真正去使用、去测试、去收集反馈,才能发现设计中的问题。
如果你也在设计组件库或 API,我有几个建议:
- 多参考优秀的设计:Element UI、Ant Design、Material UI 等都是很好的学习对象
- 保持一致性:一旦确定了命名规范,就要始终坚持,不要随意改变
- 写文档:好的文档比好的代码更重要,因为代码是给开发者看的,文档是给使用组件的人看的
- 收集反馈:让实际使用者提意见,他们的痛点就是你改进的方向
- 迭代优化:没有完美的 API,只有不断优化的 API
希望这篇文章能对你有所帮助!如果你有任何问题或建议,欢迎随时交流。记住,踩坑不可怕,可怕的是踩了坑还不总结。每一次踩坑都是成长的机会,对吧?😊
