这节教程中,我们利用 React 为石头剪刀布游戏制作一个网页前端,可以在本地测试并部署到共识网络上。 至此,我们的石头剪刀布游戏便大功告成了!
[Reach教程翻译] Reach是安全简单的Dapp开发语言 让用户可以像开发传统App一样开发DApp 目前使用Reach开发的智能合约可以部署在以太坊、Conflux、Algorand Reach官网 Reach官方文挡
在上节我们展示了不用修改 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);
.. // ...
backend
。stdlib
导入为 reach
。React 编译 Reach 标准库时没有办法直接获取选择合适标准库的环境变量,因此我们需要将 process.env
传入。
.. // ...
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};
.. // ...
我们将应用程序主要的视图 App
定义为一个 React 组件,并告诉它挂载后要做什么,"挂载"是 React 的术语,即启动的意思。
.. // ...
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 }
.. // ...
.. // ...
39 render() { return renderView(this, AppViews); }
40 }
41
.. // ...
将应用程序组件加上视图(rps-9-web/views/AppViews.js)后,看起来就像这样:
接下来,我们定义 App
上的 callback,即当用户点击按钮时该做什么。
.. . // ...
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'}); }
.. . // ...
Fund Account
时要做什么。Skip
按钮时要做的事情,即设置组件状态以显示 Choose Role 对话框(2.9.4)加上视图(rps-9-web/views/AppViews.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)后,看起来是这样的:
接下来,我们定义 React 组件 Player
, Alice 和 Bob 将会在此基础上扩展。
我们的前端需要实现这些 Reach 后端中定义的参予者交互接口:
.. // ...
20 const Player = {
21 ...hasRandom,
22 getHand: Fun([], UInt),
23 seeOutcome: Fun([UInt], Null),
24 informTimeout: Fun([], Null),
25 };
.. // ...
我们直接通过 React 组件提供这些 callback
.. // ...
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
.. // ...
random
回调函数seeOutcome
和 informTimeout
回调,它们设置组件状态来分别显示 Done 视图(2.9.8)和 Timeout 视图(2.9.9)。这个对话框(rps-9-web/views/PlayerViews.js)用来让玩家出拳,他看起来是这样的:
这个对话框(rps-9-web/views/PlayerViews.js)看起来是这样:
游戏结束时我们展示这个对话框(rps-9-web/views/PlayerViews.js)
当有玩家观察到超时时,我们展示(rps-9-web/views/PlayerViews.js),看起来如下:
接下来考虑Alice,我们替她定义一个名为 Deployer
的 React 组件,它扩展了 Player
。
同样的,前端需要实现后端定义的这些参与者交互接口:
.. // ...
28 const Alice = Participant('Alice', {
29 ...Player,
30 wager: UInt, // atomic units of currency
31 deadline: UInt, // time delta (blocks/rounds)
32 });
.. // ...
我们要提供赌注值 wager
,并定义部署合约的按钮。
.. // ...
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 }
.. // ...
Set Wager
按钮时要做的事情,即设置组件状态以显示 Deploy 对话框(2.9.12)。Deploy
按钮时要做什么。wager
。deadline
。this
作为参与者交互接口对象。这个对话框(rps-9-web/views/DeployerViews.js)让玩家设置赌注:
这个对话框(rps-9-web/views/DeployerViews.js)用于让玩家部署合约:
在部署过程中,我们展示这个对话框(rps-9-web/views/DeployerViews.js):
玩家部署后等待另一位玩家加入时,我们 呈现这个视图((rps-9-web/views/DeployerViews.js)):
类似的,对于 Bob,前端需要实现后端定义的这些参与者交互接口:
.. // ...
33 const Bob = Participant('Bob', {
34 ...Player,
35 acceptWager: Fun([UInt], Null),
36 });
.. // ...
我们会定义 acceptWager
回调函数,并定义加入另一个玩家部署的合约的按钮。
.. // ...
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
.. // ...
acc.attach
this
作为参与者交互接口对象。acceptWager
回调函数。Accept Terms and Pay Wager
按钮时发生的事情:resolve 第 90 行的 Promise ,设置组件状态以显示 Waiting For Turn 对话框(2.9.19)。这个对话框(rps-9-web/views/AttacherViews.js)让玩家加入部署好的合约:
在加入过程中,我们展示这个对话框(rps-9-web/views/AttacherViews.js),看起来是这样的:
这个对话框(rps-9-web/views/AttacherViews.js)让玩家同意赌注:
在玩家等待对方出拳的时候,我们展示这个对话框(rps-9-web/views/AttacherViews.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开发者一起学习交流!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!