# 一、开始

最近在整理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层,有以下好处:

  1. 重构人员可修改UI组件,不修改带逻辑的Module组件,分工明确
  2. 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前缀统一处理等。

# 四、总结

架构的根本目的是解决问题。当一个新问题出现后,往往是某种方法解决了问题,恰好它可以被看作什么架构,而不是非要套用什么什么架构。

什么时候重构代码呢,当代码混乱、庞杂、冗余,通用性不足时,就可以考虑重构,比如提取公共部分、封装公共模块。