LoginSignup
42
24

More than 5 years have passed since last update.

React でも Angular みたいに DI したい!

Last updated at Posted at 2017-02-07
1 / 26

When

2017/02/07

At

Meguro.es #8 @ アカツキ


自己紹介

ちきさん
(Tomohiro Noguchi)

Twitter/GitHub/Qiita: @ovrmrw

ただのSIer。

Angular Japan User Group (ng-japan)スタッフ。

3a2512bb-aa72-4515-af42-1f1721252f39.jpg


アカウントIDの由来

  1. the day after tomorrow
  2. overmorrow(俗語)
  3. 略して
  4. ovrmrw
  5. 「先を見据えて」よりさらに先を、みたいな感じです。

(よく聞かれるので:innocent:)


(ここからInversifyJSの基本の話)

===

GitHubリポジトリ ovrmrw/inversify-mock-example


モチベーション: ReactだってDIしたい:tired_face:


InversifyJS

  • a library for Inversion of Control
  • Angularのように @injectable とか @inject とか書いてDependency Injection(依存性注入)できます。

C19WVJ9XAAAiv8n.jpg


事前準備 (TypeScriptの場合)

tsconfig.jsonでデコレーターを有効にします。

tsconfig.json
{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

なるべく最初の方でreflect-metadataを読み込みます。(polyfill)

index.ts
import 'reflect-metadata'

まずInjectするクラスを作ります。
DIコンテナの中でインスタンス化させるクラスは@injectableデコレーターが必須です。

services.ts
import { injectable } from 'inversify'

@injectable()   // ★
export class Katana {
  hit() { 
    return 'cut!' 
  }
}

@injectable()   // ★
export class Shuriken {
  throw() { 
    return 'hit!' 
  }
}

DIコンテナの定義を書きます。慣例としてファイル名はinversify.config.tsとします。

inversify.config.ts
import { Container } from 'inversify'
import { Katana, Shuriken } from './services'

const rootContainer = new Container()
rootContainer.bind(Katana).toSelf()    // ★
rootContainer.bind(Shuriken).toSelf()  // ★

export const container = rootContainer.createChild()

補足説明

container.bind(Katana).toSelf()

意味: Katanaトークンに対してKatanaクラスのインスタンスをInjectする。

===

モックする場合は?

container.bind(Katana).to(MockKatana)

意味: Katanaトークンに対してMockKatanaクラスのインスタンスをInjectする。


NinjaクラスにKatana,ShurikenクラスをInjectしてみる。

index.ts
@injectable()
class Ninja {
  constructor(
    @inject(Katana) private katana: Katana, // @inject(Katana)は省略可
    @inject(Shuriken) private shuriken: Shuriken, // @inject(Shuriken)は省略可
  ) { }

  fight() { 
    return this.katana.hit() 
  }
  sneak() { 
    return this.shuriken.throw() 
  }
}

@injectable()
class MockKatana implements Katana {
  hit() { 
    return 'cut! (mock)' 
  }
}

container.bind(Ninja).toSelf()
container.bind(Katana).to(MockKatana)  // ★

const ninja = container.get(Ninja)     // ★

console.log(ninja.fight()) // output: "cut! (mock)"
console.log(ninja.sneak()) // output: "hit!"

補足説明

  constructor(
    @inject(Katana) private katana: Katana, // @inject(Katana)は省略可
    @inject(Shuriken) private shuriken: Shuriken, // @inject(Shuriken)は省略可
  ) { }

Ninjaクラスのconstructor

  • Katanaトークンにbindされたクラスをインスタンス化して変数katanaにInjectする。
  • Shurikenトークンにbindされたクラスをインスタンス化して変数shurikenにInjectする。

ということが行われています。
そして変数katanaにはKatanaクラスのインスタンスではなく、

container.bind(Katana).to(MockKatana)

上記の一行によってMockKatanaクラスのインスタンスがInjectされます。
もしこの行が無かったら、

inversify.config.ts
rootContainer.bind(Katana).toSelf()

rootContainerの定義が採用されてKatanaクラスがInjectされます。


図で説明 (1/2)

container-01.png


図で説明 (2/2)

container-02.png

  • ChildContainerで上位のContainerの定義を上書きできる。
  • ChildContainerでbindされたクラスが見つからなかったら上位のContainerを辿って探す。

さらに補足説明

Ninjaクラスのインスタンスを取得するときにnew Ninja()のように書いてはいけません。

const ninja = container.get(Ninja)

上記のようにcontainer.get(Ninja)と書くことでDIコンテナからNinjaクラスのインスタンスを取り出します。
依存性の解決はDIコンテナの中でよしなにやってくれるというわけです。


(ここからReactの話、つまり本題)

===

GitHubリポジトリ ovrmrw/meguroes-react-inversify-typescript


まずInjectする適当なクラスを作ります。
@injectable()を付けるのがコツですね。

actions.ts
import { injectable } from 'inversify'

@injectable()   // ★
export class Actions {
  goo(): string {
    return 'goo!'
  }

  choki(): string {
    return 'choki!'
  }

  paa(): string {
    return 'paa!'
  }
}

DIコンテナの定義を書きます。

inversify.config.ts
import { Container } from 'inversify'
import getDecorators from 'inversify-inject-decorators'
import { Actions } from './actions'

const container = new Container()
container.bind(Actions).toSelf()   // ★

export const { lazyInject } = getDecorators(container)  // ★

ActionsトークンにActionsクラス自身をbindします。

inversify-inject-decoratorsというライブラリを使ってlazyInjectというデコレーターをexportしているのがポイントです。


Reactのコンポーネントを書きます。
ようやく今日一番話したかった@lazyInject(Actions) actions: Actionsが登場しました。

App.ts
import { lazyInject } from './inversify.config'
import { Actions } from './actions'

export class App extends React.Component<{}, {}> {
  @lazyInject(Actions) actions: Actions   // ★ constructorですらない

  goo(event): void {
    this.setState({ janken: this.actions.goo() })
  }

  choki(event): void {
    this.setState({ janken: this.actions.choki() })
  }

  paa(event): void {
    this.setState({ janken: this.actions.paa() })
  }

  random(event): void {
    const random = Math.random()
    if (random > 0.66) {
      this.goo(event)
    } else if (random > 0.33) {
      this.choki(event)
    } else {
      this.paa(event)
    }
  }

  render() {
    return (
      <div>
        <button onClick={(e) => this.goo(e)}>グー</button>
        <button onClick={(e) => this.choki(e)}>チョキ</button>
        <button onClick={(e) => this.paa(e)}>パー</button>
        <button onClick={(e) => this.random(e)}>ランダム</button>
        <h1>{this.state.janken}</h1>
      </div>
    )
  }
}

(改行してもしなくてもどちらでも良い)

  @lazyInject(Actions) actions: Actions
  @lazyInject(Actions) 
  actions: Actions

意味: ActionsトークンにActionsクラスのインスタンスをInjectする。ただしインスタンス生成時ではなく実行時にInjectされる。

(追記)...と思ったけどconstructorの中でthis.actionsを参照してもエラーにはならないのでインスタンス生成時にうまいことInjectしてるのかも。


Q: lazyである必要があるの?

A: 本来であればcontainer.get(App)のようにInversifyJSがインスタンス生成のタイミングを握らなければならないが、Reactが握っているため仕方なく後からInjectする必要がある。


READMEにもこのように書いてあります。

Some frameworks and libraries take control over the creation of instances of a given class. For example, React takes control over the creation of instances of a given React component. This kind of frameworks and libraries prevent us from being able to use constructor injection and as a result they are not easy to integrate with InversifyJS.

いくつかのフレームワークとライブラリは、特定のクラスのインスタンスの作成を制御します。例えば、Reactは与えられたReactコンポーネントのインスタンスの作成を制御します。この種のフレームワークやライブラリは、コンストラクタインジェクションを使用できないため、InversifyJSとの統合が容易ではありません。


ReactでもAngularのようにDIできる:raised_hands:


Angular使おう:raised_hands:


Thanks!

42
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
24