# 一、开始
最近在整理common库,加深了对下面一句话的理解:“计算机科学领域的任何问题都可以通过增加一个中间层来解决“,这里记录下。
Any problem in computer science can be solved by anther layer of indirection.
分层架构的典型例子有网络七/四层协议,低代码中的UI层、逻辑层、引擎层等,文档中的渲染层、数据层、网络层等。
# 二、业务
先谈谈业务与技术的关系。
业务做的太少,技术比较难提升,做的太多了,就成了搬砖工。二者有点相爱相杀的关系,只有适度的、有挑战性的业务才能促进技术成长。
另外,业务工作是最占时间的,只有提高业务上的工作效率,才能做更多的kpi项目,或者自我提升。
技术上的心得,应该多半都是业务上的,而不是kpi项目的,业务做的不够好、不够稳定,一天到晚都在改bug,谈不上技术提升。
如何提高业务上的效率呢?需要从一些设计、细节做起,提高复用性、通用性、扩展性,提高抽象能力。
比如,业务不仅要考虑本次需求实现的高效,也要考虑后续迭代的效率和稳定,所以需要抽象、拆分组件模块,方便扩展,也就是为什么单个文件不应该很大。所以即便没有复用,也会从大文件中抽离一部分组件。
# 三、分层架构
分层架构是最常见的软件架构,要是不知道用什么架构,或者不知道怎么解决问题,那就尝试加多一层。
分层架构可以看做其他架构(模式)的基础,比如洋葱模型、MVVM、MVC、虚拟DOM、Vuex、微前端、发布订阅,这些都可以当作分层架构的扩展。
分层架构,有点像武侠中的“天下武功,唯快不破”,大道至简,用最简单的方法解决问题。
下面是一些最近做的,可以体现分层架构的设计。注意,下面的例子只是体现了分层架构的思想,是开发过程中的实际问题。
# 1. index.js收口
common库更新后,业务侧需要更改引用路径。
之前:
import { getCityName } from 'src/comm/tools/city'
需要改成:
import { getCityName } from 'src/common/tools/city'
业务有大量这种类似的引用,挨个查找、替换的话太费劲,而且如果改成上面这种,保不准哪天common又改了引用路径。
优化方法就是为common库提供一个收口index.ts
:
// index.ts
export * from 'tools/city'
export * from 'tools/env'
export * from 'tools/url'
业务侧只需要改成:
import { getCityName, getEnv } from 'src/common'
顶层不用关心底层,只要为我提供一个帮助方法。底层负责对外接口的稳定,内部如何升级,顶层不用关注。比如vue2内的升级不会影响用户使用。
类似的,一个页面上,同种类型的多个请求接口、同种类型的多个组件,都可以用这种收口。
# 2. 引用路径别名
还是上面那个例子,有没有其他办法让业务保持稳定,减少改动呢?
可以在引用路径上下手,利用路径别名。业务只要引用路径别名,而别名具体指向的是什么,外层根本不关心。
比如:
alias: {
'@url': 'src/common/tools/city'
}
假如,某一天底层库发布到了npm上,就改下alias配置就行了,比如:
alias: {
'@url': 't-comm'
}
业务侧始终这样使用,爽歪歪:
import { getUrlParam } from '@url'
# 3. Module模块的抽离
这个是较早实现的功能,主要思想是将组件分为两类:不带逻辑的UI组件和带逻辑的Module组件。
- UI组件只能引用UI组件
- Module组件由UI组件或者其他Module组件构成
- 页面可以由两者构成
这样保持一种单向依赖架构。
增加了一个Module层,有以下好处:
- 重构人员可修改UI组件,不修改带逻辑的Module组件,分工明确
- UI组件可有多种类型,比如PC版本、H5版本等,一套逻辑可以多处复用
# 4. logic子仓库
这个也是较早实现的功能,主要解决逻辑的复用。
比如后台多个接口可能复用同一个结构体,前端需要对这一个结构体做一些格式化、解析的工作,我们把这部分抽离出来,而不是每个页面都单独处理。
这个处理可能是跨工程、跨仓库的,我们把它沉淀成子仓库,从而更好的使用。
# 5. 其他
还有一些公共方法的提取,比如之前是这样写接口请求的:
import { post } from 'src/comm/logic/api/comm';
export function fetchScheList({
gid,
start = 0,
num = 10,
}) {
return new Promise((resolve, reject) => {
const reqData = {
gid,
start,
num,
};
post({
url: '/a.b.c.d',
reqData,
})
.then((resp) => {
const data = resp.data || {};
resolve(data);
})
.catch((err) => {
reject(err);
});
});
}
每个接口都要写一次return new Promise(...)
,其实可以再包装一层:
// make-request.js
import { post } from 'src/comm/logic/api/comm';
export function makeRequest({
data,
url,
}) {
return new Promise((resolve, reject) => {
post({
url,
reqData: data,
})
.then((resp) => {
const data = resp.data || {};
resolve(data);
})
.catch((err) => {
reject(err);
});
});
}
接口请求的logic可以这样使用:
import { makeRequest } from '@makeRequest'
export function fetchScheList({
gid,
start = 0,
num = 10,
}) {
return makeRequest({
data: {
gid,
start = 0,
num = 10,
},
url: '/a.b.c.d',
})
}
makeRequest可以再增加一些mock数据、url前缀统一处理等。
# 四、总结
架构的根本目的是解决问题。当一个新问题出现后,往往是某种方法解决了问题,恰好它可以被看作什么架构,而不是非要套用什么什么架构。
什么时候重构代码呢,当代码混乱、庞杂、冗余,通用性不足时,就可以考虑重构,比如提取公共部分、封装公共模块。
← 关于BEM与CSS变量 前端安全 →