《如何写出高质量的前端代码》学习笔记
书接上文《前端组件化开发指南(一)》,这篇我们聊聊如何提升组件的可读性和正交性
HOW如何写出好组件?
4. 可读性
组件命名
- 组件命名应能体现其功能。例如,
AddUserDialog
表示一个用于添加用户的弹窗组件。
顶部注释
- 在组件顶部添加注释,说明组件的作用、适用场景和注意事项。
结构化开发
- 组件应围绕主线任务展开,非主线任务应封装成子组件或工具库。
- 例如,添加用户弹窗的主线任务包括初始化表单数据、配置校验规则和请求API。至于用户头像上传、裁剪、接口调用等信息,不属于主线任务,不应该放在这个组件内部。
显式修改数据
- 子组件不应修改父组件传递的 **
props
****数据。**应通过事件抛出数据变更。
⚠️错误示例 ,父组件中给子组件传值,子组件修改了父组件的值。如何解决?必须查看所有子组件才能读懂该组件的逻辑,难以轻易理解。
html
<template>
<div>
<child1 :data="formData" />
<child2 :data="formData" />
</div>
</template>
<script>
export default {
data(){
return {
formData:{
key1: {},
key2: {}
}
}
}
}
</script>
子组件不能修改父组件传递过来的props数据,这应该是组件开发的一个重要原则。更好的方式就是子组件对外抛出事件。
html
<template>
<div>
<child1 :data="formData" @change="child1Change"/>
<child2 :data="formData" @change="child2Change"/>
</div>
</template>
<script>
export default {
data(){
return {
formData:{
key1: {},
key2: {}
}
}
},
methods:{
child1Change(value){
this.formData.key1 = value;
},
child2Change(value){
this.formData.key2 = value;
}
}
}
</script>
区分元数据和派生数据
元数据:是指组件中最基本的、不可或缺的数据。这些数据通常是直接从外部获取的,或者是用户输入的,代表了组件的当前状态。元数据是组件逻辑的基础,其他数据通常是基于这些元数据计算或派生出来的。比如说代码中的userList。
派生数据:派生数据是基于元数据计算得出的数据。它们通常是元数据的某种变体或总结,帮助简化组件的逻辑和视图渲染。派生数据不需要单独存储,因为它们可以在需要时通过计算得到。比如说代码中的totalCount和vipCount,它们是由userList计算出来的。
比如现在有个用户列表页,展示用户列表,总人数和vip人数,没有使用计算属性:
html
<script>
export default {
data(){
return {
userList: [],
totalCount: 0,
vipCount: 0
}
},
methods:{
init(){
this.userList = [
{id:1, name: 'zhangsan', isVip: true},
{id: 2, name: 'lisi', isVip: false}
];
this.totalCount = this.userList.length;
this.vipCount = this.userList.filter(item => item.isVip).length;
},
updateUserList(){
this.userList = [
{id:1, name: 'zhangsan', isVip: true},
{id: 2, name: 'lisi', isVip: false}
];
this.totalCount = this.userList.length;
this.vipCount = this.userList.filter(item => item.isVip).length;
}
}
}
</script>
对元数据和派生数据不加区分会导致逻辑繁琐/重复,同时变更元数据需要同步修改其他数据。如果忘记修改,会出现bug。
可以将他们改造为计算属性:
html
<script>
export default {
data(){
return {
userList: []
}
},
computed:{
totalCount(){
return this.userList.length;
},
vipCount(){
return this.userList.filter(item => item.isVip).length;
}
}
}
</script>
使用计算属性来处理派生数据有以下几个优点:
-
自动更新:
- 计算属性会自动追踪其依赖的元数据,当元数据发生变化时,计算属性会自动重新计算。这减少了手动更新派生数据的需要,降低了出错的可能性。
-
避免冗余:
- 通过计算属性,派生数据的计算逻辑被集中在一个地方,代码更简洁,逻辑更清晰。不需要在多个地方手动更新派生数据,减少了代码重复和潜在的同步问题。
-
提高可读性:
- 计算属性的定义通常很直观,能清晰地展示派生数据是如何从元数据中得出的,提升了代码的可读性。
不要滥用watch
仅在需要副作用时使用watch
,其他情况应使用计算属性。
还是上面这个例子,如果使用watch代替computed
html
<script>
export default {
data(){
return {
userList: [],
totalCount: 0,
vipCount: 0
}
},
watch:{
userList(){
this.totalCount = this.userList.length;
this.vipCount =this.userList.filter(item => item.isVip).length;
}
},
methods:{
init(){
this.userList = [];
},
updateUserList(userList){
this.userList = userList;
}
//其他逻辑
}
}
</script>
虽然可以看到 totalCount
和 vipCount
的变化逻辑,但你能确定只有在这个 watch
中会修改它们吗?如果其他方法也修改了这些数据,就会出现问题。而计算属性是只读的,不会被其他方法修改。此外,使用 watch
还需要关注数据的初始化值和是否立即执行的问题。
5. 正交性
正交性在组件设计中指的是组件之间的低耦合性。低耦合性意味着组件可以独立于其他组件进行修改和测试,从而提高系统的可维护性和可扩展性。
父组件耦合子组件
父组件与子组件的通信应通过props
、事件和方法进行,避免直接访问子组件的内部状态或DOM。错误示例:
a. 访问组件的内部状态
html
<template>
<MyButton ref="button">按钮</MyButton>
</template>
<script>
export default {
mounted(){
// 错误用法:不应直接操作子组件的内部数据
this.$refs['button'].color = 'red';
}
}
</script>
b. 访问子组件的内部DOM
html
<template>
<MyButton ref="button">按钮</MyButton>
</template>
<script>
export default {
mounted(){
// 错误用法:不应直接操作子组件的内部DOM
this.$refs['button'].$refs['innerButton'].method();
}
}
</script>
子组件耦合父组件
子组件与父组件的通信应通过接收props
和抛出事件,避免直接修改父组件的数据或调用父组件的方法。错误示例:
a. 修改props
html
<script>
export default {
props: ['data'],
methods:{
edit(){
// 错误:不应修改父组件传来的props
this.data.type = 'vip';
}
}
}
</script>
b. 调用父组件方法或修改父组件数据
html
<script>
export default {
methods:{
edit(){
// 错误:不应直接修改父组件的数据或调用其方法
this.$parent.data.type = 'vip';
this.$parent.method();
}
}
}
</script>
组件耦合外部数据
组件应避免直接依赖外部数据(如全局变量、URL参数等),而应通过props
传递必要的数据。错误示例:
html
// 用户详情组件,耦合了URL参数
<script>
export default {
data(){
return {
userId: this.$route.params.userId // 耦合了URL参数
}
}
}
</script>
组件耦合过多业务逻辑
组件应避免耦合具体业务逻辑,通过props
控制功能。错误示例:
html
// MemberList组件
<template>
<div>
<div class="operate">
<button v-if="type==='ordinary'">添加用户</button>
<button v-if="type==='vip'">添加VIP</button>
<button v-if="type==='super-vip'">添加超级VIP</button>
</div>
<MyTable>
<template slot="operate" slot-scope="props">
<button v-if="type!=='ordinary'">删除</button>
</template>
</MyTable>
</div>
</template>
<script>
export default {
props: ['type'],
}
</script>