你会在JS中使用依赖注入吗?

1,082 阅读9分钟

控制反转(IoC)技术的出现打破了开发者的传统编程模式,她使得开发者更灵活更容易地控制代码。依赖注入则是控制反转的一种形态,旨在分离构造对象和使用对象的关注点。本文中,你将会了解到什么是依赖注入,什么时候你会用到她,哪些流行的JS框架已经实现了她。

什么是依赖注入?

依赖注入是一种软件设计模式,一个对象或者函数需要使用其他的对象或者函数(依赖)的时候并不需要关心其内部实现细节,向需要的对象提供依赖关系的任务留给了注入者,有时候我们也称他们为汇编器、供应者或者容器。

例如,考虑视频游戏机如何只需要兼容的光盘或盒式磁带即可运行。不同的光盘或盒式磁带通常包含有关不同游戏的信息。玩家不需要了解游戏机的内部结构,通常只需要插入或更换游戏光盘即可玩游戏。

通过这种方式设计软件或编写代码,可以很容易地删除或替换组件(如上面的示例),编写单元测试,并减少代码库中的样板代码。

虽然依赖注入是其他语言(特别是Java、C#和Python)中常用的一种模式,但在JavaScript中却不太常见。在下面的部分中,您将考虑这种模式的利弊、它在流行的JavaScript框架中的实现,以及如何在考虑依赖注入的情况下设置JavaScript项目。

你为什么需要依赖注入?

为了充分理解依赖注入为什么有用,了解IoC技术解决的问题很重要。例如,考虑前面的视频游戏机类比;尝试用代码表示控制台的功能。在本例中,您将把控制台称为NoSleepStation,并使用以下假设:

  • NoSleepStation控制台只能玩为NoSleepStation设计的游戏
  • 控制台的唯一有效输入源是光盘

有了这些信息,可以通过以下方式实现NoSleepStation控制台:

// The GameReader class 
class GameReader { 
    constructor(input) { 
        this.input = input; 
    } 
    readDisc() { 
        console.log("Now playing: ", this.input); 
    } 
}

// The NoSleepStation Console 
class class NSSConsole { 
    gameReader = new GameReader("TurboCars Racer"); 

    play() { 
        this.gameReader.readDisc(); 
    } 
} 

// use the classes above to play 
const nssConsole = new NSSConsole(); 
nssConsole.play();

核心控制台逻辑位于“GameReader”类中,它有一个从属的“NSSConsole”。控制台的“play”方法使用“GameReader”实例启动游戏。然而,这里有一些很明显的问题,包括灵活性和测试。

灵活性

前面提到的代码是不灵活的,如果用户想玩不同的游戏,他们必须修改“NSSConsole”类,这类似于在现实生活中拆开控制台。这是因为核心依赖项“GameReader”类被硬编码到“NSSConsole”实现中。

依赖注入通过将类与其依赖项分离来解决这个问题,只在需要时提供这些依赖项。在前面的代码示例中,“NSSConsole”类真正需要的只是来自“GameReader”实例的“readDisc()”方法。

通过依赖注入,可以像这样重写前面的代码:

class GameReader { 
    constructor(input) { 
        this.input = input; 
    } 
    readDisc() { 
        console.log("Now playing: ", this.input); 
    } 
    changeDisc(input) { 
        this.input = input; 
        this.readDisc(); 
    } 
} 

class NSSConsole { 
    constructor(gameReader) { 
        this.gameReader = gameReader; 
    } 
    play() { 
        this.gameReader.readDisc(); 
    } 
    playAnotherTitle(input) { 
        this.gameReader.changeDisc(input); 
    } 
} 

const gameReader = new GameReader("TurboCars Racer"); 
const nssConsole = new NSSConsole(gameReader); 
nssConsole.play(); 
nssConsole.playAnotherTitle("TurboCars Racer 2");

此代码中最重要的更改是“NSSConsole”和“GameReader”类已分离。虽然“NSSConsole”仍然需要“GameReader”实例才能运行,但它不必显式创建一个。创建“GameReader”实例并将其传递给“NSSConsole”的任务留给依赖注入提供程序。

在大型代码库中,这可以显著帮助减少代码样板,因为创建和连接依赖项的工作由依赖项注入提供程序处理。这意味着您不必担心创建某个类所需的类的实例,尤其是当这些依赖项有自己的依赖项时。

测试

依赖注入最重要的优点之一是在单元测试中。通过将提供类依赖项的工作委托给外部提供程序,可以使用模拟或存根来代替未测试的对象:

// nssconsole.test.js 
const gameReaderStub = { 
    readDisc: () => { 
        console.log("Read disc"); 
    },
    changeDisc: (input) => { 
        console.log("Changed disc to: " + input); 
    }, 
}; 
const nssConsole = new NSSConsole(gameReaderStub);

只要依赖项的接口(公开的方法和属性)不变,就可以使用具有相同接口的简单对象单元测试来代替实际实例。

依赖注入的限制

需要注意的是,使用依赖注入并非没有缺点,这通常与依赖注入如何解决减少代码样板的问题有关。

为了充分利用依赖注入,特别是在大型代码库中,必须设置依赖注入提供程序。设置这个提供程序可能需要很多工作,在一个简单的项目中有时是不必要的开销。

这个问题可以通过使用依赖注入库或框架来解决。然而,在大多数情况下,使用依赖注入框架需要完全购买,因为在配置过程中,不同的依赖注入框架之间没有太多API相似之处。

流行js框架中的依赖注入

在很多流行的js框架中依赖注入是个很重要的特性,比较有名的包括Angular,NestJS和Vue。

Angular

Angular是一个客户端web开发框架,包含构建、测试和维护web应用程序所需的工具。依赖注入内置于框架中,这意味着不需要配置,这是使用Angular开发的推荐方法。

以下是依赖注入如何在Angular中工作的代码示例(摘自Angular文档):

// logger.service.ts 
import { Injectable } from '@angular/core'; 

@Injectable({providedIn: 'root'}) 
export class Logger { 
    writeCount(count: number) { 
        console.warn(count); 
    } 
} 

// hello-world-di.component.ts 
import { Component } from '@angular/core'; 
import { Logger } from '../logger.service'; 

@Component({ 
    selector: 'hello-world-di', 
    templateUrl: './hello-world-di.component.html' 
}) 
export class HelloWorldDependencyInjectionComponent { 
    count = 0; 
    constructor(private logger: Logger) { } 
    onLogMe() { 
        this.logger.writeCount(this.count); 
        this.count++; 
    } 
}

在Angular中,对类使用“@Injectable”修饰符表示可以注入该类。可注射类可通过三种方式提供给家属:

  • 在组件级别,使用“@component”修饰符的“providers”字段
  • 在NgModule级别,使用“@NgModule”修饰符的“providers”字段
  • 在应用程序根级别,通过将“providedIn:root”添加到“@Injectable”修饰符(如前所述)

在Angular中注入依赖项就像在类构造函数中声明依赖项一样简单。参考前面的代码示例,下面一行显示了如何做到这一点:

constructor(private logger: Logger) // Angular injects an instance of the LoggerService class

NestJS

NestJS是一个JavaScript框架,其架构可有效构建高效、可靠和可扩展的后端应用程序。由于NestJS在设计上从Angular中汲取了大量灵感,因此NestJS中的依赖注入工作原理非常相似:

// logger.service.ts 
import { Injectable } from '@nestjs/common'; 

@Injectable() 
export class Logger { 
    writeCount(count: number) { 
        console.warn(count); 
    } 
} 

// hello-world-di.component.ts 
import { Controller } from '@nestjs/common'; 
import { Logger } from '../logger.service'; 

@Controller('') 
export class HelloWorldDependencyInjectionController { 
    count = 0; 
    constructor(private logger: Logger) { } 
    onLogMe() { 
        this.logger.writeCount(this.count); 
        this.count++; 
    } 
}

注意,在NestJS和Angular中,依赖关系通常被视为单体。这意味着一旦找到依赖项,它的值就会被缓存并在整个应用程序生命周期中重用。要在NestJS中更改此行为,需要配置“@Injectable”装饰器选项的“scope”属性。在Angular中,您将配置“@Injectable”装饰器选项的“providedIn”属性。

Vue

Vue.js是一个用于构建用户界面的声明性和基于组件的JavaScript框架。在创建组件时,可以使用“provide”和“inject”选项配置Vue.js中的依赖注入。

要指定哪些数据应提供给组件的后代,请使用“provide”选项:

export default { 
    provide: { 
        message: 'hello!', 
    } 
}

然后可以使用“inject”选项将这些依赖项注入到需要它们的组件中:

export default {
    inject: ['message'],
    created() {
        console.log(this.message) // injected value
    }
}

要提供应用程序级依赖关系,类似于Angular中的“providedIn:'root”,请使用Vue.js应用程序实例中的“提供”方法:

import { createApp } from 'vue' 

const app = createApp({}) 

app.provide(/* key */ 'message', /* value */ 'hello!')

Js依赖注入框架

到目前为止,您已经在JavaScript框架的上下文中考虑了依赖注入。但是,如果有人想在不使用任何上述框架的情况下启动一个项目,并且他们不想从头开始实现依赖注入容器/解析器,该怎么办?

有几个库为这个场景提供依赖注入功能,即injection-jsInversifyJSTSyringe。在下一节中,您将重点介绍InversifyJS,但可能需要看看其他包,看看它们是否更适合您的需求。

InversifyJS是JavaScript的依赖注入容器。它旨在尽可能减少运行时开销,同时促进和鼓励良好的面向对象编程(OOP)和IoC实践。项目的存储库可以在GitHub上找到。

要在项目中使用InversifyJS,需要将其添加到项目的依赖项中。在这里,您将使用整个游戏示例来设置InversifyJS。因此,使用“npm init”创建一个新项目。打开终端,按顺序运行以下命令:

mkdir inversify-di 
cd inversify-di 
npm init

接下来,使用npm或Yarn将“inversify”添加到项目的依赖项中:

# Using npm 
npm install inversify reflect-metadata 
# or if you use yarn: 
# yarn add inversify reflect-metadata

InversifyJS依赖于Metadata Reflection API,因此您需要安装并使用“reflect-Metadata”包作为polyfill。

接下来,通过首先创建两个空文件来添加“NSSConsole”和“GameReader”类的代码:

touch game-reader.mjs nssconsole.mjs

然后继续将以下代码分别附加到每个文件:

// game-reader.mjs 
export default class GameReader { 
    constructor(input = "TurboCars Racer") { 
        this.input = input; 
    } 
    readDisc() { 
        console.log("Now playing: ", this.input); 
    } 
    changeDisc(input) { 
        this.input = input; this.readDisc(); 
    } 
}

// nssconsole.mjs 
export default class NSSConsole { 
    constructor(gameReader) { 
        this.gameReader = gameReader; 
    } 
    play() { 
        this.gameReader.readDisc(); 
    } 
    playAnotherTitle(input) { 
        this.gameReader.changeDisc(input); 
    } 
}

最后,将项目配置为使用InversifyJS:

touch config.mjs index.mjs
// config.mjs 
import { decorate, injectable, inject, Container } from "inversify"; 
import GameReader from "./game-reader.mjs"; 
import NSSConsole from "./nssconsole.mjs"; 

// Declare our dependencies' type identifiers 
export const TYPES = { 
    GameReader: "GameReader", 
    NSSConsole: "NSSConsole", 
}; 
// Declare injectables 
decorate(injectable(), GameReader); 
decorate(injectable(), NSSConsole); 
// Declare the GameReader class as the first dependency of NSSConsole 
decorate(inject(TYPES.GameReader), NSSConsole, 0); 
// Declare bindings 
const container = new Container(); 
container.bind(TYPES.NSSConsole).to(NSSConsole); 
container.bind(TYPES.GameReader).to(GameReader); 

export { container };
// index.mjs 
// Import reflect-metadata as a polyfill 
import "reflect-metadata"; 
import { container, TYPES } from "./config.mjs"; 

// Resolve dependencies 
// Notice how we do not need to explicitly declare a GameReader instance anymore 
const myConsole = container.get(TYPES.NSSConsole); 

myConsole.play(); 
myConsole.playAnotherTitle("Some other game");

您可以通过从项目的根目录运行“node-index.mjs”来测试新设置。

这个例子虽然很小,但对于大多数希望使用InversifyJS的项目来说,可以作为一个很好的起点。

总结

在本文中,您了解了JavaScript中的依赖注入、它的优缺点、流行JavaScript框架中的示例,以及如何在普通JavaScript项目中使用它。

如果您正在寻找JavaScript的替代依赖注入库或审查其维护和包运行状况评分,请查阅Snyk Advisor列出的23个最佳JavaScript依赖注入库