JavaScript 依赖注入 101

2025-05-25

JavaScript 依赖注入 101

在我的文章和演示文稿“现代 Web 开发的 3D”中,我解释了我认为现代 JavaScript 框架成功的关键要素。

等等,你没看我的演示?没关系……如果你只有不到一个小时的时间,我相信你在这里观看会很有价值。

什么/为什么/如何依赖注入?

依赖注入就是其中之一。我发现开发人员经常难以理解它是什么、它是如何工作的,以及为什么它如此重要。

我边做边学,希望一个简单的代码示例能帮助解释。首先,我编写了一个非常小的应用程序,用于组装和运行一辆汽车。依赖关系如下:

Car
|
|--Engine
|  |  
|  |--Pistons
|
|--Wheels
Enter fullscreen mode Exit fullscreen mode

可以将各个部分视为组件之间的依赖关系。您可以在此处查看代码并以交互方式运行它: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();
view raw index.js hosted with ❤ by GitHub

输出应该是您所期望的。

示例输出

太棒了!到目前为止,我们已经完成了一些工作,甚至不需要安装什么复杂的框架。那么,问题出在哪里呢?

代码虽然简单,但能正常工作。问题在一个更大的应用程序中就显现出来了。想象一下,有数百个组件相互依赖……你就会遇到一些问题:

  1. 这些组件彼此直接依赖。如果您尝试将每个组件(车轮、活塞等)拆分到单独的文件中,则必须确保所有内容都按正确的顺序包含才能正常工作。如果您在定义活塞之前创建或包含引擎,代码将会失败。
  2. 您无法并行开发组件。紧密耦合意味着不可能一个开发人员在开发引擎的同时,另一个开发人员在开发活塞。(同样,您也无法在开发引擎的同时,轻易地创建一组空的对象作为活塞的占位符。)
  3. 组件会创建自己的依赖项,因此如果没有依赖项,就无法有效地测试它们。您无法轻松地将“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");
Enter fullscreen mode Exit fullscreen mode

很简单!重要的是要理解,你现在可以并行且独立地进行开发了。例如,这是Engine定义。注意,它依赖于活塞,但没有明确引用实现,而只是引用了标签。

事实上,在我创建的示例中,我在依赖项之前Car定义了和Engine,这完全没问题!您可以在此处查看完整的示例(该库包含在精简代码的底部):https://jsfiddle.net/jeremylikness/8y0ro5gx/$$jsInject

这个解决方案确实有效,但还有一个可能不太明显的额外好处。在示例中,我明确地将“测试引擎”注册为“测试活塞”。但是,您也可以轻松地将“活塞”标签注册到TestPistons构造函数中,一切正常。事实上,我把注册放在函数定义中是有原因的。在一个完整的项目中,它们可能是独立的组件。想象一下,如果您将活塞pistons.js和引擎放入构造函数中engine.js,您可以这样做:

main.js
--engine.js 
--pistons.js
Enter fullscreen mode Exit fullscreen mode

这样就可以创建引擎了。现在你需要编写单元测试。你可以像这样TestPiston实现:testPiston.js

请注意,即使注册了构造函数,你仍然使用标签“pistons” TestPistons。现在你可以进行如下设置:

test.js
--engine.js
--testPistons.js

Enter fullscreen mode Exit fullscreen mode

太棒了!你成功了。

依赖注入 (DI) 不仅仅适用于测试。IoC 容器使并行构建组件成为可能。依赖项在一个地方定义,而不是在整个应用中定义,并且依赖于其他组件的组件可以轻松请求它们,而无需了解完整的依赖链。“汽车”可以请求“引擎”,而无需知道“引擎”依赖于“活塞”。包含文件没有固定的顺序,因为所有依赖项都会在运行时解析。

这是一个非常简单的例子。想要更高级的解决方案,可以看看Angular 的依赖注入。你可以定义不同的注册(称为Providers),例如类型(通过 TypeScript)、硬编码值,甚至是返回所需值的函数工厂。你还可以管理生命周期作用域,例如:

  • 当我请求汽车时总是给我相同的实例(单例)
  • 当我请求汽车(工厂)时总是给我一个新实例

如你所见,尽管人们经常互换使用控制反转 (IoC) 和依赖注入 (DI),但它们之间虽然存在关联,但并非一回事。本示例演示了如何实现 IoC、如何添加依赖注入以及如何使用 IoC 容器解决问题。你觉得理解得更透彻了吗?有任何反馈或疑问吗?请在下方评论区留言告诉我你的想法。

问候,

杰里米·莱克尼斯

文章来源:https://dev.to/azure/dependency-injection-in-javascript-101-2b1e
PREV
学习 Docker - 从零开始,第二部分
NEXT
SOLID 设计原则:构建稳定灵活的系统