JavaScript 依赖注入 101
在我的文章和演示文稿“现代 Web 开发的 3D”中,我解释了我认为现代 JavaScript 框架成功的关键要素。
等等,你没看我的演示?没关系……如果你只有不到一个小时的时间,我相信你在这里观看会很有价值。
依赖注入就是其中之一。我发现开发人员经常难以理解它是什么、它是如何工作的,以及为什么它如此重要。
我边做边学,希望一个简单的代码示例能帮助解释。首先,我编写了一个非常小的应用程序,用于组装和运行一辆汽车。依赖关系如下:
Car
|
|--Engine
| |
| |--Pistons
|
|--Wheels
可以将各个部分视为组件之间的依赖关系。您可以在此处查看代码并以交互方式运行它:https://jsfiddle.net/jeremylikness/gzt6o1L5/。
function Wheels() { | |
this.action = () => log("The wheels go 'round and 'round."); | |
log("Made some wheels."); | |
} | |
function Pistons() { | |
this.action = () => log("The pistons fire up and down."); | |
log("Made some pistons."); | |
} | |
function Engine() { | |
this.pistons = new Pistons(); | |
this.action = () => { | |
this.pistons.action(); | |
log("The engine goes vroom vroom."); | |
}; | |
log("Made an engine."); | |
} | |
function Car() { | |
this.wheels = new Wheels(); | |
this.engine = new Engine(); | |
this.action = () => { | |
this.wheels.action(); | |
this.engine.action(); | |
log("The car drives by."); | |
}; | |
log("Made a car."); | |
} | |
var car = new Car(); | |
car.action(); |
输出应该是您所期望的。
太棒了!到目前为止,我们已经完成了一些工作,甚至不需要安装什么复杂的框架。那么,问题出在哪里呢?
代码虽然简单,但能正常工作。问题在一个更大的应用程序中就显现出来了。想象一下,有数百个组件相互依赖……你就会遇到一些问题:
- 这些组件彼此直接依赖。如果您尝试将每个组件(车轮、活塞等)拆分到单独的文件中,则必须确保所有内容都按正确的顺序包含才能正常工作。如果您在定义活塞之前创建或包含引擎,代码将会失败。
- 您无法并行开发组件。紧密耦合意味着不可能一个开发人员在开发引擎的同时,另一个开发人员在开发活塞。(同样,您也无法在开发引擎的同时,轻易地创建一组空的对象作为活塞的占位符。)
- 组件会创建自己的依赖项,因此如果没有依赖项,就无法有效地测试它们。您无法轻松地将“piston”替换为“test piston”。在 Web 应用中,这对于单元测试至关重要。例如,您希望能够在测试中模拟 Web API 调用,而不是发出真正的 HTTP 请求。
稍微重构一下就能解决第三个问题。你听说过一种叫做控制反转的模式吗?它很简单。现在,组件控制着它们自己的依赖关系。让我们反转一下,这样组件就不再控制它们了。我们会在其他地方创建依赖关系并注入它们。控制反转会移除直接依赖,而依赖注入则是将实例传递给组件的方式。
为了简单起见,我只会包含更改的代码。请注意,现在不再直接创建依赖项,而是将依赖项传递给构造函数。您可以在此处查看整个应用程序并以交互方式运行它:https://jsfiddle.net/jeremylikness/8r35saz6/
现在我们已经应用了控制反转模式,并正在进行一些简单的依赖注入。然而,在大型代码库中我们仍然面临一个问题。之前的问题(#1 和 #2)尚未得到解决。请注意,对象必须按照正确的顺序创建。以非正常顺序添加或创建对象将导致失败。这使得并行开发或非正常顺序开发变得复杂(相信我,这种情况在大型团队中经常发生)。团队中的新开发人员必须了解所有依赖项,才能在自己的代码中实例化组件。
再说了,我们能做什么?
解决方案是引入 IoC(控制反转)容器来管理依赖注入。容器有很多种类型,但它们的典型工作方式如下:
- 您将获得一个容器的全局实例(您可以拥有多个容器,但为了保持简单,我们将坚持使用一个容器)
- 向容器注册你的组件
- 你从容器中请求组件,它会为你处理依赖关系
首先,我将包含一个我编写的非常小的库,名为jsInject。这是我专门为学习和理解依赖注入而编写的库。您可以在这里阅读它:通过 JavaScript 解释依赖注入,但我建议您等到阅读完本文之后。在您熟悉 DI 和 IoC 之后,您可以深入了解我是如何创建容器的。该库可以做很多事情,但简而言之,您传递一个标签和一个构造函数来注册一个组件。如果有依赖项,则可以传递一个包含这些依赖项的数组。下面是我定义Pistons
类的方法。请注意,除了注册组件的代码行之外,代码几乎与上次迭代 100% 相同。
要获取类的实例,您不需要直接创建它,而是“询问”容器:
var pistons = $jsInject.get("pistons");
很简单!重要的是要理解,你现在可以并行且独立地进行开发了。例如,这是Engine
定义。注意,它依赖于活塞,但没有明确引用实现,而只是引用了标签。
事实上,在我创建的示例中,我在依赖项之前Car
定义了和Engine
类,这完全没问题!您可以在此处查看完整的示例(该库包含在精简代码的底部):https://jsfiddle.net/jeremylikness/8y0ro5gx/。$$jsInject
这个解决方案确实有效,但还有一个可能不太明显的额外好处。在示例中,我明确地将“测试引擎”注册为“测试活塞”。但是,您也可以轻松地将“活塞”标签注册到TestPistons
构造函数中,一切正常。事实上,我把注册放在函数定义中是有原因的。在一个完整的项目中,它们可能是独立的组件。想象一下,如果您将活塞pistons.js
和引擎放入构造函数中engine.js
,您可以这样做:
main.js
--engine.js
--pistons.js
这样就可以创建引擎了。现在你需要编写单元测试。你可以像这样TestPiston
实现:testPiston.js
请注意,即使注册了构造函数,你仍然使用标签“pistons” TestPistons
。现在你可以进行如下设置:
test.js
--engine.js
--testPistons.js
太棒了!你成功了。
依赖注入 (DI) 不仅仅适用于测试。IoC 容器使并行构建组件成为可能。依赖项在一个地方定义,而不是在整个应用中定义,并且依赖于其他组件的组件可以轻松请求它们,而无需了解完整的依赖链。“汽车”可以请求“引擎”,而无需知道“引擎”依赖于“活塞”。包含文件没有固定的顺序,因为所有依赖项都会在运行时解析。
这是一个非常简单的例子。想要更高级的解决方案,可以看看Angular 的依赖注入。你可以定义不同的注册(称为Providers
),例如类型(通过 TypeScript)、硬编码值,甚至是返回所需值的函数工厂。你还可以管理生命周期或作用域,例如:
- 当我请求汽车时总是给我相同的实例(单例)
- 当我请求汽车(工厂)时总是给我一个新实例
如你所见,尽管人们经常互换使用控制反转 (IoC) 和依赖注入 (DI),但它们之间虽然存在关联,但并非一回事。本示例演示了如何实现 IoC、如何添加依赖注入以及如何使用 IoC 容器解决问题。你觉得理解得更透彻了吗?有任何反馈或疑问吗?请在下方评论区留言告诉我你的想法。
问候,
文章来源:https://dev.to/azure/dependency-injection-in-javascript-101-2b1e