目录结构
组织一个清晰的项目结构需要具备这几个思维和原则:
MECE 法则:强调在划分目录时既要相互独立,又要完全穷尽,可以通过二分法、要素法、流程分析法和矩阵分析法可实现这个法则。 分层思维:合理的分层可以降低项目的开发难度,解除循环依赖,在设计项目时要通过分层思维对我们的组件、样式等进行层次划分,定义好每个层次的职责 领域驱动设计 DDD:通过增加领域层来组织业务代码是一个很好的实践策略,领域层中每个模块就是业务中的一个具体概念,是一个名词,某个领域的全部资源都应该放到领域模块下。 就近原则:如果因为同一个理由需要修改多个文件,那么这几个文件最好放到一起 一致原则:团队应该制定一个统一的命名规范,同时目录下的文件内容要和目录名保持一致,页面组件的命名要和路由 path 保持一致
网络请求
先看一段示例:
// 某业务组件中
const compUser = {
getUsers() {
request(url, "GET", {
params: {
page: 1,
page_size: -1, //假设-1代表请求全部数据
},
}).then((res) => {
this.usersList = res.data || [];
});
},
};虽然现在项目中就这么写的(没法改了,知道归知道,代码是不能动的,依旧是不碰就不背锅指导原则)
- 违反最小知识原则
这个很好解释,用户模块中用到该方法。试想下,如果是电商项目,购物车里也需要请求该方法呢? 倒回去复制粘贴,怕麻烦就越来越麻烦。 所以最好的是封装成一个 service 进行调用:
import { getUsers, deleteUser } from "/apis/user";
export function getUsers(page = 1, page_size = -1) {
return request(userApis.getUsers, {
params: {
page: page,
page_size: page_size,
},
});
}
export function deleteUser(userId) {
return request(userApis.deleteUser, {
params: {
id: userId,
},
});
}- 耦合了后端的接口设计
比如删除用户接口,刚开始后端要求传一个 int 类型的用户 id,后来随着需求变化,删除用户接口支持了批量功能,后端要求 id 必须传数组,而且 id 也变成了 ids。
而如果业务组件中只调用 service 服务则不存在这个问题,我们只需要修改 service 中的 deleteUser 一处代码即可,即省力又省心。
export function deleteUser(userId) {
return request(userApis.deleteUser, {
params: {
ids: Array.isArray(userId) ? userId : [userId],
},
});
}- 增加重复工作
如果我们要对接口返回的数据进行格式化,比如将后端返回的时间戳转为我们前端需要的格式,没有封装 service 就需要在多个业务组件中重复进行,而封装了 service 则只需要在某个方法中处理一次即可。
不绝对,这里根据实际情况而定
总结
- 所有的 API 不能硬编码,建议封装为常量
- 所有的增删改查建议封装为 service 服务,不在业务模块中单独实现
- 任何业务代码和 service 中都不要出现第三方库调用
引用: 最小知识原则:对一个功能模块的更改最好不要依赖大量的知识储备,在设计时就要考虑别人怎么用最简单,最好让一个新人就能快速上手。 依赖倒置原则:上层应用不应该依赖底层实现,而是要从甲方的角度去提要求,要关注我们需要什么,怎么用第三库来实现,而不是别人有什么我们用什么。通过依赖倒置原则解除了项目对第三方库(如 axios)的依赖,实现了解耦。
表单开发
受控组件
就是组件的内部状态可以通过修改属性值的方式进行控制,受控组件必须满足以下 2 个条件:
- 存在一个名为 value 的属性
- 组件的初始值由 value 属性值决定
- value 属性值变化后组件的内部状态也必须跟着变化
- 组件对外抛出 onChange 事件
- 组件内部状态变化后,以抛出 onChange 事件的方式将表单项的最新值传递给父组件
非受控组件
即组件的内部状态由组件自身进行维护,而不是受到外部传递过来的 props 控制,一般来说,非受控组件应该满足以下 2 个条件:
- 对外提供一个名为 defaultValue 的属性
- 组件的初始状态由 defaultValue 决定
- 后续组件的状态随着用户交互而进行变化,但不通知父组件
- 对外提供一个可以获取组件内部状态的方法
- 一般通过 ref 方式访问组件的内部状态 value,如$refs.***.value(不建议直接访问组件内部状态)
- 建议提供一个 method 方法获取内部状态,如 getValue()
复杂业务表单的开发
无论是受控组件方式还是非受控组件方式,都能将复杂表单拆分成多个简单的小表单,并无本质区别,只是受控组件方式的主表单可以一直获取最新且完整的表单数据,而非受控组件则会推迟到全部子表单校验成功后;不过非受控组件的开发逻辑相对简单点,各有优势,可以根据是否需要子表单互动来决定使用哪一种。
重要总结
| 存在问题 | 解决方案 |
|---|---|
| 1. 充满细节 | 将表单项提取为组件 |
| 2. DOM 操作繁琐 | 通过配置驱动表单生成,可以使用配置表单等 |
| 3. 表单项组件封装不规范 | 遵守受控组件和非受控组件的开发要求 |
| 4. 校验规则不统一 | 提取常用校验规则到常量中 |
组件封装
组件(Component)是指一种可重用的UI+逻辑代码块,用于描述用户界面中的一个部分。组件可以包含HTML、CSS和JavaScript代码,可以被重复使用,可以根据需要进行组合和嵌套,从而构建复杂的用户界面。
组件化开发源于分治的思想,在面对复杂的任务时,分而治之是一种行之有效的解决问题方式,将庞大的复杂的系统,拆分成一个个单一的简单的可完成的小任务,然后逐个击破。
组件化优点
- 分离关注点,提升了代码的可读性
- 提升复用性
- UI更一致
- 提升可测试性
- 方便多人协作
如何抽组件
考虑复用性 多个页面重复使用即可抽组件
考虑复杂度 复杂度较高,可拆分多个组件
分离关注点
如何写组件
通用性 = 复用性 + 扩展性
用抽象代替具体
扩展性
扩展DOM 插槽
扩展逻辑 钩子函数
自定义样式
易用性
减少配置,默认值满足大部分需求
符合用户习惯
提供注释或参考文档
可读性
- 组件命名
- 看一眼名字就能知道这个组件大概是做什么用的
- 顶部注释
- 介绍这个组件的作用、适用场景、注意的问题等
- 结构化开发
- 显式修改数据
- 子组件不能修改父组件传递过来的props数据
- 区分元数据(data)和派生数据(computed)
- 不要滥用watch
正交性
正交性代表着耦合程度,耦合低,正交性好,反之正交性差。 父组件耦合子组件、子组件耦合父组件、组件耦合外部数据、组件耦合太多的业务逻辑等
组件开发总结
组件化开发是分治思想的体现,在技术层面和工作流程层面都有很多益处。
技术层面:
分离关注点,提升了代码的可读性 提升了代码的复用性 UI更一致,利于团队规范的执行 提升可测试性,单一公共组件更容易进行测试 工作流程层面:
方便多人协作:只要提前定义好组件的职责和接口,可以多人并行开发 前端人才结构分层:建议搭建自己的团队组件库,可以减低业务开发门槛 何时抽取组件,可以从复用性、复杂度、结构化编程、分离关注点几个角度去考虑。
一个好的组件需要平衡复用性、扩展性、易用性、可读性和正交性,编程就是平衡的艺术。
学会抽象,遵守单一职责原则,有利于提升组件的复用性 插槽可用于扩展DOM,钩子函数可用于扩展逻辑,支持自定义类可用于扩展样式,尽量不在基础组件中使用 !important 组件最好能傻瓜式使用,减少配置,默认值就可以满足大部分场景,命名要符合用户习惯 提升组件可读性需要结合多种手段:组件命名、顶部注释、结构化开发、显式修改数据、区分元数据和派生数据、不要滥用watch等,切记,子组件不可修改父组件传递的props 为了提升组件正交性,要尽量减少父子组件的耦合、组件与外部数据的耦合以及组件和业务逻辑的耦合,组件之间进行通信只能通过对方提供的接口进行,不可擅自访问组件内部的状态和DOM,子组件不要指挥父组件做事 要开发一个组件,需要遵循以下流程:
明确组件的定位:不同定位的组件有不同的评价指标,基础组件更专注通用性,业务组件更专注易用性 列举组件各个场景的使用方法:在未开发之前就能让其他同事感知到该组件是否是他们需要的 确定组件的接口:props、events、methods和slots 设计组件内部的数据:区分哪些是元数据,哪些是派生数据 梳理组件的交互逻辑:通过图、表形式,列举不同交互的处理逻辑 编码:编码是最后一个流程,而不是第一个流程
面向对象
面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计范式,它将程序中的数据和操作数据的方法组织成对象,通过对象之间的交互来实现程序的功能。
类 (Class):类是面向对象编程的基本概念,用于描述具有相似属性和行为的对象的模板。类定义了对象的属性(成员变量)和行为(方法),是创建对象的蓝图。
对象(Object):对象是类的实例,是具体的数据实体。每个对象都有自己的状态(属性值)和行为(方法),可以通过类的构造函数来创建对象。
- 抽象 抽象是对现实世界或问题领域的概念进行简化和概括,从而创建出更通用、更易于理解和使用的程序结构。
- 封装 封装是将数据和操作封装在对象中,隐藏对象的内部实现细节,只暴露必要的接口供外部访问。通过封装,可以保护对象的数据不受外部直接访问和修改。
- 继承 继承允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并可以在其基础上进行扩展和修改。
- 多态 多态是指同一个方法可以根据对象的不同类型表现出不同的行为。
设计模式
设计模式的核心思想 —— 应对变化。
隔离变化
减少对其他模块或整体的影响
- 分离敏态代码和稳态代码
- 隔离使用方和提供方
适配器模式:适配器模式用于将一个类的接口转换成客户希望的另一个接口。通过适配器模式,可以隔离使用方和提供方之间的不匹配,使得它们可以协同工作而不需要修改原有的代码。
代理模式:代理模式用于控制对对象的访问。通过代理模式,可以在使用方和提供方之间引入一个代理对象,代理对象可以控制对真实对象的访问,并在必要时进行一些额外的处理,从而实现对真实对象的访问控制和隔离。
外观模式:外观模式提供了一个统一的接口,用于访问子系统中的一组接口。通过外观模式,可以隐藏子系统的复杂性,为使用方提供一个简单的接口,从而隔离使用方和提供方之间的复杂性。
中介者模式:中介者模式定义了一个对象,它封装了一组对象如何交互的细节,使得这些对象不需要直接相互引用。中介者对象负责协调多个对象之间的交互,从而降低了它们之间的耦合度。
对扩展开放,对修改封闭
代码可读性
总结
重视命名是最简单高效的提升代码可读性的方法,好的命名应该具有自解释性,可以做到见名知意;命名要有区分度,减少歧义;慎用缩写,好名称要能读出来;好名称不怕长,表达清楚意义最重要;命名和内容要一致,拒绝误导;团队应该制作一套统一的词汇表及命名规范。
小而美的代码更容易理解,可以通过拆分函数、模块化、组件化开发来降低单个文件的大小。
清晰的结构有利于提升可读性,使用卫语句尽早return;使用switch替代多个if-else;将一些链式代码改为同步代码;使用管道操作替代循环;结构化编程,一个模块内的代码应该处在一个抽象层次;参数的结构要清晰。
代码应该尽量简洁,不要啰里啰嗦,可以简化Boolean类型的返回;使用短路求值和三元运算来简化代码;善用德摩根定律简化判断条件;表达尽量语义化;尽量使用ES6更加简洁的语法,如用箭头函数替代普通函数。
不要玩魔术,尽可能显式传递数据,减少依赖全局变量,尽量使用纯函数。
只编写必要的注释,当你写注释时应该考虑能不能通过优化代码来替代注释。
团队应该使用良好且统一的代码风格,尽量借助工具来实现风格检查和美化,注意代码编写的顺序。
复用
本章小结
重复的来源可能是代码层面的,也可能是工作流程层面的,比如由于沟通不畅,缺少必要的基础设施(如组件库),知识没有共享等,都会造成团队的工作重复,集体效率和质量的降低。 提升复用性有两个重要方法:抽象和单一职责,越具体的东西越不容易复用,越抽象的东西越容易复用,职责越单一的越容易复用 要注意数据之间的重复,防止出现数据不一致的情况 不合理的复用会带来耦合以及可读性的降低 当一个共用的函数或组件出现大量if-else的时候,说明复用出现了问题,可能需要进行拆分 入口文件尽量不要重复,这样可以增强代码的可读性 是否复用取决于逻辑相关性,不能因为代码相似而复用
