Frontend, Mobile

Wstrzykiwanie zależności w React przy użyciu InversifyJS

InversifyJS to rozbudowana, a zarazem lekka i łatwa w użyciu biblioteka do wstrzykiwania zależności w JavaScript. Niestety, ze względu na naturę Reacta, wykorzystanie jej w komponentach nie należy do bardzo oczywistych. Wstrzykiwanie zależności w InversifyJS polega na wstrzykiwaniu w konstruktory, podczas gdy React nie pozwala nam rozszerzać konstruktorów komponentów. W tym artykule pokażę, na przykładzie prostego projektu, jak możemy obejść to ograniczenie, by móc korzystać z odwrócenia sterowania w projektach Reactowych.

Tomasz Świstak.Absolwent informatyki na Politechnice Wrocławskiej. Od 3 lat pracuje jako .NETowy full-stack developer w firmie Synergy Codes, gdzie tworzy zaawansowane wizualizacje danych w aplikacjach webowych. Oprócz tego interesuje się tematyką sztucznej inteligencji i gamedevem.


Przykładowy projekt ReactNative

Na potrzeby artykułu stworzyłem prosty projekt Reactowy w TypeScript. Z punktu widzenia artykułu najciekawsze dla nas są:

  • Inicjalizacja odwrócenia sterowania (w skrócie: IoC).
  • Klasa NameProvider, która dostarczy tekst do wyświetlenia przez komponent Reactowy.
import * as React from "react";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Hello } from "./Hello";

const App = () => (
  <div>
    <Hello />
  </div>
);

render(<App />, document.getElementById("root"));
import { Container } from "inversify";
import { IProvider, NameProvider } from "./providers";

export const container = new Container();
container.bind<IProvider<string>>("nameProvider").to(NameProvider);
import { injectable } from "inversify";

export interface IProvider<T> {
  provide(): T;
}

@injectable()
export class NameProvider implements IProvider<string> {
  provide() {
    return "World";
  }
}

To, co widzimy na listingu 1, jest typowym wykorzystaniem InversifyJS. Tworzymy nowy kontener IoC, do którego przypisujemy klasę pod ustalonym identyfikatorem (tutaj jest to string, ale może też być Symbol lub typ). Jest to deklaracja w tak zwanym krótkim zasięgu (ang. transient scope), co oznacza, że z każdym wstrzyknięciem tworzymy nową instancję klasy. Dostępne są także zasięgi: singleton (zawsze ta sama instancja przy każdym wstrzyknięciu) i żądania (ang. request scope – ta sama instancja w trakcie pojedynczego wywołania container.get).

Każda klasa, która może być wstrzyknięta przez Inversify, musi posiadać dekorator injectable. Przy zwykłym użyciu, zależności byłyby wstrzykiwane przy użyciu dekoratora inject wewnątrz konstruktora lub na poszczególnych właściwościach. Jednak w przypadku Reacta nie jest to możliwe, toteż musimy to zrobić w inny sposób.

Reszta projektu to prosta aplikacja Reactowa. Hello to komponent, który wykorzystuje provider do wyświetlenia nazwy w nagłówku. W index wykonujemy renderowanie całości w DOMie. Na obecną chwilę kod nie będzie działać, ponieważ nameProvider jest pusty. W tym właśnie miejscu wstrzykniemy instancję klasy NameProvider.

Wykorzystanie inversify-inject-decorators

Jest to “oficjalny” sposób użycia Inversify, w przypadku gdy nie możemy skorzystać ze wstrzykiwania do konstruktorów. Biblioteka ta udostępnia nam cztery dodatkowe dekoratory: lazyInject, lazyInjectNamed, lazyInjectTagged, lazyMultiInject. Jak widać po nazwach, wykonują one wstrzykiwanie z zastosowaniem leniwej ewaluacji. Oznacza to, że zależność nie jest dostarczona w momencie inicjalizacji obiektu (jak to się dzieje w przypadku domyślnego inject), a dopiero podczas jej pierwszego użycia i jest przechowywana do późniejszych użyć. Przechowywanie może być wyłączone poprzez ustawienie odpowiedniej wartości boolowskiej przy inicjalizacji dekoratorów.

Użycie jest bardzo proste. Pierwsze, co należy zrobić, to wykonać funkcję getDecorators, która zwróci wspomniane wyżej dekoratory dla podanego kontenera. W przypadku naszego przykładu użyjemy jedynie lazyInject, ze względu na to, że nie używamy nazw, tagów, a także nie przypisujemy wielu różnych klas pod jeden identyfikator. Następnie, jedyne co musimy zrobić, to użyć dekoratora lazyInject na właściwości nameProvider. Możesz zobaczyć te zmiany na Listingu 2.

import * as React from "react";
import { lazyInject } from "./ioc";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  @lazyInject("nameProvider") private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
import { Container } from "inversify";
import getDecorators from "inversify-inject-decorators";
import { IProvider, NameProvider } from "./providers";

const container = new Container();
container.bind<IProvider<string>>("nameProvider").to(NameProvider);

const { lazyInject } = getDecorators(container);

export { lazyInject };

Biblioteka jest bardzo prosta w użyciu i osobiście miałem okazję używać ją w wielu projektach, które jednocześnie korzystały z Reacta i InversifyJS. Niestety, korzystając z niej można bardzo łatwo doprowadzić do sytuacji, gdzie mamy zależności cykliczne. Najczęściej miałem je wtedy, gdy dzieliłem kontener na moduły, które trzymałem w oddzielnych plikach. Jeżeli masz podobną strukturę kodu i natrafiłeś na zależność cykliczną, najprostszym sposobem na jej uniknięcie jest rozdział ładowania modułów do kontenera i eksportowania lazyInject do oddzielnych plików.

Cały przykład można znaleźć tutaj.

Wykorzystanie inversify-react

Innym sposobem jest użycie inversify-react. Jest to biblioteka, która wykonuje wstrzykiwanie zależności w nieco inny sposób. Udostępnia nam ona komponent Reactowy – Provider, który przechowuje kontener IoC i przekazuje go w dół drzewa Reactowego. Razem z nim otrzymujemy cztery dekoratory – provide, provide.singleton, provide.transient i resolve. W naszym przykładzie będziemy potrzebować jedynie resolve, aby otrzymać zależność oraz Provider, aby przekazać kontener. resolve działa w podobny sposób jak lazyInject z poprzednio opisywanego sposobu, jednak nie wymaga żadnej inicjalizacji.

Aby użyć biblioteki, musimy umieścić naszą aplikację wewnątrz komponentu Provider, do którego przekazujemy kontener IoC. W Hello używamy dekoratora resolve na nameProvider i przekazujemy mu odpowiedni identyfikator. ioc.ts pozostaje bez zmian (w przeciwieństwie do poprzedniego sposobu). Zmiany zostały pokazane na Listingu 3.

import * as React from "react";
import { resolve } from "inversify-react";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  @resolve("nameProvider") private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "inversify-react";
import { Hello } from "./Hello";
import { container } from "./ioc";

const App = () => (
  <Provider container={container}>
    <div>
      <Hello />
    </div>
  </Provider>
);

render(<App />, document.getElementById("root"));

Moim zdaniem ten sposób również jest bardzo prosty w użyciu. Uwagę zwraca fakt, że tym razem nie musimy wykonywać żadnych zmian w IoC, a jedyna rzecz jaką robimy, to przekazywanie kontenera w drzewie Reactowym i pobieranie zależności z użyciem odpowiedniego dekoratora. Jednak w trakcie pisania tego artykułu, biblioteka była we wczesnym stadium developmentu. Jej autor nawet ostrzega nas, że jej API może zmienić się w każdym momencie, bez ostrzeżenia. Kolejną wadą jest to, że dostajemy jedynie najprostszy scenariusz wstrzykiwania zależności. Jest to oczywiście najpopularniejszy przypadek, jednak nie zawsze jest on wystarczający. W moim przypadku, w niektórych projektach potrzebowałem wstrzykiwania wielu klas zdefiniowanych pod jednym identyfikatorem, czego nie mogłem uzyskać korzystając z tej biblioteki.

Cały przykład można znaleźć tutaj.

Wykorzystanie react-inversify

Mamy tu do czynienia z podobną nazwą, ale z zupełnie innym podejściem. W tej bibliotece otrzymujemy dwie rzeczy: komponent Provider (zbliżony do tego z inversify-react) oraz higher-order component connect, który wstrzykuje zależności do propsów.

Użycie jest zupełnie inne niż w poprzednich sposobach. Nie dostajemy tutaj żadnych dekoratorów do wykorzystania wewnątrz komponentów. Zamiast tego tworzymy klasę, gdzie wstrzykujemy zależności w zwykły sposób, korzystając z inject. Następnie, wykorzystując HOC connect, używamy instancję tej klasy, w celu dostarczenia zależności do propsów naszego komponentu. Użycie zostało pokazane w Listingu 4.

import * as React from "react";
import { inject, injectable } from "inversify";
import { connect } from "react-inversify";
import { IProvider } from "./providers";

type Props = {
  nameProvider: IProvider<string>;
};

@injectable()
class Dependencies {
  constructor(
    @inject("nameProvider") public readonly nameProvider: IProvider<string>
  ) {}
}

@connect(
  Dependencies,
  deps => ({
    nameProvider: deps.nameProvider
  })
)
export class Hello extends React.Component<Props> {
  render() {
    return <h1>Hello {this.props.nameProvider.provide()}!</h1>;
  }
}
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-inversify";
import { Hello } from "./Hello";
import { container } from "./ioc";

const App = () => (
  <Provider container={container}>
    <div>
      <Hello />
    </div>
  </Provider>
);

render(<App />, document.getElementById("root"));

Jak widać, użycie jest zdecydowanie bardziej skomplikowane. Musimy tworzyć klasę i opakować nasz komponent w HOC. W pokazanym przykładzie użyłem HOC jako dekoratora, jednak można go też użyć tradycyjnie jako funkcję. Generalnie, sposób ten zdaje się być najbardziej „Reactowy” z racji, że przekazujemy obiekty przez propsy, zamiast inicjalizować cokolwiek wewnątrz komponentu. Inną zaletą jest to, że wykorzystujemy wprost funkcjonalności InversifyJS, więc nie jesteśmy ograniczani możliwościami biblioteki. Jednak wszystko to wiąże się z tym, że ostatecznie musimy napisać dużo więcej kodu.

Cały przykład można znaleźć tutaj.

Podsumowując

W artykule przedstawiłem trzy sposoby na użycie InversifyJS w projektach reactowych. Każdy z nich ma swoje wady i zalety, więc nie mogę jasno polecić konkretnego. Liczę, że porównanie to pomoże Ci w wyborze biblioteki najlepiej pasującej do Twojego projektu.


Artykuł został pierwotnie opublikowany na synergycodes.com.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/wstrzykiwanie-zaleznosci-react-przy-uzyciu-inversifyjs/" order_type="social" width="100%" count_of_comments="8" ]