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

  • 16
    いいね
  • 0
    コメント

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!