2.9 网页交互

604 阅读10分钟

原文链接

在上节我们没有对 Reach 程序进行修改就让石头剪刀布可以作为命令行应用程序运行。在本节中,我们仍不需要修改 Reach 程序,我们只将 Web 接口替换命令行接口。
本教程中使用 React.js ,但其中的方法适用于任何 Web 框架。

如果你以前未曾使用过 React ,它的基本工作原理如下:

  • React 程序是 JavaScript 程序,它使用一个特殊的库,允许您在 JavaScript 主体程序中加入 HTML 。
  • React 有一个特殊的编译器,它将一组 JavaScript 程序及其所有依赖项组合成一个可以部署在静态 Web 服务器上的大文件。这就是所谓的“打包”。
  • 当您使用 React 进行开发和测试时,您可以运行一个特殊的开发 Web 服务器,该服务器会在您每次修改源文件时监视这个打包文件的更新,因此您不必经常运行编译器。
  • 在运行./reach react时会自动启动开发服务器,并让您访问它http://localhost:3000/.

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

本节中的代码不建立在前一节的基础上。Reach 附有一个方便的命令来删除配置文件:

$ ./reach unscaffold

同样,我们不需要前面的 index.mjs 文件,因为我们会完全从头编写它以使用 React 。可以运行以下命令来删除它:

$ rm index.mjs

或者,您可以将 index.rsh 文件复制到一个新目录中,然后在那里开始工作。

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

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


我们将重点讨论 tut-9/index.js ,因为 tut-9/index.rsh 与前面的章节相同。
tut-9/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 * as reach from '@reach-sh/stdlib/ETH';
 9    
..    // ...
  • 第 1 行到第 6 行,我们导入视图代码和 CSS 。
  • 第 7 行,我们导入已编译的后端。
  • 第8行中,我们将 stdlib 导入为 reach。


要在 Algorand 上运行,请在第 8 行更改导入。

从“@reach-sh/stdlib/ALGO”中导入*作为reach

tut-9/index.js

..    // ...
10    const handToInt = {'ROCK': 0, 'PAPER': 1, 'SCISSORS': 2};
11    const intToOutcome = ['Bob wins!', 'Draw!', 'Alice wins!'];
12    const {standardUnit} = reach;
13    const defaults = {defaultFundAmt: '10', defaultWager: '3', standardUnit};
14    
..    // ...

在这些行上,我们定义了一些有用的常量和默认值,它们对应于我们在 Reach 中定义的枚举。

我们开始将 App 定义为一个 React 组件,并告诉它挂载后要做什么,"挂载"是 React 的术语,即启动的意思。

图1:ConnectAccount视图。参见:AppView。

tut-9/index.js

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

tut-9/index.js

..    // ...
39      render() { return renderView(this, AppViews); }
40    }
41    
..    // ...
  • 在第 18 行,我们初始化组件状态以显示 ConnectAccount 视图(图1)。
  • 在第 20 行到第 31 行,我们连接到 React 的 componentDidMount 生命周期事件,该事件在组件启动时被调用。
  • 在第 21 行,我们使用 getDefaultAccount ,它访问默认的浏览器帐户。例如,当与以太坊一起使用时,它可以发现当前选择的 MetaMask 帐户。
  • 在第 26 行中,我们使用 getFaucet 尝试访问 Reach 开发人员测试网络水龙头。
  • 在第 27 行,如果 getFaucet 成功,我们将组件状态设置为显示 FundAccount 视图(图2)。
  • 在第 29 行,如果 getFaucet 不成功,我们将组件状态设置为跳到 DeployerOrAttacher 视图(图3)。
  • 在第 39 行,我们从 tut-9/views/AppViewws.js 中呈现适当的视图。

图2:基金账户视图。参见:AppView。

接下来,我们在App上定义callback,即当用户点击某些按钮时该做什么。
tut-9/index.js

..    . // ...
32     async fundAccount(fundAmount) {
33       await reach.transfer(this.state.faucet, 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 行,我们设置组件状态以显示 DeployerOrAttacher 视图(图3)。
  • 在第 36 行,我们定义了当用户单击  Skip  按钮时要做的事情,即设置组件状态以显示 Deployer Or Attacher 视图(图 3 )。

图3:Deployer或Attacher视图。AppViews.DeployerOrAttacher

tut-9/index.js

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

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

接下来,我们将把 Player 定义为 React 组件,它将由 Alice 和 Bob 的专用组件扩展。

图4:GetHand视图。参见:PlayerView。

我们的 Web 前端需要为玩家实现参与者交互界面,我们定义为:
tut-9/index.rsh

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

我们将通过 React 组件直接提供这些 callback 。
tut-9/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 行,我们提供 hasRandom 回调函数
  • 在第 44 至 50 行,我们提供 getHand 回调函数。
  • 在第 45 行到第 47 行,我们将组件状态设置为显示 Get Hand 视图(图 4 ),并等待可以通过用户交互解决的 Promise 。
  • 在 Promise 解析之后的第 48 行中,我们将组件状态设置为显示 Waiting For Results 视图(图 5 )。
  • 在第 51 行和第 52 行中,我们提供了seeOutcome 和 informTimeout 回调,它们设置组件状态来分别显示 Done 视图(图6)和 Timeout 视图(图7)。
  • 在第 53 行,我们定义了当用户点击石头、剪刀、布时会发生什么:第 45 行的 Promise 被解析。

图5:WaitingForResults视图。参见:PlayerViews.WaitingForResults 图6:完成视图。参见:PlayerView.Done 图7:超时视图。参见:PlayerViews.Timeout


接下来,我们将把 Deployer 定义为 Alice 的 React 组件,它扩展了 Player 。

图8:SetWager视图。参见:DeployerView。 图9:部署视图。

我们的 Web 前端需要实现 Alice 的参与者交互界面,我们定义为:
tut-9/index.rsh

..    // ...
25    const Alice =
26          { ...Player,
27            wager: UInt };
..    // ...

我们将提供赌注值,并定义一些按钮处理程序,以触发合约的部署。
tut-9/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        backend.Alice(ctc, this);
67        const ctcInfoStr = JSON.stringify(await ctc.getInfo(), null, 2);
68        this.setState({view: 'WaitingForAttacher', ctcInfoStr});
69      }
70      render() { return renderView(this, DeployerViews); }
71    }
72    
..    // ...
  • 第 59 行,我们设置组件状态以显示 SetWager 视图(图8)。
  • 在第 61 行,我们定义了当用户单击 Set Wager 按钮时要做的事情,即设置组件状态以显示 Deploy 视图(图 9 )。
  • 在第 62 至 69 行中,我们定义了当用户单击 Deploy 按钮时要做什么。
  • 在第 63 行中,我们调用 acc.deploy ,它触发契约的部署。
  • 在第 64 行,我们设置组件状态以显示部署视图(图10)。
  • 在第 65 行,我们设置了赌注属性。
  • 在第 66 行,我们开始作为 Alice 运行 Reach 程序,使用这个 React 组件作为参与者交互界面对象。
  • 在第 67 行和第 68 行,我们设置组件状态以显示 WaitingForAttacher 视图(图11),它将部署的合约信息显示为 JSON 。
  • 在第 70 行中,我们从 tut-9/views/DeployerViews.js. 中呈现适当的视图。

图10:部署视图。参见:部署查看 图11:WaitingForAttacher视图。DeployerViews.WaitingForAttacher

图12:Attacher视图。 图13:附加视图,参见:附件视图

我们的 Web 前端需要为 Bob 实现参与者交互界面,我们定义为:
tut-9/index.rsh

..    // ...
28    const Bob =
29          { ...Player,
30            acceptWager: Fun([UInt], Null) };
..    // ...

我们将提供 acceptWager 回调,并定义一些按钮处理程序,以便附加到已部署的合约。
tut-9/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 视图(图12)。
  • 在第 78 至 82 行,我们定义了当用户单击 Attach 按钮时会发生什么。
  • 在第 79 行,我们调用 acc.attach
  • 在第 80 行,我们设置组件状态以显示附加视图(图13)。
  • 在第 81 行,我们开始以 Bob 的身份运行 Reach 程序,使用这个 React 组件作为参与者交互接口对象。
  • 在第 83 行到第 88 行,我们定义了 acceptWager 回调函数。
  • 在第 85 行到第 87 行,我们将组件状态设置为显示 Accept Terms 视图(图 14 ),并等待可以通过用户交互解决的 Promise 。
  • 在第 89 行到第 92 行,我们定义了当用户单击 Accept Terms 和 Pay Wager 按钮时发生的事情:第 90 行的 Promise 被解析,我们设置组件状态以显示 Waiting For Turn 视图(图 15 )。
  • 在第 93 行,我们从 tut-9/views/AttacherViews.js 中呈现适当的视图

图14:AcceptTerms视图,请参见:AttacherViews.AcceptTerms 图15:WaitingForTurn视图。参见:AttacherViews.WaitingForTurn


tut-9/index.js

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

最后,我们调用 tut-9/views/render.js 中的一个小助手函数来呈现我们的 App 组件。

为了方便运行 React 开发服务器,您可以调用:

$ ./reach react

要使用 Algorand 运行 React 开发服务器,您可以调用:

$ REACH_CONNECTOR_MODE=ALGO./reach react

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

$ npm install @reach-sh/stdlib

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

与往常一样,您可以将 Reach 程序 index.rsh 编译到后端构建工件 build/index.main.mjs 中,使用:

$ ./reach run

现在我们的石头剪刀布的实现在浏览器上!我们可以利用参与者交互界面中的回调,通过我们选择的任何 Web UI 框架向用户显示和收集信息。
如果我们想将这个应用程序发布,那么我们将获取 React 生成的静态文件,并将它们托管在 Web 服务器上。这些文件嵌入了你编译好的 Reach 程序,所以除了将它们提供给外界之外,别无他法。
下一节中,我们将总结我们所取得的成果,并指导您迈向精通去中心化应用程序之旅的下一步。

您知道了吗?:

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

答案是: 正确

您知道了吗?:

是非题: Reach 通过嵌入 React 开发服务器和部署过程来在本地测试 React 程序,从而加快您使用 React 的开发。

答案是: 正确

上一篇 : 2.8 交互及自主运行

下一篇 : 2.10 未来展望