依赖注入, 在 English 里缩写是 DI, 感兴趣但还不太了解这个概念的大哥可以去 Dependency Injection https://en.wikipedia.org/wiki/Dependency_injection
开头先来一句: M$ 大法好, TypeScript 真香!
前言
前些日子一直在抽空搞一个自己的 TypeScript 相关项目, 大体是一个 base 在依赖注入 (DI, 后面就缩写了) 平面之下的 node 服务端框架. 写着写着, 觉得很有必要把 DI 平面从框架里面剥离出来, 改善一下扩展性, 沉淀成一个通用的解决方案. 在这里呢, 我打算整理一下之前所写的东西, 也大致讲讲怎样一步一步地搭建出一个简单的 DI 系统.
先列个提纲比较有利于最后烂尾:)
DI 相关的技术概念
DI 可以用来解决什么场景的问题
一个最小化功能的 DI 库应该拥有哪些功能
有哪些值得注意的点
那我们写一个?
后面的还没有想好...
DI 相关的技术概念
概念就不啰嗦了, 直接来一发「 Dependency Injection https://en.wikipedia.org/wiki/Dependency_injection 」维基百科解决.
我曾经接触过几款好用的 DI 框架, 比如伟大的信仰「ASP.NET Core」, 有近几年在前端引领 ts 潮流的「Angular」, 也包括现在在 node 领域里也非常火的「nest」. 我甚至在 PHP 中 (我真的非常讨厌写 PHP) 也见过 DI 框架, 只是对于 PHP 个人了解的太少, 也就不拿出来细说了.
说来惭愧, 我还没有看过其中任何一个的源代码, 任何一个! 我太懒了 !!!
我曾经用过 C# 的「Simple Injector」, 大致和「ASP.NET Core」官方的理念相似, 我也在 JS 中见过 DI, 比如最早的「AngularJS」, 相比静态语言强大的 DI 框架, JS 里面的 DI 虽然相对蹩脚, 但那也的确算得上是实用至上了.
按照我自己的理解, 只要是类 C/S 模型, 在设计上秉承着静态语义, 接口分离, 开放封闭, 供应消费, 定义先行这些理念, DI 基本可以认为总是一个首选的解决方案. DI 能够带来 API 层面更大的稳定性, 不仅可以让我们更专注于业务的编排, 解开领域之间的强耦合, 还能优化逻辑的拆分隔离, 为系统提供更强的健壮性.
相比较前端而言, 后端老板们已经玩了 DI 很多年了, 虽然有些复杂的 DI 特性在大多数情况下显得是屠龙术了, 但 DI 本身上面提到最精干的那些能力, 足以经受住时间和实践的考验.
而从另一个方面来讲, 近年来 TypeScript 火起来其实也并不是一个偶然. JS 这门动态语言在经历岁月的磨洗之后, 终于让广大的前端老板认识到 "不自由毋宁死, 一自由就堕落" 这句真谛. 稳定还是需要静态语法的强暴才可以保障, 而 DI 则是可以突破静态语言缺陷的超级武器.
DI 到底要用来解决什么场景的问题
这个问题详细展开之前, 不如来用一个例子来描述一下现实的痛点.
就拿用的很多的 koa 来讲吧(express 的回调语法太蛋疼了, 不用 - -),koa2 是一个很优秀也很轻巧的 node 服务端框架, 包了一些简单的能力出来, 以适应最小化的服务器模型.
但 koa 终究还是过于轻量了, 或者说并不存在那种绝对完备的 node 框架, 可以适应一切业务的需求, 大多数时候我们都需要对他进行定制. 所以我们可能会对 koa2 进行基于 es6 class 语法的重新组装, 构造出诸如 controller,service 的典型 MVC 概念. koa 提供了一个 context 对象来承载单词请求的所有信息, 为了方便我们就在 ctx(context, 上下文)上扩展我们所要的能力, 并把他送往请求的任意一个角落.
到目前为止这并不会存在问题.
接下来我们会面临一些能力共用的场景: 假如有个基础能力, 简单到诸如存储和读取当前 session 的用户信息, 这个能力真的非常基础和简单, 但可能用到的地方却又相当广泛. 在 service 里我们可能需要直接读取登录态来发动请求或者数据落库, 在 controller 我们需要用户信息进行一些简单的逻辑组合, 在中间件里我们可能需要针对用户信息进行权限处理. 有一些基础而通用的能力, 我们总是在很多地方重复的使用, 这并不适合挂载在单继承模式的 controller 或者 service 的继承链路里面, 因为存在一个很简单的矛盾: 不同继承分支中的能力并不能够共用.
这些暂且都可以放在一遍, 业务不大, 这些问题并不是什么痛点.
然后业务扩大了, 能力共用的需求迫在眉睫, 框架继承是一种相比分包引用更佳稳妥的思路 (虽然扩展和定制能力相对较弱) 来适应业务的普适性. 于是经验丰富的老板开始围绕 koa 设计出了一套框架继承的做法, 每一层框架会继承前一层框架所有的配置, 插件, ctx 扩展和基础服务组件等等, 相当牛 b. 这是合理的, 一个公司可能会抽象一些通用逻辑作为基础框架, 就省去了各个业务的大量重复开发成本.
但这样的处理, 会加剧之前面临的问题: 每一个仓库提供出来一个继承了原始框架基础 service 和 controller 的新 base, 事实上已经将后续的能力彻底的隔离开来了, 未来基于这个框架的业务都会自己继承 base 扩展进去新的能力, 并按照自己的继承链路继续下去, 有些能力在业务中本来可以共享, 但他们却在之前一步分道扬镳, 进入了两条独立的继承链路中去了. 形象的描述就是: 一条单分叉的河 (一单分叉就不会再汇合了) 在一个地方分叉 (分成支流 controller 和支流 service) 了, 他的两条支流未来就不会再有交集了, 包括一些通用的逻辑(比如获取 session, 两边必须有一份完全相同的逻辑), 都不能在想互共享了, 你只能写两份.
那不还有 context 吗?
对, context 可以很好的解决这种困境. 只要 service 和 controller 都拥有一个 context, 那挂载 context 伤的属性和行数就都可以在 service 和 controller 中共用了. 这也正是 koa 的做法. koa 的 context 是一个巨大的对象, 大到几乎整个业务的核心要素都在里面了. 各级框架通过不断定制 context,service 和 controller 或者中间件通过修改 context 的数据, 就可以简单地做到数据共享. prefect!
事情似乎完美的解决了?
然后有一天, 我们的老板一声令下, 整个架构都应该准备切到 TypeScript 来了.
糟糕, 这下问题可就大了....
... .. .
太晚了, 再不睡觉明天早上又要迟到了 (待续
来源: https://juejin.im/post/5c7ea41fe51d4541ad0c9da1