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

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

*[Reach教程翻译] Reach是安全简单的Dapp开发语言 让用户可以像开发传统App一样开发DApp 目前使用Reach开发的智能合约可以部署在以太坊、Conflux、Algorand [Reach官网](https://reach.sh/) [Reach官方文挡](https://docs.reach.sh/)* # 2.9 通过 React 实现游戏网页交互 [原文链接](https://docs.reach.sh/tut-9.html) 在上节我们展示了不用修改 Reach 程序就让石头剪刀布可以作为命令行应用程序运行。在本节中,我们仍不需要修改 Reach 程序,而我们将前端由命令行改为 Web 界面。 本教程中使用 React.js ,但你可以使用任何语言来为你的 Reach 合约制作前端。 和之前一样,在本教程中,我们假设将使用以太坊进行部署(和测试)。Reach Web 应用程序需要浏览器支持的共识网络帐户及其关联钱包。在以太坊上,标准的钱包是 MetaMask 。如果你想测试教程里的代码,需要先安装并设置 MetaMask 。此外,MetaMask 不支持多个实例账户,所以如果你想在本地测试石头剪刀布!您需要有两个独立的浏览器实例(例如:火狐+Chrome):一个作为 Alice ,另一个作为 Bob 。 我们将之前写好的 index.rsh 文件复制到一个新目录中,然后在那里开始这份教程的工作。 我们不会用到之前的 `index.mjs` 文件,取而代之的,我们会新写一个 `index.js` 文件。 我们还需要 [index.css](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.css) 和一些[视图](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views)。这些细节相当琐碎且不是 Reach 特有的,因此我们不会解释这些文件的细节。如果要在本地运行,你需要下载那些文件。目录应该如下所示: | ├── index.css | | ├── index.js | | ├── index.rsh | | └── views | | ----├── AppViews.js | | ----├── AttacherViews.js | | ----├── DeployerViews.js | | ----├── PlayerViews.js | | ----└── render.js | — 我们将重点讨论 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js) ,因为 [rps-9-web/index.rsh](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.rsh) 与前面的章节中完全相同。 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L1-L9) ```javascript 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L10-L14) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L15-L31) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L39-L41) ```javascript .. // ... 39 render() { return renderView(this, AppViews); } 40 } 41 .. // ... ``` * 在第 19 行,我们初始化组件状态以显示 ConnectAccount 对话框(2.9.2)。 * 在第 21 行到第 31 行,我们连接到 React 的 componentDidMount 生命周期事件,该事件在组件启动时被调用。 * 在第 22 行,我们使用 [getDefaultAccount](https://docs.reach.sh/ref-frontends-js-acc.html#%28javascript._%28%28get.Default.Account%29%29%29) ,它会连接默认的浏览器钱包。例如,当与以太坊一起使用时,它会连接当前的 MetaMask 帐户。 * 在第 26 行中,我们使用 [canFundFromFaucet](https://docs.reach.sh/ref-frontends-js-acc.html#%28javascript._%28%28can.Fund.From.Faucet%29%29%29) 尝试访问 Reach 开发人员测试网络水龙头。 * 在第 27 行,如果 [canFundFromFaucet](https://docs.reach.sh/ref-frontends-js-acc.html#%28javascript._%28%28can.Fund.From.Faucet%29%29%29) 为 true,我们设置组件状态,显示 FundAccount 对话框(2.9.3)。 * 在第 29 行,如果 [canFundFromFaucet](https://docs.reach.sh/ref-frontends-js-acc.html#%28javascript._%28%28can.Fund.From.Faucet%29%29%29) 为 false,我们设置组件状态,跳到 Choose Role 对话框(2.9.4)。 * 在第 39 行,我们从 [rps-9-web/views/AppViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AppViews.js) 中渲染对应的视图。 ## 2.9.2 Connect Account 对话框 将应用程序组件加上视图([rps-9-web/views/AppViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AppViews.js#L19-L28))后,看起来就像这样: [![](https://camo.githubusercontent.com/6d62484738725fb8940be710e16b3061dc202d3dff8e72b731aeffa6ba4eb31a/68747470733a2f2f646f63732e72656163682e73682f436f6e6e6563744163636f756e742e706e67)](https://camo.githubusercontent.com/6d62484738725fb8940be710e16b3061dc202d3dff8e72b731aeffa6ba4eb31a/68747470733a2f2f646f63732e72656163682e73682f436f6e6e6563744163636f756e742e706e67) ## 2.9.3 Fund Account 对话框 接下来,我们定义 `App` 上的 callback,即当用户点击按钮时该做什么。 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L32-L36) ```javascript .. . // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AppViews.js#L30-L54))后看起来就是这样: [![](https://camo.githubusercontent.com/b7ad645e3a64ee9ec403bb2b7953dc8bb472f26c6329c27ba3142261ea8b6332/68747470733a2f2f646f63732e72656163682e73682f46756e644163636f756e742e706e67)](https://camo.githubusercontent.com/b7ad645e3a64ee9ec403bb2b7953dc8bb472f26c6329c27ba3142261ea8b6332/68747470733a2f2f646f63732e72656163682e73682f46756e644163636f756e742e706e67) ## 2.9.4 Choose Role 对话框 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L37-L38) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AppViews.js#L56-L78))后,看起来是这样的: [![](https://camo.githubusercontent.com/35ec01e8a69c9f0850b87ae78a50e18a245d1d653af108bd870c66e2a77f7b24/68747470733a2f2f646f63732e72656163682e73682f4465706c6f7965724f7241747461636865722e706e67)](https://camo.githubusercontent.com/35ec01e8a69c9f0850b87ae78a50e18a245d1d653af108bd870c66e2a77f7b24/68747470733a2f2f646f63732e72656163682e73682f4465706c6f7965724f7241747461636865722e706e67) ## 2.9.5 Player 组件 接下来,我们定义 React 组件 `Player`, Alice 和 Bob 将会在此基础上扩展。 我们的前端需要实现这些 Reach 后端中定义的参予者交互接口: [rps-9-web/index.rsh](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.rsh#L20-L25) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L42-L55) ```javascript .. // ... 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 行中,我们提供了`seeOutcome` 和 `informTimeout` 回调,它们设置组件状态来分别显示 Done 视图(2.9.8)和 Timeout 视图(2.9.9)。 * 在第 53 行,我们定义了当用户点击石头、剪刀、布时会发生什么:resolve 第 45 行的 Promise 。 ## 2.9.6 Get Hand 对话框 这个对话框([rps-9-web/views/PlayerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/PlayerViews.js#L8-L32))用来让玩家出拳,他看起来是这样的: [![](https://camo.githubusercontent.com/3dd3b81c4851f5c70cf5ae0f612ab4d672a1d059f412759fde82c87b9d358ab2/68747470733a2f2f646f63732e72656163682e73682f47657448616e642e706e67)](https://camo.githubusercontent.com/3dd3b81c4851f5c70cf5ae0f612ab4d672a1d059f412759fde82c87b9d358ab2/68747470733a2f2f646f63732e72656163682e73682f47657448616e642e706e67) ## 2.9.7 Waiting for results 对话框 这个对话框([rps-9-web/views/PlayerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/PlayerViews.js#L34-L42))看起来是这样: [![](https://camo.githubusercontent.com/e29e7d726d749bf2dd9f316edac5d104a119bb81572b2cc81e0677a3f859ad17/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f72526573756c74732e706e67)](https://camo.githubusercontent.com/e29e7d726d749bf2dd9f316edac5d104a119bb81572b2cc81e0677a3f859ad17/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f72526573756c74732e706e67) ## 2.9.8 Done 对话框 游戏结束时我们展示这个对话框([rps-9-web/views/PlayerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/PlayerViews.js#L44-L54)) [![](https://camo.githubusercontent.com/1b34d2c5d574cddd092d4e9f6ea844dd73fcb615bb6f9ac8412418b13606943b/68747470733a2f2f646f63732e72656163682e73682f446f6e652e706e67)](https://camo.githubusercontent.com/1b34d2c5d574cddd092d4e9f6ea844dd73fcb615bb6f9ac8412418b13606943b/68747470733a2f2f646f63732e72656163682e73682f446f6e652e706e67) ## 2.9.9 Timeout 对话框 当有玩家观察到超时时,我们展示([rps-9-web/views/PlayerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/PlayerViews.js#L56-L64)),看起来如下: [![](https://camo.githubusercontent.com/32f15fbcb4481596e4b8730b53d8d800d18b51154a000e0e94199153b8847377/68747470733a2f2f646f63732e72656163682e73682f54696d656f75742e706e67)](https://camo.githubusercontent.com/32f15fbcb4481596e4b8730b53d8d800d18b51154a000e0e94199153b8847377/68747470733a2f2f646f63732e72656163682e73682f54696d656f75742e706e67) ## 2.9.10 Deployer 组件 接下来考虑Alice,我们替她定义一个名为 `Deployer` 的 React 组件,它扩展了 `Player` 。 同样的,前端需要实现后端定义的这些参与者交互接口: [rps-9-web/index.rsh](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.rsh#L28-L32) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L56-L72) ```javascript .. // ... 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` 作为[参与者交互接口](https://docs.reach.sh/ref-programs-appinit.html#%28tech._participant._interact._interface%29)对象。 * 在第 68 - 69 行,我们设置组件状态,显示 Waiting For Attacher 对话框(2.9.14),它将部署的合约信息显示为 JSON 。 * 在第 71 行中,我们从 [rps-9-web/views/DeployerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/DeployerViews.js) 中呈现对应的视图。 ## 2.9.11 Set Wager 对话框 这个对话框([rps-9-web/views/DeployerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/DeployerViews.js#L20-L38))让玩家设置赌注: [![](https://camo.githubusercontent.com/78d0682eacaf558f8ee94de24237afffc3f1506fd226f3be9427f3359b95bc90/68747470733a2f2f646f63732e72656163682e73682f53657457616765722e706e67)](https://camo.githubusercontent.com/78d0682eacaf558f8ee94de24237afffc3f1506fd226f3be9427f3359b95bc90/68747470733a2f2f646f63732e72656163682e73682f53657457616765722e706e67) ## 2.9.12 Deploy 对话框 这个对话框([rps-9-web/views/DeployerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/DeployerViews.js#L40-L53))用于让玩家部署合约: [![](https://camo.githubusercontent.com/b88a3c5e8b05ea05b63132757f582faa82e7178ce474e0d2a586a13e7222c16e/68747470733a2f2f646f63732e72656163682e73682f4465706c6f792e706e67)](https://camo.githubusercontent.com/b88a3c5e8b05ea05b63132757f582faa82e7178ce474e0d2a586a13e7222c16e/68747470733a2f2f646f63732e72656163682e73682f4465706c6f792e706e67) ## 2.9.13 Deploying 对话框 在部署过程中,我们展示这个对话框([rps-9-web/views/DeployerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/DeployerViews.js#L55-L61)): [![](https://camo.githubusercontent.com/35e1f776b2fae4f0db8685a81676fb65d6cd90b2ed4269b68d80682fde8da87f/68747470733a2f2f646f63732e72656163682e73682f4465706c6f79696e672e706e67)](https://camo.githubusercontent.com/35e1f776b2fae4f0db8685a81676fb65d6cd90b2ed4269b68d80682fde8da87f/68747470733a2f2f646f63732e72656163682e73682f4465706c6f79696e672e706e67) ## 2.9.14 Waiting for Attacher 对话框 玩家部署后等待另一位玩家加入时,我们 呈现这个视图(([rps-9-web/views/DeployerViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/DeployerViews.js#L63-L90))): [![](https://camo.githubusercontent.com/3f247bd8291f585db1f404bcc2c09e5427193dc37d0348c7da77b7c09f037ad3/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f7241747461636865722e706e67)](https://camo.githubusercontent.com/3f247bd8291f585db1f404bcc2c09e5427193dc37d0348c7da77b7c09f037ad3/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f7241747461636865722e706e67) ## 2.9.15 Attacher 组件 类似的,对于 Bob,前端需要实现后端定义的这些参与者交互接口: [rps-9-web/index.rsh](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.rsh#L33-L36) ```javascript .. // ... 33 const Bob = Participant('Bob', { 34 ...Player, 35 acceptWager: Fun([UInt], Null), 36 }); .. // ... ``` 我们会定义 `acceptWager` 回调函数,并定义加入另一个玩家部署的合约的按钮。 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L73-L95) ```javascript .. // ... 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](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AttacherViews.js) 中呈现对应的视图 ## 2.9.16 Attach 对话框 这个对话框([rps-9-web/views/AttacherViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AttacherViews.js#L18-L39))让玩家加入部署好的合约: [![](https://camo.githubusercontent.com/3ca55875f05b96705c2d04decf721a17eece3c0144ec74e826ea2dde34fc390d/68747470733a2f2f646f63732e72656163682e73682f4174746163682e706e67)](https://camo.githubusercontent.com/3ca55875f05b96705c2d04decf721a17eece3c0144ec74e826ea2dde34fc390d/68747470733a2f2f646f63732e72656163682e73682f4174746163682e706e67) ## 2.9.17 Attaching 对话框 在加入过程中,我们展示这个对话框([rps-9-web/views/AttacherViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AttacherViews.js#L41-L49)),看起来是这样的: [![](https://camo.githubusercontent.com/c30f4fce78feb73c2ba2a53eb4a57fbd9727c98cac56ffda93637bb416c7479e/68747470733a2f2f646f63732e72656163682e73682f417474616368696e672e706e67)](https://camo.githubusercontent.com/c30f4fce78feb73c2ba2a53eb4a57fbd9727c98cac56ffda93637bb416c7479e/68747470733a2f2f646f63732e72656163682e73682f417474616368696e672e706e67) ## 2.9.18 Accept Terms 对话框 这个对话框([rps-9-web/views/AttacherViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AttacherViews.js#L51-L70))让玩家同意赌注: [![](https://camo.githubusercontent.com/4a87bf809aaef6a03e20794c889a519e6bae84d87e3c001ee53c6ebe751203d9/68747470733a2f2f646f63732e72656163682e73682f4163636570745465726d732e706e67)](https://camo.githubusercontent.com/4a87bf809aaef6a03e20794c889a519e6bae84d87e3c001ee53c6ebe751203d9/68747470733a2f2f646f63732e72656163682e73682f4163636570745465726d732e706e67) ## 2.9.19 Waiting for Turn 对话框 在玩家等待对方出拳的时候,我们展示这个对话框([rps-9-web/views/AttacherViews.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/AttacherViews.js#L72-L81)): [![](https://camo.githubusercontent.com/0f1cf1d0207f991b531af44b2ab6417c6ec5eff44ba7a47d2f84c6ac6ed0d704/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f725475726e2e706e67)](https://camo.githubusercontent.com/0f1cf1d0207f991b531af44b2ab6417c6ec5eff44ba7a47d2f84c6ac6ed0d704/68747470733a2f2f646f63732e72656163682e73682f57616974696e67466f725475726e2e706e67) ## 2.9.20 大功告成 [rps-9-web/index.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/index.js#L96-L96) ```javascript .. // ... 96 renderDOM(<App />); ``` 最后,我们调用 [rps-9-web/views/render.js](https://github.com/reach-sh/reach-lang/blob/master/examples/rps-9-web/views/render.js)中的函数帮助呈现我们渲染 `App` 组件。 如果要使用 React 开发服务器在本地测试,只需要运行: `$ ./reach react` 之后,便可以通过两个浏览器实例访问 [http://localhost:3000/](`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](https://img.learnblockchain.cn/attachments/2021/11/IslW4w8i6192082848c1a.png)

[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(&lt;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开发者一起学习交流!

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

  • 发表于 2021-12-06 14:59
  • 阅读 ( 185 )
  • 学分 ( 5 )
  • 分类:其他
  • 专栏:Reach开发资料

评论