随遇而安的DAPP开发实践教程(四)第一个自创DAPP应用下

1、调试合约代码

在上一篇教程(随遇而安的DAPP开发实践教程(三)第一个自创DAPP应用 [上])中,我们使用Solidity语言编写了一个简单但是还比较有趣的“21点”扑克牌游戏,并且使用JS测试脚本测试了它的可行性。它最大的特色莫过于结合了区块链的“去中心化”优势,以及可靠安全的分布式数据存储特性。游戏的开发者(也就是笔者本人)不需要维护一个专门的中央服务器来保存每个用户的筹码(也就是前文的BJT代币),只要把合约发布到一个公共链上(例如Ethereum),然后再提供一个可视化的游戏前端即可。而这也正是本篇教程所试图完成的工作。

这项工作本身并没有多么复杂,事实上,我们在上一篇教程中已经使用JS编写了一部分核心的合约接口调用脚本,即test/blackjack.js的实现部分。通过阅读更早的一些教程以及代码(例如WebPack工程),我们也已经了解到这些脚本在实际的前端开发中具有相当的可复用性。不过,在开始着手编码之前,我们还是要先后了解一下智能合约的调试方法,以及最终程序的打包方法,这对于实际的DAPP开发同样是非常重要的。

截至本文写作的期间,Solidity代码的调试还远不同于其它编程语言的调试过程。它没有一个直观的IDE(统一的开发环境)界面,也没有特别方便的打印和Log日志工具。虽然Solidity语言中没有复杂的垃圾回收,指针访问等操作,不会轻易造成系统崩溃;但是开发过程中同样存在数组越界,类型转换溢出等问题,或者因为参数传递错误而造成整个函数的执行结果不正确——关键的问题在于,如果你是在发布合约之后才查到了这些错误,那么反复修改和重发布合约就意味着要为此付出更多的真金白银,甚至是让DAPP的使用者感到不安和厌烦。

因此,合约代码的调试工作是一个绝对不容忽视的话题,而Truffle目前的debug机制又实在是晦涩难懂(后文中我们也会简单提及)。这该怎么办才好呢?

我们在之前的教程里曾经提及过在合约代码中使用Event的方法,但是并没有演示它是如何被前端脚本监听和调用的。对于一个难以直观地进行调试工作,也没有完善的日志机制的系统来说,最顺手的调试方法就只剩下传统的“插旗子”,也就是在代码中适当的位置插入事件Event,然后从JS端获取反馈信息,并辅助判断。

我们在之前的BlackJackGame.sol文件中,加入如下两个事件定义:

contract BlackJackGame is StandardToken {

……

event NewGameCreated(uint gameId, address playerA, address playerB);

event NewHit(uint gameId, address player, uint card);

……

}

它们的触发时机分别对应于新游戏创建的时候,以及新的扑克牌发出的时候,因此在createNewGame()函数中,我们加入:

function createNewGame(address _playerA, address _playerB, uint _initialBet) external {

……

for (uint i = 0; i

if (gameDataPool[i].finished) {

NewGameCreated(i, _playerA, _playerB); //触发事件

……

}

}

……

NewGameCreated(gameDataPool.length - 1, _playerA, _playerB); //触发事件

}

而在hitNewCard()函数的末尾部分,我们有:

function hitNewCard(uint _gameId, address _player, uint _bet) external {

……

NewHit(_gameId, _player, card);

}

其中card是我们在函数代码中定义的局部变量,它的取值应当总是在0-51之间。

下面是test/blackjack.js文件的部分,当新的事件被触发时,JS代码应当即时获取这个事件以及它的具体参数值,并且反馈到后继代码中:

let blackjack = await BlackJackGame.deployed();

blackjack.NewGameCreated().watch(function(error, result) {

if (error) { console.log(error); return; }

blackjack.NewGameCreated().stopWatching();

});

blackjack.NewHit().watch(function(error, result) {

if (error) { console.log(error); return; }

var type = parseInt(cardValue / 13 + 1), card = cardValue % 13 + 1;

console.log("New card " + card + " (Type = " + type

});

代码不长,很容易理解。Truffle编译得到的build文件中已经包含了所有的接口函数和事件名称,因此我们可以直接在前端代码中调用它们。在第一次取得BlackJackGame对象之后,我们就可以使用watch去监听所有的事件了。监听的回调函数中有两个固定的参数error和result,前者顾名思义是用来记录一些可能的错误信息,后者则是一个JSON字符串,其中包含了当前事件的参数值,合约地址,以及交易记录和区块信息等。

在事件中调用下面的语句可以直接打印result的全部内容:

console.log(result);

它的执行结果可能如下所示:

{

logIndex: 0,

transactionIndex: 0,

transactionHash:……,

blockHash:……, blockNumber: 36,

address:……,type: 'mined',

event: 'NewGameCreated', //事件的名称

args: { //事件的参数

gameId: BigNumber { s: 1, e: 0, c: [Array] },

playerA: '0xf17f52151ebef6c7334fad080c5704d77216b732',

playerB: '0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef'

}

}

现在我们也可以通过truffle.cmd test test/blackjack.js来之际显示测试比赛的过程和事件中的信息。注意,回调事件的触发和其它JS脚本的执行流程相比可能有所延迟,不能视为是顺序执行的。

除了插旗子之外,我们也可以在Solidity代码中使用断言的方法,与之对应的函数有两个:require()和assert(),其用法也是类似的。比如,我们可以在之前的createNewGame()函数中使用断言来判断用户手中的代币是否还足够支付初始下注的筹码:

function createNewGame(address _playerA, address _playerB, uint _initialBet) external {

require(_initialBet

require(_initialBet

……

}

或者在hitNewCard()的末尾处使用断言判断生成的card数值是否合法(0-52之间):

function hitNewCard(uint _gameId, address _player, uint _bet) external {

……

assert(card

}

上面的两端代码同时也演示了require()和assert()这两个函数在实际应用中的区别:require()通常用在一个函数的开头部分,用来检查输入参数是否正确,是否可用;如果发生错误,它不会继续消耗使用者的财产(Gas)去执行后面的代码。

而assert()则通常用在一个函数的末尾部分,它可以检查函数运行过程的一些结果数据是否正确。如果发生错误(这类错误通常会很严重,很可能已经导致整个交易出现错误了),那么assert()会尝试恢复之前改动过的数据,确保整个系统不会因此而变得无法使用。不过,正因为assert()有这样的特性,它无论如何都会消耗掉函数执行对应的所有Gas。

我们尝试在测试代码中触发一下createNewGame()里的断言,我们修改JS代码中稍早的部分,让用户只给用户1发放1个代币:

it("Send tokens to first 2 users", async () => {

let blackjack = await BlackJackGame.deployed();

await blackjack.transfer(accounts[1], 1, );

await blackjack.transfer(accounts[2], 100, );

……

});

重新编译和发布合约代码,然后执行测试脚本,我们可以看到类似下面的错误提示:

这显然是一团乱麻,虽然没有钱下注确实是一件非常丢脸的事情,不过也犯不上让整个系统看起来如同崩溃一样吧。为此,我们有必要在测试的JS代码中加入try...catch语法来捕获错误信息:

it("Start game and bet 10BJT each", async () => {

……

try {

await blackjack.approve(accounts[0], 100, );

await blackjack.approve(accounts[0], 100, );

await blackjack.createNewGame(accounts[1], accounts[2], 10);

gameCreated = true;

} catch (error) {

console.log("Error! Current game must stop");

gameCreated = false; return;

}

……

}

这里的gameCreated是一个全局定义的变量,它可以用在后面的测试过程中,判断当前比赛是否成功建立了(否则也就没有必要继续后面的测试)。执行测试代码,这次我们的JS程序发现了没有钱参加比赛的用户1,并且果断发出了警告:

当然,除了本节所介绍的Event“插旗子”调试法,以及使用断言的调试方法之外。Truffle也提供了一个目前还不够直观的合约代码调试方案,篇幅所限这里就不再深入讲解了,读者可以通过下面的链接来了解它的强大(以及简陋)之处:

http://truffleframework.com/tutorials/debugging-a-smart-contract

2、准备npm包配置文件

下一步我们将打开BlackJack工程根目录下的package.json文件,npm配置工具会根据这个文件的具体内容去下载对应的NodeJS依赖库,执行特定的脚本,或者生成可发布的前端代码。

使用package.json配置NodeJS工程的过程,本身就足以写成一个系列教程了。当然在本文中我们不会花费过多的篇幅去介绍它,感兴趣的读者可以参阅以下网址:

https://docs.npmjs.com/files/package.json

我们直接使用任意文本编辑工具在文件的开头部分(第二行)中插入如下内容:

{

"name": "blackjack-game", //包的名称

"version": "0.0.1", //包的版本

"description": "An example DAPP: BlackJack", //包的介绍

"author": "Wang Rui", //作者名称

"license": "MIT", //遵循的协议

"scripts": { //可以用npm run执行的脚本

"build": "webpack", //调用预设的打包脚本

"dev": "webpack-dev-server" //调用预设的测试服务器脚本

},

"devDependencies": { //开发所需的依赖库

"babel-cli": "^6.22.2", //支持新的JS语法的编译器Babel

"babel-core": "^6.22.1",

"babel-loader": "^6.2.10",

"babel-plugin-transform-runtime": "^6.22.0",

"babel-preset-env": "^1.1.8",

"babel-register": "^6.22.0",

"copy-webpack-plugin": "^4.0.1", // webpack的资源拷贝插件

"css-loader": "^0.26.1", // webpack的CSS加载插件

"html-webpack-plugin": "^2.28.0", // webpack的HTML打包插件

"json-loader": "^0.5.4", // webpack的JSON加载插件

"style-loader": "^0.13.1", // webpack的样式加载插件

"truffle-contract": "^1.1.11", // Truffle提供的合约交互抽象层

"web3": "^0.20.0", // Ethereum提供的合约接口交互库

"webpack": "^2.2.1", //前端打包工具webpack

"webpack-dev-server": "^2.3.0" // webpack的测试服务器

},

……//以下为文件之前的内容

这里要特别提到一个知名的前端JS代码和模块打包工具,webpack,它的官方网站为:

在前面的教程中,我们就是使用这个工具来完成Truffle的WebPack示例工程的测试和打包输出的。最终输出的结果可以只是一个.html文件和一个.js文件,后者集成了所有必备的依赖库代码,以及用户自行编写的JS脚本部分;而在此之前,我们也可以用npm run dev来启动一个位于本地8080端口的服务器端,测试前端程序是否正确运行,并修改和完善游戏逻辑的部分。

现在我们可以进入工程根目录,并用下面的控制台指令来加载所有必备的依赖库了,目前它们还不存在于本地环境中:

# npm install

经过一段时间的等待,现在我们可以在node_modules目录中看到足够丰富的内容了。下一步的工作是webpack库的配置,我们可以在之前给出的网址中找到足够多的说明文档和示例;不过在这个有关DAPP配置和发布的教程中,我们选择直接在工程根目录中新建一个webpack.config.js文件,并输入下面的内容:

const path = require('path');

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {

entry: './app/javascripts/app.js', //要打包的JS文件

output: {

path: path.resolve(__dirname, 'build'),

filename: 'app.js' //输出路径和文件名

},

plugins: [ // HTML文件直接拷贝到输出路径

new CopyWebpackPlugin([])

],

module: { //定义不同格式的文件转换到JS的规则和工具

rules: [],

loaders: [

{ test: /\.json$/, use: 'json-loader' },

{

test: /\.js$/,

exclude: /(node_modules|bower_components)/,

loader: 'babel-loader',

query:

}

]

}

}

准备就绪,现在是时候编写前端的代码了。

3、用户界面和前端代码

在本教程的最后一部分,我们开始为“21点”这款简单的DAPP游戏编写用户界面和前端代码。如果这是一个完整的线上棋牌类游戏的话,可能还要“用户管理”,“游戏大厅”,“建立游戏房间”,“聊天室”等繁杂的模块开始。可惜我们并没有足够的篇幅去处理这么多的内容,因此这里选择将前端的功能简化到最低限度,即,假设游戏始终发生在两位固定的选手之间(例如之前测试用例中的用户1和用户2),让他们在同一个页面下直接开始比赛,下注,发牌,以及裁定结果。

这样一个“热座”(Hot Seating)类型的游戏对应的HTML界面如下所示,它虽然还十分简陋,但是已经涵盖了一个“21点”扑克牌游戏所需的基本功能:

为了正确显示52种扑克牌样式,我们还需要提前准备一套图片素材,共四种花色(1-4分别表示黑桃/梅花/方块/红桃),每种花色13张牌(1-13),因此红桃J的图片对应为4-11.png,如图:

因为篇幅所限,这里不再给出index.html和app.js文件的全部代码,只是针对重点函数及其调用进行说明。欢迎读者访问本系列教程的GitHub地址来获取最新的源代码,并提出修改意见:

https://github.com/xarray/ethereum_dapp_demos

我们仿照之前教程中的WebPack工程,编写app/app.js文件并在其中定义一个App对象,它包含了所有必要的合约调用接口,包括:

start():获取合约接口对象BlackJackGame,以及当前的用户名单。

initializeBalance():初始化用户1和2所持的代币数,这一段代码的实现与之前测试代码的第一个步骤类似。注意,这个初始化过程对于整个合约来说应当是只执行一次的,因为我们的合约只分配了200个代币,并且一次性分给了用户1和2;如果再次执行初始化函数的话(比如刷新页面)就会产生错误,因为系统已经没有更多的代币可以分配了。读者可以自行考虑如何让BJT代币的分配机制更为严谨和可靠。

refreshBalance():刷新用户1和2所持代币数在HTML界面上的显示,这一函数在初始化时和每次游戏结束时都会执行一次。

createNewGame():创建新的游戏,此时应当清除HTML页面上所有上一盘比赛的信息,并且执行对应的合约函数开始新游戏。我们不妨把事件的监听处理也放在这里执行,本教程第一节中提到的两个事件都会反馈一些有用的信息(游戏场次ID,以及发牌信息),它们需要被随时更新到HTML界面上。

hitNewCard():调用合约函数,指定的玩家发一张牌。

holdForResult():指定的玩家停牌,如果两位玩家都停牌,则自动开始判断比赛结果。

checkWinner():调用合约函数,判断比赛结果,并反馈到HTML界面上。

在app/index.html中,我们需要首先点击“Start new game”来启动一场新的比赛:

BlackJack

Game not started

Start new game

……

然后是两位参赛者的地址,所持的代币数,每次Hit之前的下注数(默认为2),以及“Hit”(发牌)和“Hold”(停牌)两个按钮:

Account 1 has 0 BJT

Bet:

Hit

Hold

我们可以结合app.js文件中的内容来观察HTML元素是如何被重新赋值的。以NewHit事件的回调函数为例:

blackjack.NewHit().watch(function(error, result) {

if (error) { alert(error); return; }

//返回参数除以13并加1,得到花色(1-4);取余数再加1,得到点数

var type = parseInt(cardValue / 13 + 1), card = cardValue % 13 + 1;

//判断玩家对应的HTML元素

var elementName = "";

//直接增加对象,显示对应花色和点数的牌

document.getElementById(elementName).innerHTML +=

"";

});

一切准备就绪之后,输入npm run build,从app目录(这是webpack.config.js文件中指定的)构建发布文件,JS文件和依赖库会被自动打包到一起,并存入build目录下。注意将app/images目录也拷贝过去,以确保所有的图片在当前目录下。直接双击build/index.html打开浏览器,此时合约接口应当已经被初始化,两位玩家各自持有100BJT的代币:

点击“Start new game”开始比赛。然后两位玩家可以各自点击“Hit”给自己发牌,并根据双方的牌型来判断当前局势。当然,坐在同一台电脑前的两位游戏者恐怕会因此迅速展开一场“肉搏战”,作为一款网络游戏来说这显然是不恰当的,不过作为我们的第一个DAPP实验游戏来说,这样也别有一番风味呢。

当双方都点击“Hold”停牌后,系统将自动判断本场比赛的胜负,并直接输出结果:

意犹未尽的玩家不妨再次点击“Start new game”,重新开局。此时他们之前获得的代币筹码依然保留,可以一直战斗到一方油尽灯枯为止了:

当然,正如之前所提到的,因为我们在JS脚本的initializeBalance()函数中并没有定义合理的代币分配方法,因此如果刷新这个页面或者重新打开它,系统将会提示错误,因为用户(也就是负责合约执行的系统用户)手中已经没有多余的代币了。对于这个例程来说,我们只能在控制台重新发布合约并重新打包JS文件,即:

# truffle.cmd migrate --reset

# npm run build

然后再次打开build/index.html文件。这样很愚蠢,并且这绝非一个完整的网络棋牌类游戏。不过从编写智能合约来实现一个示例游戏,并且将其与可视化的前端结合的角度来说,我们的基本工作已经胜利完成了。如何细化游戏逻辑,甚至是将它成品化与商品化;又或者举一反三,实现更多类似类型的DAPP游戏,这就是聪明的读者您所要面临的问题了。而我们的教程下一步的目标,是考虑如何让游戏方式和显示内容变得更有趣一些,或者变得更专业一些,例如用在一些规模化的科学数据可视化领域?

欢迎随便转载。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180301G1K1VV00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券