在本教程中,我们将使用GraphQL mutations 来构建游戏。游戏的目标是在9个方格的网格中找到隐藏的宝藏。我们将会给玩家们三次尝试去找宝藏。 这应该可以让我们看到 Relay 完整的工作流 – 从服务器上的 GraphQL schema,到客户端上的 React 应用程序。
我们开始一个项目,使用 Relay 入门工具 作为基础。
git clone https://github.com/relayjs/relay-starter-kit.git relay-treasurehunt cd relay-treasurehunt npm install
我们需要一个隐藏我们的宝藏的地方,一种检查隐藏点的宝藏的方法,以及跟踪我们的转弯的方法。为了本教程的目的,我们将这些数据隐藏在内存中。
/** * ./data/database.js */ // Model types export class Game {} export class HidingSpot {} // Mock data const game = new Game(); game.id = '1'; const hidingSpots = []; (function() { let hidingSpot; const indexOfSpotWithTreasure = Math.floor(Math.random() * 9); for (let i = 0; i < 9; i++) { hidingSpot = new HidingSpot(); hidingSpot.id = `${i}`; hidingSpot.hasTreasure = (i === indexOfSpotWithTreasure); hidingSpot.hasBeenChecked = false; hidingSpots.push(hidingSpot); } })(); let turnsRemaining = 3; export function checkHidingSpotForTreasure(id) { if (hidingSpots.some(hs => hs.hasTreasure && hs.hasBeenChecked)) { return; } turnsRemaining--; const 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; }
我们在这里写的是一个模拟数据库界面。我们可以想象连接到一个真正的数据库,但现在让我们继续前进。
GraphQL模式描述了您的数据模型,并为GraphQL服务器提供了一组知道如何获取数据的关联方法。我们将使用 graphql-js 和 graphql-relay-js 来构建我们的 schema.
我们来打开入门工具,并用刚才创建的数据库导入替换数据库:
/** * ./data/schema.js */ /* ... */ import { Game, HidingSpot, checkHidingSpotForTreasure, getGame, getHidingSpot, getHidingSpots, getTurnsRemaining, } from './database';
在此刻,你可以刪除 ./data/schema.js
中直到 queryType
之前的代码。
接下来,我们来定义一个节点接口和类型。我们只需要为Relay提供一种从对象映射到与该对象关联的GraphQL类型的方式,并从全局ID到其指向的对象:
const {nodeInterface, nodeField} = nodeDefinitions( (globalId) => { const {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; } } );
接下来,我们来定义我们的游戏和隐藏点类型,以及每个可用的字段。
const 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], }); const hidingSpotType = new GraphQLObjectType({ name: 'HidingSpot', description: 'A place where you might find treasure', fields: () => ({ id: globalIdField('HidingSpot'), hasBeenChecked: { type: GraphQLBoolean, description: 'True if 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], });
由于一个游戏可以有许多隐藏点,我们需要创建一个可以用来将它们链接在一起的连接。
const {connectionType: hidingSpotConnection} = connectionDefinitions({name: 'HidingSpot', nodeType: hidingSpotType});
现在让我们将这些类型与根查询类型相关联。
const queryType = new GraphQLObjectType({ name: 'Query', fields: () => ({ node: nodeField, game: { type: gameType, resolve: () => getGame(), }, }), });
随着 queries 已经完成,让我们开始着手我们唯一的 mutation:消耗一个回合来检查一个 spot 有没有宝藏的那一个。在这里,我们定义给 mutation 的 input (spot 的 id 用来检查宝藏) 和在 mutation 发生之后所有客户端可能会可能想要更新的 fields 清单。最后,我们实现一个方法来执行背后的 mutation。
const 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}) => { const localHidingSpotId = fromGlobalId(id).id; checkHidingSpotForTreasure(localHidingSpotId); return {localHidingSpotId}; }, });
我们将我们刚刚创建的 mutation 与根 mutation 类型相关联:
const mutationType = new GraphQLObjectType({ name: 'Mutation', fields: () => ({ checkHidingSpotForTreasure: CheckHidingSpotForTreasureMutation, }), });
最后,我们构造我们的 schema (其起始查询类型是我们上面定义的查询类型) 并将其导出。
export const Schema = new GraphQLSchema({ query: queryType, mutation: mutationType });
在进一步之前,我们需要将我们的可执行模式序列化为JSON,以供Relay.QL transpiler使用,然后启动服务器。执行命令:
npm run update-schema
npm start
让我们修改文件 ./js/routes/AppHomeRoute.js
以把我们的 game 绑到 schema 的 game root field::
export default class extends Relay.Route { static queries = { game: () => Relay.QL`query { game }`, }; static routeName = 'AppHomeRoute'; }
接下来,让我们创建一个文件 ./js/mutations/CheckHidingSpotForTreasureMutation.js
并创建 Relay.Mutation
被调用的子类
CheckHidingSpotForTreasureMutation
来保存我们的 mutation 实现:
import Relay from 'react-relay'; 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 @relay(pattern: true) { 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/components/App.js
:
import CheckHidingSpotForTreasureMutation from '../mutations/CheckHidingSpotForTreasureMutation'; import React from 'react'; import Relay from 'react-relay'; class App extends React.Component { _getHidingSpotStyle(hidingSpot) { let color; if (this.props.relay.hasOptimisticUpdate(hidingSpot)) { color = 'lightGrey'; } else if (hidingSpot.hasBeenChecked) { if (hidingSpot.hasTreasure) { color = 'blue'; } 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; } this.props.relay.commitUpdate( 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 key={edge.node.id} onClick={this._handleHidingSpotClick.bind(this, edge.node)} style={this._getHidingSpotStyle(edge.node)} /> ); }); } render() { let 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')}, } `, }, });
treasure hunt 副本可以在 relay 示例 资源库中找到。
现在我们已经完成了本教程,我们来看看构建GraphQL客户端框架的意义,以及与传统REST系统的客户端进行比较。