《如何写出高质量的前端代码》学习笔记
书接上文《前端组件化开发指南(二)》,这篇我们聊聊组件开发的基本流程
组件开发的基本流程
在日常组件开发中,遵循以下流程可以帮助我们写出高质量的组件:
- 明确组件定位:确定组件是通用组件还是业务组件,是纯UI组件还是带状态组件等。
- 列举组件使用示例:在编码前,假设组件已开发完成,给出使用示例并进行团队评审。
- 确定组件的接口:包括属性(props)、事件(events)、方法(methods)和插槽(slots)。
- 设计组件的内部数据:区分元数据(data)和派生数据(computed)。
- 梳理组件的交互逻辑:用图或文字描述组件内部交互流程。
- 编码:在详细设计文档的指导下进行编码。
1. 组件定位
组件定位决定了后续的设计和实现。以 chakra-ui 的 Table 组件为例:
<TableContainer>
<Table variant='simple'>
<TableCaption>Imperial to metric conversion factors</TableCaption>
<Thead>
<Tr>
<Th>To convert</Th>
<Th>into</Th>
<Th isNumeric>multiply by</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>inches</Td>
<Td>millimetres (mm)</Td>
<Td isNumeric>25.4</Td>
</Tr>
<!-- More rows -->
</Tbody>
<Tfoot>
<Tr>
<Th>To convert</Th>
<Th>into</Th>
<Th isNumeric>multiply by</Th>
</Tr>
</Tfoot>
</Table>
</TableContainer>
如果每个业务表格都用这个组件写,确实会觉得代码量很大,易用性差。但如果我们用它作为基础组件,在其上封装一个定制的表格组件,就会变得容易得多。因为基础组件的每个部分都可以通过代码进行修改和扩展。因此,评价一个组件的好坏,首先要看它的定位。
- 通用组件:更注重通用性,易用性可以适当让步,不应耦合业务逻辑,支持通用属性和事件;
- 业务组件:更注重易用性,只为某个具体业务服务的。
组件使用示例
在开发组件时,先给出使用示例:
<!--组件支持传递log内容-->
<CommonLog :log="log"/>
<!--组件支持自行获取log内容, 需要传递一个函数-->
<CommonLog :get-log="getLogFunction"/>
<script>
export default {
methods: {
getLogFunction() {
return '';
}
}
}
</script>
<!--组件支持自动刷新数据,间隔为2000ms-->
<CommonLog :get-log="getLogFunction" auto-refresh :interval="2000"/>
2. 确定组件的接口
组件接口包括:
- 属性(props) :如
type,value / v-model。 - 事件(events) :如
blur,focus。 - 方法(methods) :如
focus,blur。 - 插槽(slots) :如
prefix,suffix。
3. 设计组件的内部数据
组件的内部数据分为元数据和派生数据:
export default {
data() {
return {
loading: false,
list: []
}
},
computed: {
vipList() {
return this.list.filter(item => item.isVip);
},
vipCount() {
return this.vipList.length;
}
}
}
4. 梳理组件的交互逻辑
描述组件内部交互流程:
/**
* 组件挂载时:请求某某接口,初始化某某数据
* 点击删除按钮时: 校验某某,然后二次确认,确认后将loading设为true,调用某某接口,关闭loading,toast删除成功
* 点击保存按钮时:表单校验,不通过滚动到表单失败处;通过后loading设为true,调用某某接口,关闭loading,toast添加成功
*/
5. 编码
编码是最后一个流程,基于详细的设计文档进行实现。
实战示例
组件定位
实现一个通用的 SearchForm 组件,不耦合业务逻辑,支持自定义配置。
使用示例
- 示例1:通过数组配置表单项,暴露
search事件。
将这个数组命名为fields,在fields中配置每个搜索项的key、label和组件component,component可以为字符串,如input、select、radio、switch等常见的表单类型。
<template>
<SearchForm :fields="fields" @search="search"/>
</template>
<script>
export default {
data() {
return {
fields: [
{ key: 'rule', label: '规则名称', component: 'input' },
{ key: 'description', label: '描述', component: 'input' }
]
}
},
methods: {
search({ rule, description }) {
// 根据搜索项的值进行查询
}
}
}
</script>
- 示例2:组件样式支持配置。
支持在表单项之间设置间隔,参考ElementUI布局组件;默认间隔为20px,支持自定义。支持表单项宽度、label宽度、类名className,便于后续扩展自定义样式。
<template>
<SearchForm :gutter="20" :labelWidth="'120px'" :fields="fields"/>
</template>
<script>
export default {
data() {
return {
fields: [
{ key: 'rule', label: '规则名称', component: 'input', width: '300px', labelWidth: '100px', className: 'rule' },
{ key: 'description', label: '描述', component: 'input' }
]
}
}
}
</script>
- 示例3:表单项支持自定义组件。
上面我们配置的component是一些常见表单项的名称,如果不满足用户需求,则可以传递过来一个自定义组件。
<template>
<SearchForm :fields="fields" />
</template>
<script>
import CustomComponent from './CustomComponent.vue'
export default {
data() {
return {
fields: [
{ key: 'rule', label: '规则名称', component: CustomComponent }
]
}
}
}
</script>
- 示例4:支持通过插槽形式扩展表单项。
每个表单项提供两个插槽:label的插槽和表单项的插槽,label插槽名称为 ${key}-label,表单项插槽名和key值相同。
<template>
<SearchForm :fields="fields">
<template slot="rule-label">
<span>规则名称<i class="el-icon-question"></i></span>
</template>
<template slot="rule" slot-scope="{formData}">
<el-input v-model="formData.rule" />
</template>
</SearchForm>
</template>
<script>
export default {
data() {
return {
fields: [{ key: 'rule' }]
}
}
}
</script>
- 示例5:表单项支持设置默认值。
表单首次挂载时,支持设置每个表单项的默认值defaultValue,后续不再监听defaultValue值的变化,除非手动调用SearchForm的方法resetFields。
<template>
<SearchForm ref="SearchForm" :fields="fields" />
</template>
<script>
export default {
data() {
return {
fields: [
{ key: 'rule', component: 'input', defaultValue: 'test' }
]
}
},
methods: {
resetFields() {
this.$refs.SearchForm.resetFields();
}
}
}
</script>
- 示例6:表单项支持设置属性和事件。
每个表单项可以配置其支持的所有属性props和事件events。需要注意一点就是,像Select这种组件,我们支持通过options来配置选项,这不是原生的Select或ElementUI 中的el-select的功能,所以需要实现一种特殊的Select子组件。
<template>
<SearchForm :fields="fields"/>
</template>
<script>
export default {
data() {
return {
fields: [
{
key: 'rule',
label: '规则名称',
component: 'input',
componentProps: { placeholder: '请输入规则名称', maxlength: 30 },
componentEvents: {
change(value) {
// 规则的change事件
}
}
},
{
key: 'type',
label: '规则名称',
component: 'select',
componentProps: {
placeholder: '请选择类型',
options: [
{ label: '类型1', value: 'type1' },
{ label: '类型2', value: 'type2' }
]
}
}
]
}
}
}
</script>
- 示例7:获取
SearchForm的当前值。
<template>
<SearchForm ref="SearchForm"/>
</template>
<script>
export default {
methods: {
getSearchFormData() {
this.$refs.SearchForm.getFields();
}
}
}
</script>
组件接口
SearchForm 属性
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| gutter | 间隔,单位像素 | number | 20 |
| labelWidth | label宽度 | string / number | — |
| fields | 表单项配置 | array | [] |
SearchForm 事件
| 事件名称 | 说明 | 回调参数 |
|---|---|---|
| search | 触发搜索事件 | (values) |
SearchForm 方法
| 方法名 | 说明 |
|---|---|
| resetFields | 重置搜索表单为初始值 |
| getFields | 获取表单当前值 |
SearchForm 插槽
| name | 说明 |
|---|---|
| 表单key-label | 每个表单项的label插槽 |
| 表单key | 每个表单项的插槽 |
组件内部数据
组件内部至少包含以下状态:
formData:存放表单项当前值。componentsMap:存储fields中配置的component字符串值与真实组件的对应关系。defaultValues:表单的默认值,基于fields的计算属性。innerFields:经过处理的fields,用于渲染所需的真正数据。
组件内部交互逻辑
- mounted:初始化
formData数据。 - 表单项渲染逻辑:通过动态组件实现。
- 点击搜索按钮逻辑:抛出
search事件。 - 点击重置按钮逻辑:将
formData设为defaultValues。
编码实现
最终 SearchForm 组件的完整实现如下:
<template>
<div class="search-form">
<div class="search-form-fields">
<div class="search-form-field" v-for="(field, index) in innerFields"
:key="field.key"
:style="{ width: field.width, marginRight: index < fields.length - 1 ? gutter + 'px' : 0 }">
<span class="search-form-field-label" :style="{ width: field.labelWidth }">
<slot :name="`${field.key}-label`">{{ field.label }}:</slot>
</span>
<div class="search-form-field-component">
<slot :name="field.key" :formData="formData">
<component
:is="field.component"
size="mini"
v-model="formData[field.key]"
v-bind="field.componentProps"
v-on="field.componentEvents"
style="width: 100%"/>
</slot>
</div>
</div>
</div>
<div class="search-form-buttons">
<el-button size="mini" @click="resetFields">重置</el-button>
<el-button type="primary" size="mini" @click="search">查询</el-button>
</div>
</div>
</template>
<script>
import SearchFormSelect from "./SearchFormSelect";
export default {
name: "SearchForm",
props: {
gutter: { type: Number, default: 20 },
labelWidth: { type: [String, Number] },
fields: { type: Array, default: () => [] }
},
data() {
return {
formData: {},
componentsMap: { 'input': 'el-input', 'select': SearchFormSelect }
};
},
computed: {
defaultValues() {
return this.fields.reduce((values, field) => {
values[field.key] = field.defaultValue;
return values;
}, {});
},
innerFields() {
return this.fields.map(field => {
let width = field.width ? (typeof field.width === 'number' ? field.width + 'px' : field.width) : '250px';
let labelWidth = field.labelWidth ? (typeof field.labelWidth === 'number' ? field.labelWidth + 'px' : field.labelWidth) : this.labelWidth;
let component = typeof field.component === 'string' ? this.componentsMap[field.component] || 'el-input' : field.component;
return { ...field, width, labelWidth, component };
});
}
},
mounted() {
this.formData = { ...this.defaultValues };
},
methods: {
search() {
this.$emit('search', { ...this.formData });
},
resetFields() {
this.formData = { ...this.defaultValues };
this.$emit('search', { ...this.formData });
},
getFields() {
return { ...this.formData };
}
}
};
</script>
<style scoped lang="less">
.search-form {
display: flex;
align-items: center;
.search-form-fields {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 10px;
.search-form-field {
display: flex;
align-items: center;
.search-form-field-label {
display: inline-block;
text-align: right;
margin-right: 4px;
}
.search-form-field-component {
flex: 1;
}
}
}
}
</style>
前端组件化开发指南总结
组件化开发体现了分治思想,具有以下优点:
-
技术层面:
- 提升代码可读性和复用性。
- 提高UI一致性和可测试性。
-
工作流程层面:
- 方便多人协作。
- 降低业务开发门槛。
开发组件时需平衡复用性、扩展性、易用性、可读性和正交性。遵循单一职责原则,使用插槽和钩子函数扩展功能,减少组件间耦合。通过详细设计文档指导编码,确保组件设计合理,编码工作轻松。