[Reach教程翻译] 2.9 通过 React 实现游戏网页交互

  • Ivan
  • 更新于 2021-12-06 14:59
  • 阅读 3053

这节教程中,我们利用 React 为石头剪刀布游戏制作一个网页前端,可以在本地测试并部署到共识网络上。 至此,我们的石头剪刀布游戏便大功告成了!

[Reach教程翻译] Reach是安全简单的Dapp开发语言 让用户可以像开发传统App一样开发DApp 目前使用Reach开发的智能合约可以部署在以太坊、Conflux、Algorand Reach官网 Reach官方文挡

2.9 通过 React 实现游戏网页交互

原文链接

在上节我们展示了不用修改 Reach 程序就让石头剪刀布可以作为命令行应用程序运行。在本节中,我们仍不需要修改 Reach 程序,而我们将前端由命令行改为 Web 界面。

本教程中使用 React.js ,但你可以使用任何语言来为你的 Reach 合约制作前端。

和之前一样,在本教程中,我们假设将使用以太坊进行部署(和测试)。Reach Web 应用程序需要浏览器支持的共识网络帐户及其关联钱包。在以太坊上,标准的钱包是 MetaMask 。如果你想测试教程里的代码,需要先安装并设置 MetaMask 。此外,MetaMask 不支持多个实例账户,所以如果你想在本地测试石头剪刀布!您需要有两个独立的浏览器实例(例如:火狐+Chrome):一个作为 Alice ,另一个作为 Bob 。

我们将之前写好的 index.rsh 文件复制到一个新目录中,然后在那里开始这份教程的工作。

我们不会用到之前的 index.mjs 文件,取而代之的,我们会新写一个 index.js 文件。

我们还需要 index.css 和一些视图。这些细节相当琐碎且不是 Reach 特有的,因此我们不会解释这些文件的细节。如果要在本地运行,你需要下载那些文件。目录应该如下所示:

| ├── index.css | | ├── index.js | | ├── index.rsh | | └── views | | ----├── AppViews.js | | ----├── AttacherViews.js | | ----├── DeployerViews.js | | ----├── PlayerViews.js | | ----└── render.js | —

我们将重点讨论 rps-9-web/index.js ,因为 rps-9-web/index.rsh 与前面的章节中完全相同。 rps-9-web/index.js

1    import React from 'react';
 2    import AppViews from './views/AppViews';
 3    import DeployerViews from './views/DeployerViews';
 4    import AttacherViews from './views/AttacherViews';
 5    import {renderDOM, renderView} from './views/render';
 6    import './index.css';
 7    import * as backend from './build/index.main.mjs';
 8    import {loadStdlib} from '@reach-sh/stdlib';
 9    const reach = loadStdlib(process.env);
..    // ...
  • 第 1 行到第 6 行,我们导入视图代码和 CSS 。
  • 第 7 行,我们导入编译好的后端 backend
  • 第8行中,我们将标准库 stdlib 导入为 reach

React 编译 Reach 标准库时没有办法直接获取选择合适标准库的环境变量,因此我们需要将 process.env 传入。

rps-9-web/index.js

..    // ...
10    
11    const handToInt = {'ROCK': 0, 'PAPER': 1, 'SCISSORS': 2};
12    const intToOutcome = ['Bob wins!', 'Draw!', 'Alice wins!'];
13    const {standardUnit} = reach;
14    const defaults = {defaultFundAmt: '10', defaultWager: '3', standardUnit};
..    // ...
  • 在这部份中,我们定义了一些对应于 Reach 合约中的常量和默认值。

2.9.1 应用程序组件

我们将应用程序主要的视图 App 定义为一个 React 组件,并告诉它挂载后要做什么,"挂载"是 React 的术语,即启动的意思。

rps-9-web/index.js

..    // ...
15    
16    class App extends React.Component {
17      constructor(props) {
18        super(props);
19        this.state = {view: 'ConnectAccount', ...defaults};
20      }
21      async componentDidMount() {
22        const acc = await reach.getDefaultAccount();
23        const balAtomic = await reach.balanceOf(acc);
24        const bal = reach.formatCurrency(balAtomic, 4);
25        this.setState({acc, bal});
26        if (await reach.canFundFromFaucet()) {
27          this.setState({view: 'FundAccount'});
28        } else {
29          this.setState({view: 'DeployerOrAttacher'});
30        }
31      }
..    // ...

rps-9-web/index.js

..    // ...
39      render() { return renderView(this, AppViews); }
40    }
41    
..    // ...
  • 在第 19 行,我们初始化组件状态以显示 ConnectAccount 对话框(2.9.2)。
  • 在第 21 行到第 31 行,我们连接到 React 的 componentDidMount 生命周期事件,该事件在组件启动时被调用。
  • 在第 22 行,我们使用 getDefaultAccount ,它会连接默认的浏览器钱包。例如,当与以太坊一起使用时,它会连接当前的 MetaMask 帐户。
  • 在第 26 行中,我们使用 canFundFromFaucet 尝试访问 Reach 开发人员测试网络水龙头。
  • 在第 27 行,如果 canFundFromFaucet 为 true,我们设置组件状态,显示 FundAccount 对话框(2.9.3)。
  • 在第 29 行,如果 canFundFromFaucet 为 false,我们设置组件状态,跳到 Choose Role 对话框(2.9.4)。
  • 在第 39 行,我们从 rps-9-web/views/AppViews.js 中渲染对应的视图。

2.9.2 Connect Account 对话框

将应用程序组件加上视图(rps-9-web/views/AppViews.js)后,看起来就像这样:

2.9.3 Fund Account 对话框

接下来,我们定义 App 上的 callback,即当用户点击按钮时该做什么。

rps-9-web/index.js

..    . // ...
32     async fundAccount(fundAmount) {
33       await reach.fundFromFaucet(this.state.acc, reach.parseCurrency(fundAmount));
34       this.setState({view: 'DeployerOrAttacher'});
35     }
36     async skipFundAccount() { this.setState({view: 'DeployerOrAttacher'}); }
..    . // ...
  • 在第 32 行到第 35 行,我们定义了当用户点击 Fund Account 时要做什么。
  • 在第 33 行,我们将资金从水龙头转到用户的帐户。
  • 在第 34 行,我们设置组件状态以显示 Choose Role 对话框(2.9.4)。
  • 在第 36 行,我们定义了当用户单击 Skip 按钮时要做的事情,即设置组件状态以显示 Choose Role 对话框(2.9.4)

加上视图(rps-9-web/views/AppViews.js)后看起来就是这样:

2.9.4 Choose Role 对话框

rps-9-web/index.js

..    // ...
37      selectAttacher() { this.setState({view: 'Wrapper', ContentView: Attacher}); }
38      selectDeployer() { this.setState({view: 'Wrapper', ContentView: Deployer}); }
..    // ...

在第 37 和 38 行中,我们根据用户点击 Deployer 还是 Attacher 来设置子组件。

当加上视图(rps-9-web/views/AppViews.js)后,看起来是这样的:

2.9.5 Player 组件

接下来,我们定义 React 组件 Player, Alice 和 Bob 将会在此基础上扩展。

我们的前端需要实现这些 Reach 后端中定义的参予者交互接口:

rps-9-web/index.rsh

..    // ...
20    const Player = {
21      ...hasRandom,
22      getHand: Fun([], UInt),
23      seeOutcome: Fun([UInt], Null),
24      informTimeout: Fun([], Null),
25    };
..    // ...

我们直接通过 React 组件提供这些 callback

rps-9-web/index.js

..    // ...
42    class Player extends React.Component {
43      random() { return reach.hasRandom.random(); }
44      async getHand() { // Fun([], UInt)
45        const hand = await new Promise(resolveHandP => {
46          this.setState({view: 'GetHand', playable: true, resolveHandP});
47        });
48        this.setState({view: 'WaitingForResults', hand});
49        return handToInt[hand];
50      }
51      seeOutcome(i) { this.setState({view: 'Done', outcome: intToOutcome[i]}); }
52      informTimeout() { this.setState({view: 'Timeout'}); }
53      playHand(hand) { this.state.resolveHandP(hand); }
54    }
55    
..    // ...
  • 在第 43 行,我们提供 random 回调函数
  • 在第 44 至 50 行,我们提供 `getHand``` 回调函数。
  • 在第 45 行到第 47 行,我们设置组件状态,显示 Get Hand 对话框(2.9.6),并等待用户交互 resolve 这个 Promise 。
  • 在 Promise 被 resolve 之后的第 48 行中,我们设置组件状态,显示 Waiting For Results 对话框(2.9.7)。
  • 在第 51 行和第 52 行中,我们提供了seeOutcomeinformTimeout 回调,它们设置组件状态来分别显示 Done 视图(2.9.8)和 Timeout 视图(2.9.9)。
  • 在第 53 行,我们定义了当用户点击石头、剪刀、布时会发生什么:resolve 第 45 行的 Promise 。

2.9.6 Get Hand 对话框

这个对话框(rps-9-web/views/PlayerViews.js)用来让玩家出拳,他看起来是这样的:

2.9.7 Waiting for results 对话框

这个对话框(rps-9-web/views/PlayerViews.js)看起来是这样:

2.9.8 Done 对话框

游戏结束时我们展示这个对话框(rps-9-web/views/PlayerViews.js)

2.9.9 Timeout 对话框

当有玩家观察到超时时,我们展示(rps-9-web/views/PlayerViews.js),看起来如下:

2.9.10 Deployer 组件

接下来考虑Alice,我们替她定义一个名为 Deployer 的 React 组件,它扩展了 Player

同样的,前端需要实现后端定义的这些参与者交互接口:

rps-9-web/index.rsh

..    // ...
28      const Alice = Participant('Alice', {
29        ...Player,
30        wager: UInt, // atomic units of currency
31        deadline: UInt, // time delta (blocks/rounds)
32      });
..    // ...

我们要提供赌注值 wager ,并定义部署合约的按钮。

rps-9-web/index.js

..    // ...
56    class Deployer extends Player {
57      constructor(props) {
58        super(props);
59        this.state = {view: 'SetWager'};
60      }
61      setWager(wager) { this.setState({view: 'Deploy', wager}); }
62      async deploy() {
63        const ctc = this.props.acc.deploy(backend);
64        this.setState({view: 'Deploying', ctc});
65        this.wager = reach.parseCurrency(this.state.wager); // UInt
66        this.deadline = {ETH: 10, ALGO: 100, CFX: 1000}[reach.connector]; // UInt
67        backend.Alice(ctc, this);
68        const ctcInfoStr = JSON.stringify(await ctc.getInfo(), null, 2);
69        this.setState({view: 'WaitingForAttacher', ctcInfoStr});
70      }
71      render() { return renderView(this, DeployerViews); }
72    }
..    // ...
  • 第 59 行,我们设置组件状态,显示 Set Wager 对话框(2.9.11)。
  • 在第 61 行,我们定义了当用户单击 Set Wager 按钮时要做的事情,即设置组件状态以显示 Deploy 对话框(2.9.12)。
  • 在第 62 至 69 行中,我们定义了当用户单击 Deploy 按钮时要做什么。
  • 在第 63 行中,我们调用 acc.deploy 部署合约。
  • 在第 64 行,我们设置组件状态,显示 Deploying 对话框(2.9.13)。
  • 在第 65 行,我们设置了赌注属性 wager
  • 在第 66 行,我们根据连接的网络设置时限属性 deadline
  • 在第 67 行,我们开始作为 Alice 运行 Reach 程序,使用 React 组件 this 作为参与者交互接口对象。
  • 在第 68 - 69 行,我们设置组件状态,显示 Waiting For Attacher 对话框(2.9.14),它将部署的合约信息显示为 JSON 。
  • 在第 71 行中,我们从 rps-9-web/views/DeployerViews.js 中呈现对应的视图。

2.9.11 Set Wager 对话框

这个对话框(rps-9-web/views/DeployerViews.js)让玩家设置赌注:

2.9.12 Deploy 对话框

这个对话框(rps-9-web/views/DeployerViews.js)用于让玩家部署合约:

2.9.13 Deploying 对话框

在部署过程中,我们展示这个对话框(rps-9-web/views/DeployerViews.js):

2.9.14 Waiting for Attacher 对话框

玩家部署后等待另一位玩家加入时,我们 呈现这个视图((rps-9-web/views/DeployerViews.js)):

2.9.15 Attacher 组件

类似的,对于 Bob,前端需要实现后端定义的这些参与者交互接口:

rps-9-web/index.rsh

..    // ...
33      const Bob   = Participant('Bob', {
34        ...Player,
35        acceptWager: Fun([UInt], Null),
36      });
..    // ...

我们会定义 acceptWager 回调函数,并定义加入另一个玩家部署的合约的按钮。

rps-9-web/index.js

..    // ...
73    class Attacher extends Player {
74      constructor(props) {
75        super(props);
76        this.state = {view: 'Attach'};
77      }
78      attach(ctcInfoStr) {
79        const ctc = this.props.acc.attach(backend, JSON.parse(ctcInfoStr));
80        this.setState({view: 'Attaching'});
81        backend.Bob(ctc, this);
82      }
83      async acceptWager(wagerAtomic) { // Fun([UInt], Null)
84        const wager = reach.formatCurrency(wagerAtomic, 4);
85        return await new Promise(resolveAcceptedP => {
86          this.setState({view: 'AcceptTerms', wager, resolveAcceptedP});
87        });
88      }
89      termsAccepted() {
90        this.state.resolveAcceptedP();
91        this.setState({view: 'WaitingForTurn'});
92      }
93      render() { return renderView(this, AttacherViews); }
94    }
95    
..    // ...
  • 在第 76 行,我们初始化组件状态,显示 Attach 对话框(2.9.16)。
  • 在第 78 至 82 行,我们定义了当用户点击 Attach 按钮时会发生什么。
  • 在第 79 行,我们调用 acc.attach
  • 在第 80 行,我们设置组件状态,显示Attachign 视图(图13)。
  • 在第 81 行,我们开始以 Bob 的身份运行 Reach 程序,使用 React 组件 this 作为参与者交互接口对象。
  • 在第 83 行到第 88 行,我们定义了 acceptWager 回调函数。
  • 在第 85 行到第 87 行,我们将组件状态设置为显示 Accept Terms 对话框(2.9.18),并等待用户交互 resolve 这个 Promise 。
  • 在第 89 行到第 92 行,我们定义了当用户点击 Accept Terms and Pay Wager 按钮时发生的事情:resolve 第 90 行的 Promise ,设置组件状态以显示 Waiting For Turn 对话框(2.9.19)。
  • 在第 93 行,我们从 rps-9-web/views/AttacherViews.js 中呈现对应的视图

2.9.16 Attach 对话框

这个对话框(rps-9-web/views/AttacherViews.js)让玩家加入部署好的合约:

2.9.17 Attaching 对话框

在加入过程中,我们展示这个对话框(rps-9-web/views/AttacherViews.js),看起来是这样的:

2.9.18 Accept Terms 对话框

这个对话框(rps-9-web/views/AttacherViews.js)让玩家同意赌注:

2.9.19 Waiting for Turn 对话框

在玩家等待对方出拳的时候,我们展示这个对话框(rps-9-web/views/AttacherViews.js):

2.9.20 大功告成

rps-9-web/index.js

..    // ...
96    renderDOM(<App />);

最后,我们调用 rps-9-web/views/render.js中的函数帮助呈现我们渲染 App 组件。

如果要使用 React 开发服务器在本地测试,只需要运行:

$ ./reach react

之后,便可以通过两个浏览器实例访问 http://localhost:3000/ 进行本地测试。

若要部署在 Algorand 或 Conflux 上,只要稍微改一下: 运行

$REACH_CONNECTOR_MODE=ALGO ./reach react$REACH_CONNECTOR_MODE=CFX ./reach react

而如果你想在你自己的 JavaScript 项目中使用 Reach,可以调用:

$npm install @reach-sh/stdlib

(Reach 标准库正在不断改进,并经常更新。如果您遇到 Node.js 包的问题,请尝试更新!)

不变的是,您仍然可以将 Reach 程序 index.rsh 编译为 backend (build/index.main.mjs) 中,只要使用:

$./reach compile

现在我们已经将石头剪刀布实现在浏览器上了! 利用参与者交互接口中的回调,我们可以使用任何 Web UI 框架向用户显示和收集信息。

如果要发布这个应用程序,那么我们要是用 React 生成的静态文件(这其中嵌入了编译好的 Reach 程序),并将它们托管在 Web 服务器上(例如GitPage)。

在下一节中,我们会总结我们这几节教程中所取得的成果,并指导您迈向精通去中心化应用程序之旅的下一步。

您知道了吗?:

是非题: Reach 可以集成所有的 Web 界面库,如 React 、 Vue 等,因为 Reach 前端只是普通的 JavaScript 程序。

答案是: 正确的

您知道了吗?:

是非题: 在本地测试 React 程序时,Reach 嵌入了 React 开发服务器,从而加快您使用 React 的开发。

答案是: 正确的

欢迎关注Reach微信公众号 并在公众号目录 -> 加入群聊 选择加入官方开发者微信群与官方Discord群 与更多Reach开发者一起学习交流!

_.png

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ivan
Ivan
江湖只有他的大名,没有他的介绍。