// 演示代码,常见的依赖场景,引用自附录资料
import {A} from './A';
import {B} from './B';
class C {
consturctor() {
this.a = new A();
this.b = new B(this.a);
}
}
// 在依赖注入形式下
import {A} from './A';
import {B} from './B';
// init container & bind dep
const container = new Container();
container.bind(A); // bind也可以指定一个名字,bind('A', A)
container.bind(B);
class C {
consturctor() {
this.a = container.get('A');
this.b = container.get('B');
}
}
// 伪代码
// 绑定、获取
class container {
pravite pool = new Map();
public bind(identifier){
const binding = new Binding(identifier);
this.pool.set(identifier, binding)
}
public get(key) {
return this.pool.get(key)
}
}
前言
重构用于提高代码的可读性或者改变代码内部结构与设计,增进内部的清晰性和一致性,并且移除死代码,使其在将来更容易被维护。前段时间负责的某个前端系统也进行了一次目标接近的重构。重构前系统大致如下:
其中主工程作为平台的核心项目,多个系统(首页、管理台、Node服务)放在一个仓库中,各种配置堆到一起,归属不清晰,每次发布子模块都需要更新整个工程,如果发布时没有按节点更新,还可能会有短暂的不可用时间。从代码层面看,成片的Ctrl+C&V,到处都是一把梭代码,文件越来越多,文件结构不受控制,业务开发变得越来越困难,bug难以追踪(vscode搜索表示也无能为力),维护起来极其痛苦,开发体验、效率持续下降。以上总结一下就是:
高耦合,大量逻辑堆砌在一个组件里
0单测覆盖,且极难写好单测
代码重复率极高,难以扩展
发布流程比较乱
妥妥的技术债,该还了。
动手
物理分层
为了解决上面提及的痛点,首先要对这些应用进行拆分,即“分治”,先从物理层面拆分,主工程中的系统之间并无依赖,可拆分到不同的仓库进行维护,如下:
这样每个工程单独部署,开发、发布都互不影响,开发效率、稳定性得以大幅提升。
拆分解耦
对于逻辑众多的管理台工程,处理则要更细致些。我们先看看一般情况下如何对组件进行代码复用、分层。
以上截取了项目中很常见的组件代码组织方式,略作分析:
最后导致代码重复率极高,排查问题效率低下,加上0单测,要重构都不容易。程序不便于扩展、不便于修改,可以说它同样不是一个好的系统或者不是好的代码。另外还可以想想,如何在保证质量的前提下提供写单元测试的效率?
对症下药,在逻辑层面进行拆分,把共有逻辑抽离封装,表格、图标、表单等公共组件也类似,全局状态可以抽到顶层使用RxJS、Redux统一维护等等,同时引入TypeScript做静态类型检查等,于是可以有:
反应到代码上,
这样代码结构清晰,职责明确,模块复杂度下降极多,也更易编写测试。TypeScript对于大型项目的好处,不言而喻,结合IDE简直爽到飞起。
还可以更好
上面这种拆分会带来一个问题,漫天飞舞的引用,模块间看似独立而又不独立。
那么使用枚举试试?
本质上换汤不换药,而且入口维护在数个文件集中管理,不算是一种好的实现。往具体实现看,调用方每次使用依赖仍需要先实例化,模块间还有可能存在依赖关系,这种方法对这些无能为力。
随着模块越来越多,依赖关系也会越来越复杂,维护起来很麻烦。还有办法吗?
来看看依赖注入。依赖注入DI是控制反转(IoC)的一种实现。依赖是指可被方法调用的事物,某个方法、实例。在依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入(Inject)” ,传递依赖给调用方,而不是让调用方直接获得依赖。举个例子,
在这个例子中调用方无需关心依赖什么时候创建,使用时直接获取实例即可,容器会自动将所有依赖的对象实例化。所谓容器可以简单理解为一个Map,或者说是为调用方搭建的一个灵活、可扩展的平台,能更好地进行依赖复用。
网络上有很多手写IOC的例子、文章,这里不再赘述,亦可参见InversifyJS的实现。
但如果每次使用都要像上面这样手动绑定,再获取,那使用会非常繁琐。更好的实现是借助TypeScript装饰器。
例如以上依赖调用的场景,我们可以实现一个Inject装饰器,
这样通过@Inject标注,上述代码C中A、B的依赖可简化如下:
Provider的作用简单理解就是将类绑定到服务容器中。
依赖注入的引入,基本根除了全局引用,调用方无需关心依赖状态,随用随取,模块有了更好的可扩展性,代码健壮性进一步提升。
新架构
思路有了,那如何应用到具体的实现中呢?
先来看下我们UI的大致结构:
其中顶部栏为通用模块,例如产品/业务的切换、个人信息的展示等,左侧边栏Aside为各功能的导航,用户可以点击子项切换功能,中间区域则是具体的功能呈现。
于是可以有一个基本的设计如下:
但这样我们需要手动维护Aside、Router组件的内容 。所以更进一步的,可以把组件Link、Comp等抽象成配置维护在一个状态里,UI容器在加载时拉取配置生成对应的Aside组件和Router。跟着上面依赖注入的思路,可以实现一个Provider装饰器@Feature,来完成这个过程:
大致实现如下:
最终架构演变成如下:
首屏优化
重构不总是尽善尽美,也有不如人意的地方,比如首屏时间过长......从监控系统中看到首屏时间如下,
即使用户对加载时间不敏感,随着操作链路中大量的刷新、跳页,还是很影响效率。
要降低首屏时间,就得尽量利用浏览器的网络策略,静态资源越少越好,越小越好,数据加载越快越好。对此做了以下工作,
A.then(B)
,使用Promsie.all并发接口请求,部分请求延迟加载(loaded)...通过以上方法,打包体积下降了70%,首屏降至1s以下,页面打开速度大幅提升。
首屏时间也可以借助Chrome Lighthouse等工具来测量。
总结
最后再简单总结一下,
以上,重构完成后,整个系统的分工更加明确,责任更加清晰。代码质量大幅提升,自动化测试从0到整体覆盖率70%,圈复杂度降至1.5,代码重复率、规范问题、ESLint问题等等等全部搞定,一举还完了技术债,喜大普奔~
参考资料