Relay チュートリアル【日本語翻訳】

先日、Facebookがデータ駆動型のReactアプリケーションの開発を行うためのJavaScriptフレームワーク「Relay」のTechnical Preview版を公開しました。さっそくですが、自分の理解を深めるためにRelayのチュートリアルを和訳しました。せっかくなのでブログにもアップしておきます。誤訳などもあるかと思いますが、Google翻訳よりはマシだと参考にしていただければと思います。原文もつけておいたので、翻訳がおかしなところもなんとなくニュアンスを掴んでいただければと思います。

relay.js relay Relay

チュートリアルに行く前に、Relayの基礎知識

Relay

Relayは、「データ駆動型のReactアプリケーションを開発するためのJavaScriptのフレームワーク」です。RelayはReact同様、Facebookが開発を進めています。Relayを使うと、サーバから取得したデータをReactのコンポーネントに「props」を使って渡すことが簡単にできるようになります

Relayの特徴を表すキーワードとして、公式サイトでは「DECLARATIVE(宣言的)」「COLOCATION(コロケーション)」「MUTATIONS(更新)」の3つを挙げています。

GraphQL

Relayでは、GraphQLというこれまたFacebookが開発を進めているクエリ言語が使われています。GraphQLを使うことで、クライアントとサーバー間のデータのやり取りを容易に記述できるようになっています。

Relyをはじめるには

Relayアプリケーションを開発するには以下の3つが必要となります。

  1. A GraphQL Schema
  2. A GraphQL Server
  3. Relay

これらの3つがどのように作用するかはチュートリアルを一通りこなすとわかるようになっています。

チュートリアル

以下、チュートリアルの日本語訳となります。

Tutorial – チュートリアル(日本語翻訳)

In this tutorial, we will build a game using GraphQL mutations. The object of the game will be to guess where in a grid of 9 squares is hidden some treasure. We will give players three tries to find the treasure. This should give us an end-to-end look at Relay – from the GraphQL schema on the server, to the React application on the client.

このチュートリアルでは、GraphQLのmutationを使ってゲームを作っていきます。ゲームの目的は、9つの四角いグリッドの中にいくつかの宝がどこに隠されているか推測することです。プレイヤーには宝を探すためのトライを3回与えます。このゲームを通してRelayを端から端まで(サーバー側のGraphQLスキーマから、クライアント側のReactアプリケーションまで)見ていくことになります。

Warm up – ウォームアップ

Let’s start a project using the Relay Starter Kit as a base.

Relay Starter Kitをベースとして使い、プロジェクトを開始しましょう。

$ git clone git@github.com:facebook/relay-starter-kit.git relay-treasurehunt
$ cd relay-treasurehunt
$ npm install
CLI

A simple database – シンプルなデータベース

We need a place to hide our treasure, a way to check hiding spots for treasure, and a way to track our turns remaining. For the purposes of this tutorial, we’ll hide these data in memory.

「宝を隠すための場所」、「宝を隠しているスポットをチェックする方法」、「残りのターンを追跡する方法」が必要となります。このチュートリアルの目的として、これらのデータをメモリに隠していくことになります。

/**
 * ./data/database.js
 */

// Model types
class Game extends Object {}
class HidingSpot extends Object {}

// Mock data
var game = new Game();
game.id = '1';

var hidingSpots = [];
(function() {
  var hidingSpot;
  var indexOfSpotWithTreasure = Math.floor(Math.random() * 9);
  for (var i = 0; i < 9; i++) {
    hidingSpot = new HidingSpot();
    hidingSpot.id = `${i}`;
    hidingSpot.hasTreasure = (i === indexOfSpotWithTreasure);
    hidingSpot.hasBeenChecked = false;
    hidingSpots.push(hidingSpot);
  }
})();

var turnsRemaining = 3;

export function checkHidingSpotForTreasure(id) {
  if (hidingSpots.some(hs => hs.hasTreasure && hs.hasBeenChecked)) {
    return;
  }
  turnsRemaining--;
  var hidingSpot = getHidingSpot(id);
  hidingSpot.hasBeenChecked = true;
};
export function getHidingSpot(id) {
  return hidingSpots.find(hs => hs.id === id)
}
export function getGame() { return game; }
export function getHidingSpots() { return hidingSpots; }
export function getTurnsRemaining() { return turnsRemaining; }
./data/database.js

What we have written here is a mock database interface. We can imagine hooking this up to a real database, but for now let’s move on.

ここで書いたものは疑似的(mock)なデータベースのインターフェースです。現実のデータベースと接続することも想像できるでしょうが、今は先に進みましょう。

Authoring a schema – スキーマの作成

A GraphQL schema describes your data model, and provides a GraphQL server with an associated set of resolve methods that know how to fetch data. We will use graphql-js and graphql-relay-js to build our schema.

GaraphQLスキーマはデータモデルを記述し、どのようにデータを取ってくるかを知っているresolveメソッドの関連セットを持ったGraphQLサーバーを提供します。スキーマをビルドするためにgraphql-jsgraphql-relay-jsを使います。

Let’s open up the starter kit’s schema, and replace the database imports with the ones we just created:

Starter Kitのスキーマを開きましょう。そしてデータベースインポートの部分を元々書いてあったものと置き換えましょう。

/**
 * ./data/schema.js
 */

/* ... */

import {
  Game,
  HidingSpot,
  checkHidingSpotForTreasure,
  getGame,
  getHidingSpot,
  getHidingSpots,
  getTurnsRemaining,
} from './database';
./data/schema.js

At this point, you can delete everything up until queryType in ./data/schema.js.

この時点で、./data/schema.js内のqueryTypeの直前まで全て削除できます。

Next, let’s define a node interface and type. We need only provide a way for Relay to map from an object to the GraphQL type associated with that object, and from a global ID to the object it points to:

次に、nodeのインターフェースとタイプを定義しましょう。Relayのために「オブジェクトからそのオブジェクトと関連付けられたGraphQLのタイプまで」、そして「グローバルIDからそれが指すオブジェクトまで」を方向づける方法を提供さえすればよいです。

var {nodeInterface, nodeField} = nodeDefinitions(
  (globalId) => {
    var {type, id} = fromGlobalId(globalId);
    if (type === 'Game') {
      return getGame(id);
    } else if (type === 'HidingSpot') {
      return getHidingSpot(id);
    } else {
      return null;
    }
  },
  (obj) => {
    if (obj instanceof Game) {
      return gameType;
    } else if (obj instanceof HidingSpot)  {
      return hidingSpotType;
    } else {
      return null;
    }
  }
);
./data/schema.js

Next, let’s define our game and hiding spot types, and the fields that are available on each.

次に、「gameのタイプ(gameType)」と「hiding spotのタイプ(hidingSpotType)」、「相互に利用可能なフィールド」を定義しましょう。

var gameType = new GraphQLObjectType({
  name: 'Game',
  description: 'A treasure search game',
  fields: () => ({
    id: globalIdField('Game'),
    hidingSpots: {
      type: hidingSpotConnection,
      description: 'Places where treasure might be hidden',
      args: connectionArgs,
      resolve: (game, args) => connectionFromArray(getHidingSpots(), args),
    },
    turnsRemaining: {
      type: GraphQLInt,
      description: 'The number of turns a player has left to find the treasure',
      resolve: () => getTurnsRemaining(),
    },
  }),
  interfaces: [nodeInterface],
});

var hidingSpotType = new GraphQLObjectType({
  name: 'HidingSpot',
  description: 'A place where you might find treasure',
  fields: () => ({
    id: globalIdField('HidingSpot'),
    hasBeenChecked: {
      type: GraphQLBoolean,
      description: 'True this spot has already been checked for treasure',
      resolve: (hidingSpot) => hidingSpot.hasBeenChecked,
    },
    hasTreasure: {
      type: GraphQLBoolean,
      description: 'True if this hiding spot holds treasure',
      resolve: (hidingSpot) => {
        if (hidingSpot.hasBeenChecked) {
          return hidingSpot.hasTreasure;
        } else {
          return null;  // Shh... it's a secret!
        }
      },
    },
  }),
  interfaces: [nodeInterface],
});
./data/schema.js

Since one game can have many hiding spots, we need to create a connection that we can use to link them together.

ひとつのゲームは多くのhiding spotを持つことができるので、それらに同時にリンクするために使用できるコネクションを作る必要があります。

var {connectionType: hidingSpotConnection} =
  connectionDefinitions({name: 'HidingSpot', nodeType: hidingSpotType});
./data/schema.js

Now let’s associate these types with the root query type.

さてこれらのタイプにrootクエリータイプを関連付けましょう。

var queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    node: nodeField,
    game: {
      type: gameType,
      resolve: () => getGame(),
    },
  }),
});
./data/schema.js

With the queries out of the way, let’s start in on our only mutation: the one that spends a turn by checking a spot for treasure. Here, we define the input to the mutation (the id of a spot to check for treasure) and a list of all of the possible fields that the client might want updates about after the mutation has taken place. Finally, we implement a method that performs the underlying mutation.

クエリーがなくなったことにより、mutation(それは宝のための場所をチェックすることによってターンを消費します)にのみ取り掛かることができます。ここで、mutation(宝をチェックするためのスポットのid)ヘのinputと、mutationが起こった後にクライアント側がアップデートする可能性のあるフィールドのすべてのリストを定義します。最後に、下にあるmutationを動かすメソッドを実装します。

var CheckHidingSpotForTreasureMutation = mutationWithClientMutationId({
  name: 'CheckHidingSpotForTreasure',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
  },
  outputFields: {
    hidingSpot: {
      type: hidingSpotType,
      resolve: ({localHidingSpotId}) => getHidingSpot(localHidingSpotId),
    },
    game: {
      type: gameType,
      resolve: () => getGame(),
    },
  },
  mutateAndGetPayload: ({id, text}) => {
    var localHidingSpotId = fromGlobalId(id).id;
    checkHidingSpotForTreasure(localHidingSpotId);
    return {localHidingSpotId};
  },
});
./data/schema.js

Let’s associate the mutation we just created with the root mutation type:

ちょうど作ったmutationをルートmutation typeと関連付けましょう。

var mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: () => ({
    checkHidingSpotForTreasure: CheckHidingSpotForTreasureMutation,
  }),
});
./data/schema.js

Finally, we construct our schema (whose starting query type is the query type we defined above) and export it.

最後に、スキーマ(そのスターティングクエリータイプは上で定義したクエリータイプです)を作り、エクスポートします。

export var Schema = new GraphQLSchema({
  query: queryType,
  mutation: mutationType
});
./data/schema.js

Processing the schema – スキーマを加工する

Before going any further, we need to serialize our executable schema to JSON for use by the Relay.QL transpiler, then start up the server. From the command line:

先に進む前に、実行可能なスキーマをRelay.QLトランスパイラーによって使われるJSONにシリアライズし、コマンドラインからサーバーを起動する必要があります。

./scripts/updateSchema.js
$ npm run update-schema
CLI

※本家チュートリアルでは「npm start」となっていましたが、schema.jsonを更新するコマンドは「npm run update-schema」なので、変更しておきました。

Writing the game – ゲームを書く

Let’s tweak the file ./routes/AppHomeRoute.js to anchor our game to the game root field of the schema:

スキーマのgameのrootフィールドにゲームを固定するために、./routes/AppHomeRoute.jsファイルを調整していきましょう。

export default class extends Relay.Route {
  static path = '/';
  static queries = {
    game: (Component) => Relay.QL`
      query {
        game {
          ${Component.getFragment('game')},
        },
      }
    `,
  };
  static routeName = 'AppHomeRoute';
}
./routes/AppHomeRoute.js

Next, let’s create a file in ./mutations/CheckHidingSpotForTreasureMutation.js and create subclass of Relay.Mutation called CheckHidingSpotForTreasureMutation to hold our mutation implementation:

次に./js/mutations/CheckHidingSpotForTreasureMutation.jsファイルを作り、mutationの実装を保持するためのRelay.MutationのサブクラスCheckHidingSpotForTreasureMutationを作りましょう。

export default class CheckHidingSpotForTreasureMutation extends Relay.Mutation {
  static fragments = {
    game: () => Relay.QL`
      fragment on Game {
        id,
        turnsRemaining,
      }
    `,
    hidingSpot: () => Relay.QL`
      fragment on HidingSpot {
        id,
      }
    `,
  };
  getMutation() {
    return Relay.QL`mutation{checkHidingSpotForTreasure}`;
  }
  getCollisionKey() {
    return `check_${this.props.game.id}`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on CheckHidingSpotForTreasurePayload {
        hidingSpot {
          hasBeenChecked,
          hasTreasure,
        },
        game {
          turnsRemaining,
        },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        hidingSpot: this.props.hidingSpot.id,
        game: this.props.game.id,
      },
    }];
  }
  getVariables() {
    return {
      id: this.props.hidingSpot.id,
    };
  }
  getOptimisticResponse() {
    return {
      game: {
        turnsRemaining: this.props.game.turnsRemaining - 1,
      },
      hidingSpot: {
        id: this.props.hidingSpot.id,
        hasBeenChecked: true,
      },
    };
  }
}
./js/mutations/CheckHidingSpotForTreasureMutation.js

Finally, let’s tie it all together in ./components/App.js:

最後に、それを./components/App.jsにおいて同時にすべてを結びつけましょう。

import CheckHidingSpotForTreasureMutation from '../mutations/CheckHidingSpotForTreasureMutation';

class App extends React.Component {
  _getHidingSpotStyle(hidingSpot) {
    var color;
    if (this.props.relay.hasOptimisticUpdate(hidingSpot)) {
      color = 'lightGrey';
    } else if (hidingSpot.hasBeenChecked) {
      if (hidingSpot.hasTreasure) {
        color = 'green';
      } else {
        color = 'red';
      }
    } else {
      color = 'black';
    }
    return {
      backgroundColor: color,
      cursor: this._isGameOver() ? null : 'pointer',
      display: 'inline-block',
      height: 100,
      marginRight: 10,
      width: 100,
    };
  }
  _handleHidingSpotClick(hidingSpot) {
    if (this._isGameOver()) {
      return;
    }
    Relay.Store.update(
      new CheckHidingSpotForTreasureMutation({
        game: this.props.game,
        hidingSpot,
      })
    );
  }
  _hasFoundTreasure() {
    return (
      this.props.game.hidingSpots.edges.some(edge => edge.node.hasTreasure)
    );
  }
  _isGameOver() {
    return !this.props.game.turnsRemaining || this._hasFoundTreasure();
  }
  renderGameBoard() {
    return this.props.game.hidingSpots.edges.map(edge => {
      return (
        <div
          onClick={this._handleHidingSpotClick.bind(this, edge.node)}
          style={this._getHidingSpotStyle(edge.node)}
        />
      );
    });
  }
  render() {
    var headerText;
    if (this.props.relay.getPendingTransactions(this.props.game)) {
      headerText = '\u2026';
    } else if (this._hasFoundTreasure()) {
      headerText = 'You win!';
    } else if (this._isGameOver()) {
      headerText = 'Game over!';
    } else {
      headerText = 'Find the treasure!';
    }
    return (
      <div>
        <h1>{headerText}</h1>
        {this.renderGameBoard()}
        <p>Turns remaining: {this.props.game.turnsRemaining}</p>
      </div>
    );
  }
}

export default Relay.createContainer(App, {
  fragments: {
    game: () => Relay.QL`
      fragment on Game {
        turnsRemaining,
        hidingSpots(first: 9) {
          edges {
            node {
              hasBeenChecked,
              hasTreasure,
              id,
              ${CheckHidingSpotForTreasureMutation.getFragment('hidingSpot')},
            }
          }
        },
        ${CheckHidingSpotForTreasureMutation.getFragment('game')},
      }
    `,
  },
});
./js/components/App.js

A working copy of the treasure hunt can be found in the ./examples/ directory.

トレジャーハントの動くコピーが./examples/ディレクトリ内で見つかります。

Now that we’ve gone end-to-end with Relay, let’s dive into more detail in the guides section.

Relayの端から端まで見てきました。より詳細を知るためにガイドセクションに飛び込みましょう。


チュートリアルは以上となります。

サンプルを実行して確認する

チュートリアルで作ったTreasurehuntサンプルを実行して確認するには、ターミナルから以下のコマンドを実行します。ローカルサーバーが立ち上がります。

$ npm start

> relay-starter-kit@0.1.0 start /Users/xxxx/Sites/Project/relay-treasurehunt
> babel-node ./server.js

GraphQL Server is now running on http://localhost:8080
App is now running on http://localhost:3000
Hash: 003559e1d020c34bde73
Version: webpack 1.11.0
Time: 3993ms
 Asset   Size  Chunks             Chunk Names
app.js  19 kB       0  [emitted]  main
chunk    {0} app.js (main) 17.1 kB [rendered]
    [0] ./js/app.js 543 bytes {0} [built]
    [1] ./js/components/App.js 7.34 kB {0} [built]
    [2] ./js/mutations/CheckHidingSpotForTreasureMutation.js 6.22 kB {0} [built]
    [3] ./js/routes/AppHomeRoute.js 2.99 kB {0} [built]
CLI

ブラウザで「http://localhost:3000」にアクセスします。問題なければ以下のような画面が表示されます。

relay-treasure-hunt
Treasurehunt

チュートリアルで作ったtreasurehuntはGitHub上にアップされているので、うまく動かなかった時などどこが違うのかとか参考にするとよいと思います。


自分の場合チュートリアルを翻訳してみて、実際にチュートリアルも試してみましたが、まだまだ全然理解が進んでいません。これからGraphQLも含めてRelayを勉強していく予定です。さっそく以下のようなRelayに関して詳しく解説してくれている記事も出始めてきているので最後に紹介しておきます。

入門 React ―コンポーネントベースのWebフロントエンド開発
  • 『入門 React ―コンポーネントベースのWebフロントエンド開発』
  • 著者: Frankie Bagnardi, Jonathan Beebe, Richard Feldman, Tom Hallett, Simon HØjberg, Karl Mikkelsen, 宮崎 空(翻訳)
  • 出版社: オライリージャパン
  • 発売日: 2015年4月3日
  • ISBN: 978-4873117195

コメント一覧

  • 必須

コメント