《如何写出高质量的前端代码》学习笔记
WHAT什么是组件化开发?
组件化开发是一种将 UI 界面拆分成可重用代码块的开发方式。一个完整的页面可以由多个组件构成:
<!-- 一个典型的页面组件结构 -->
<template>
<div>
<Header/>
<main>
<Banner/>
<AboutUs/>
<Services/>
<ContactUs/>
</main>
<Footer/>
</div>
</template>
WHY为什么要使用组件化开发?
1. 提高代码可读性
未组件化的代码,当修改"关于我们"部分的内容时,很难一眼找到你要修改的内容,你需要滚动鼠标滚轮来回查找,也需要阅读与"关于我们"无关的代码。
<div>
<div class="header">
<img src="logo.png"/>
<h1>网站名称</h1>
<!-- 其他头部代码 -->
</div>
<div class="main-content">
<div class="banner">
<ul>
<li><img src="banner1.png"></li>
<!-- 省略n行代码 -->
</ul>
</div>
<div class="about-us">
<!-- 省略n行代码 -->
</div>
</div>
</div>
组件化后的代码,页面结构简洁,修改"关于我们"方便,避免干扰,聚焦组件。可直接进入"AboutUs"组件,删除、增加、编辑某模块,代码可读性增强,维护难度降低。
<div>
<Header/>
<main>
<Banner/>
<AboutUs/>
<Services/>
<ContactUs/>
</main>
<Footer/>
</div>
2. 提升代码复用性
抽取组件可以提高开发效率和降低维护成本,因为它消除了重复。不再需要重新造轮子,拿来就用,需求变更时只需修改组件一处即可。
<!-- 可复用的表单标题组件 -->
<template>
<div class="form-title">
<div class="line"></div>
<h3>{{ title }}</h3>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
title: String
}
}
</script>
3. UI更一致
协作时,若没有共用组件,各自开发的页面可能有差异,即使有UI规范也难以严格执行。例如标题,大小、颜色、padding、margin可能各异。使用组件化可避免这一问题,使开发人员只需使用组件,无需记忆规范。
<!-- 不同级别的表单标题 -->
<FormTitle :level="1"/>
<FormTitle :level="2"/>
4. 提升可测试性
每个组件有自己的职责和功能,可以定义和实现,容易测试,比大页面更简单纯粹。
WHEN何时进行组件抽取?
组件抽取的时机可以从以下4个角度来考虑:
1. 复用性
当一个功能在多个地方重复出现或预期会被重复使用时,应考虑抽取为组件。
最佳实践:
- 发现代码有复用机会时及时抽取
- 开发新功能前先重构已有代码
- 遵循"先重构,再开发"的原则
2. 复杂度
当组件变得过于复杂时,需要拆分成更小的组件以提升可维护性。
判断标准:
- 单个文件代码建议不超过300行
- 功能过于复杂难以维护
- 代码可读性严重下降
3. 结构化编程
通过组件拆分使代码结构更清晰,即使代码量不大也可以拆分。
目的:
- 清晰展示功能主结构
- 隐藏实现细节
- 提升代码可读性
- 类似PPT目录,先概览后细节
4. 分离关注点
每个组件应该只关注自己的核心任务,不同的功能关注点应该分离。
HOW如何写出好组件?
一个好的组件应该从使用者和维护者两个视角来评价:
使用者视角
好组件评价指标 = 复用性 + 扩展性 + 易用性 + 可读性 + 正交性
1. 提升复用性
用抽象代替具体
假设用户列表上有搜索区域,可输入姓名和手机号进行搜索:
反面示例 - 耦合具体业务,UserSearch组件只适用于用户列表搜索场景,复用性较差。如果需要搜索产品列表,则需要再次创建ProductSearch组件。
<!-- UserSearch.vue -->
<template>
<div>
<input v-model="name" placeholder="输入姓名筛选"/>
<input v-model="phone" placeholder="输入手机号筛选"/>
<button @click="emitSearch">搜索</button>
</div>
</template>
<script>
export default {
name: 'UserSearch',
data(){
return {
name: '',
phone: ''
}
},
methods:{
emitSearch(){
this.$emit('search', {
name: this.name,
phone: this.phone
})
}
}
}
</script>
正面示例 - 抽象通用搜索,如果我们从项目角度出发考虑搜索功能,会发现各个列表页都需要搜索功能。搜索组件可以抽象为通用的SearchForm,可配置表单内容不固定,通过抛出search事件,可以在各个页面复用。
<!-- SearchForm.vue -->
<template>
<div class="search-form">
<div v-for="field in fields" :key="field.key">
<component
:is="field.component"
v-model="formData[field.key]"
v-bind="field.props"
/>
</div>
<button @click="handleSearch">搜索</button>
</div>
</template>
<script>
export default {
name: 'SearchForm',
props: {
fields: {
type: Array,
required: true
}
},
data() {
return {
formData: {}
}
},
methods: {
handleSearch() {
this.$emit('search', this.formData)
}
}
}
</script>
使用示例:
<template>
<SearchForm
:fields="fields"
@search="search"
/>
</template>
<script>
export default {
data(){
return {
fields:[
{
key: 'name',
label:'姓名',
component: 'input',
props:{
placeholder: '输入姓名筛选'
}
},
{
key: 'phone',
label:'手机',
component: 'input',
props:{
placeholder: '输入手机号筛选'
}
}
]
}
},
methods:{
search(data){
console.log('搜索条件:', data)
}
}
}
</script>
单一原则
组件就像积木,通过组合成功能模块或页面。复杂的多功能积木用途狭窄,功能单一的积木用途广泛。像ElementUI中的icon、button、input等组件,越小单一的功能,通过组合能发挥更大的威力。适用于各种功能、各种页面、各种行业。
组件封装应遵循单一职责原则,尤其是通用组件和项目基础组件。对于业务领域组件,尽量完成单一功能以提高复用性。
2. 扩展性
组件由DOM、逻辑和样式三部分组成,要提升组件的扩展性,需要从这三个方面同时考虑:
扩展组件 = 扩展DOM + 扩展逻辑 + 扩展样式
DOM扩展 - 插槽机制
通过插槽机制让用户能够在不修改组件源码的情况下扩展组件的DOM结构。
以一个表单标题组件(FormTitle)为例:
- 基础场景:标题前有个绿色竖线
- 特殊场景:需要在标题旁添加提示图标
错误做法:
- 添加一个属性控制是否显示图标
- 导致组件与具体业务耦合
- 违背通用组件设计原则
正确做法:
- 提供插槽供用户扩展
- 保持组件的通用性
- 给插槽设置默认内容提升易用性
逻辑扩展 - 钩子函数
通过预留钩子函数,让用户能够介入组件的处理逻辑。
以一个通用表格组件(CommonTable)为例:
初始设计:
<CommonTable api="/api/v1/user"/>
问题:只支持固定的API格式和返回值结构
改进方案1 - 数据转换钩子:
<template>
<CommonTable api="/other-api/user" :parseData="parseData" />
</template>
<script>
export default {
methods:{
parseData(data){
//对数据格式进行处理
return data;
}
}
}
</script>
作用:处理不同格式的返回数据
改进方案2 - 函数式配置:
<template>
<CommonTable :api="getData" />
</template>
<script>
export default {
methods:{
getData(pageNumber, pageSize){
return request('/other/user',{
params:{
num: pageNumber,
size: pageSize
}
}).then(data=>{
//数据处理
return data
})
}
}
}
</script>
作用:完全自定义数据获取逻辑,更灵活地处理各种接口情况
样式扩展 - 自定义样式
组件除了扩展DOM和逻辑外,通常还需要支持样式扩展,可通过接收用户传递的style或class实现。例如,ElementUI的Popover组件可通过popover-class属性设置弹窗内容的类名。
最佳实践:
- 支持传入自定义class和style
- 避免使用!important
- 通过合理的CSS优先级控制样式
- 预留样式变量供用户定制
3. 易用性
易用性解决的是组件"好不好用"的问题,特别是对新手来说尤为重要。一个易用性好的组件应该具备以下特点:
傻瓜式使用
- 用最简单的方式解决问题
- 降低使用门槛
- 减少学习成本
实际案例: vue-office
问题背景: 文件预览需要写大量代码、配置复杂、新手使用困难
解决方案:
<!-- 一行代码实现文件预览 -->
<vue-office-docx src="file.docx"/>
成功原因:
- 极简使用方式
- 封装复杂性
- 提供完整解决方案
合理的默认值
- 分析使用频率
- 常用场景零配置
- 特殊场景再配置
示例:按钮组件
<!-- 基础使用 - 零配置 -->
<Button>确定</Button>
<!-- 特殊场景 - 按需配置 -->
<Button
type="primary"
size="large"
:loading="true"
>
提交
</Button>
符合用户习惯
命名规范,遵循主流组件库的命名习惯,直观、一致、可预测:
<!-- 好的命名示例 -->
<Dialog :visible="true"/> // 与ElementUI保持一致
<Modal v-model="show"/> // 与AntDesign保持一致
<!-- 不好的命名示例 -->
<Dialog :isHide="false"/> // 不符合常见习惯
<Modal :popup="true"/> // 命名不够直观
总结要点
- 简单优于复杂
- 默认值优于配置
- 习惯优于创新
最佳实践
- 组件文件结构:
components/
├── common/ # 通用基础组件
│ ├── Button/
│ └── Input/
├── business/ # 业务组件
│ ├── UserForm/
│ └── OrderList/
└── layout/ # 布局组件
├── Header/
└── Footer/
2. 组件命名规范:
// 组件名使用PascalCase
export default {
name: 'SearchForm',
// ...
}
3. 提供完整的文档:
// 组件props文档
export default {
props: {
/**
* 按钮大小
* @values small, medium, large
* @default medium
*/
size: {
type: String,
default: 'medium',
validator: value => ['small', 'medium', 'large'].includes(value)
}
}
}
如何提升组件的可读性和正交性,以及组件开发应该遵循怎样的流程,咱们下一章再聊聊。