使用webpack开发以太坊的前端应用

前情提要

使用Truffle开发以太坊智能合约一文中,我们建立了一个最基本的剪刀石头布的以太坊智能合约,并部署到了local的testnet上。

排除这个合约本身的不安全的问题,考虑到用户不可能在truffle的console上敲js代码来执行调用合约,我们还得得建立简单方便的前端应用。

游戏步骤

首先是完成后的简陋的UI的截图。

游戏时候,使用metamask连上local的testnet。然后自己扮演两个角色切换账户即可。

首先两个账户分别转账5wei进行注册。

然后两个用户分别选择是出石头,剪刀,还是布。

最后赢家被记录到LasterWinner这个项目里面。赢家还将获得对方的5wei。

boilerplate

建立一个新的目录。Truffle提供了很方便的已经成型的boilerplate来建立前端的应用。

1
2
3
$ mkdir rps-frontend
$ cd rps-frontend
$ truffle unbox webpack

一阵各种download之后,我们发现当前目录下多出来很多东西。
webpack的boilerplate是一个简单的基于ETH的Token的例子。
其中最主要的是app/这个文件。

1
2
3
4
5
6
app
├── index.html
├── javascripts
│   └── app.js
└── stylesheets
└── app.css

里面的内容由于不是我们需要部署的协议的内容,所以基本上我们都要移除。

首先删掉contracts里面除了Migration.sol以外的所有内容,然后再用这里的文件替换migrations里面的2_deploy_contracts.js的内容。这样理论上我们就可以部署我们的石头剪刀布协议了。

添加前端逻辑

打开app/javascripts/app.js进行修改。
具体可以参考这里, 我们只提几个注意点。

1
2
3
4
5
6
7
8
import "../stylesheets/app.css";

import { default as Web3} from 'web3'; // 用来与eth的节点交互的lib。这里我们需要用这个库来获取用户信息。
import { default as contract } from 'truffle-contract'

import rps_artifacts from '../../build/contracts/rps.json' // 协议的ABI接口。这个是通过truffle migrate之后生成的。

var Rps= contract(rps_artifacts);

也许我们已经习惯使用MetaMask来和基于Ethereum的网页应用交互。下面则是获取默认用户账号信息的代码,已经由这个boilderplate自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
web3.eth.getAccounts(function(err, accs) {
if (err != null) {
alert("There was an error fetching your accounts.");
return;
}

if (accs.length == 0) {
alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.");
return;
}

accounts = accs;
account = accounts[0];

self.refreshWinner();
});

接下来是通过API查询blockchain获取最后赢家的函数。核心的是rps.getLastWinner.call({from: account}), 这和我们在truffle的console的输入一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
refreshWinner: function() {
var self = this;
var rps;
Rps.deployed().then(function(instance) {
rps = instance;
return rps.getLastWinner.call({from: account}); // 注意这里
}).then(function(value) {
var balance_element = document.getElementById("winner");
balance_element.innerHTML = value.valueOf();
}).catch(function(e) {
console.log(e);
self.setStatus("Error getting balance; see log.");
});
},

再然后是注册用户并转5wei的代码。同样核心是rps.register({value: amount, from: account})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

register: function() {
var self = this;
var amount = parseInt(document.getElementById("amount").value);
this.setStatus("Initiating transaction... (please wait)");
var rps;
Rps.deployed().then(function(instance) {
rps= instance;
return rps.register({value: amount, from: account});
}).then(function() {
self.setStatus("Transaction complete!");
self.refreshWinner();
}).catch(function(e) {
console.log(e);
self.setStatus("Error sending coin; see log.");
});
},

最后是玩家执行游戏进行选择的代码。重点还是rps.play(selection, {from: account})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
play: function() {
var self = this;
var selection = document.getElementById("selection").value;
var rps;
Rps.deployed().then(function(instance) {
rps= instance;
return rps.play(selection, {from: account}); //重点
}).then(function() {
self.setStatus("Transaction complete!");
self.refreshWinner();
}).catch(function(e) {
console.log(e);
self.setStatus("Error sending coin; see log.");
});

}

修改UI

接下来就是修改默认的boilerplate的index.html了。
我们得添加上显示最后赢家,输入转账数目,确认注册,选择出拳内容,确认出拳内容的一些东西。
由于boilerplate本来写得比较脏,我也只是适当修改,所以请自己参考下面完整的repo吧。

编译,部署,执行

最后就是打开truffle dev 然后依次compile,migrate --reset了。truffle dev会自动建立local的testrpc的testnet,所以这个时候npm run dev,打开浏览器就可以看到我们上面截图中的应用内容了。

最后的话

使用MetaMask大大方便了用户通过webapplication来执行智能合约。也使得我们可以顾虑很少地开发更加健全的智能合约。试想一下,如果没有MetaMask,你的智能合约就有可能需要用户去输入自己的private key。那你的应用大抵也就没人用了。

这里是整个完整的项目。

以上。

webpack 相关整理

webpack 的必要性

webpack 是一个Assets的打包工具。

通过webpack可以把css,js,json,raw text 等等依赖打包到一个bundle的js中,在html页面中,只需要引用这么一个bundle即可。

webpack的类似工具已经太多,早在Rails红极一时的年代,Sproket就已经具备了很强大的打包功能。

那么使用webpack到底有没有必要呢? 那当然是有必要的,因为很多时候你接手的项目已经倒入了webpack,这个时候,你也只有硬着头皮上了。

webpack 的配置

一般在项目root下面直接运行不带参数的webpack命令,webpack会自动寻找该目录下的config文件。 随便找了个网上的最简单的例子来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

module.exports = {
context: __dirname + '/src',

entry: {
js: "./js/entry.js"
},

output: {
path: __dirname + '/dist',
filename: "./js/app.js"
},

module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
}
]
}
}

entry

总的文件js的入口。在这个文件里可以引用下面通过module loader 导入进来的模块。
比如

1
2
var my_style= require("mystyle.css")
var my_data = require("mydata.json")

output

这个就是最后生成的bundle文件。

modules loaders

通过loaders可以把任何assets通过上面类似require('xxx')的形式导入到entry文件中。

loaders 是一个数组,可以针对任何文件形式进行不同处理。比如上面的例子。

1
2
3
4
5
6
7
8
{
test: /\.js$/, // 所有符合条件的js文件
exclude: /node_modules/, // node_modules里面的js文件除外
loader: 'babel-loader', // 使用babel-loader处理这些js文件
query: {
presets: ['es2015'] // 处理时候带参数es2015,告诉babel使用es2015的preset进行编译。
}
}

loaders 可以拼接在一起形成pipeline。 比如

1
2
3
4
5
module: {
loaders: [{
test: /\.css$/,
loader: ‘style!css’ <--(style-loader!css-loader的省略表现)
}]

Plugin

除了module loader,我们还可以添加各种plugin。
plugin用于在我们生成bundle之后来对生成的bundle进行处理。比如下面的例子,我们对生成的bundle文件使用Uglify进行压缩。

1
2
3
4
5
6
7
8
9
10
11
12
//↑↑↑↑省略
output: {
path: __dirname + '/dist',
filename: "./js/app.js"
},

//↓↓↓↓追加
plugins: [
new webpack.optimize.UglifyJsPlugin()
],

//↓↓↓↓省略

除此之外我们还可以引入html模版的plugin,最后生成我们想要的index.html。所有的module loaders和 plugins都可以在文档的List的相关页面里找到。

webpack-dev server

webpack自带dev server。我们也可以在配置文件中进行相关配置。

1
2
3
4
devServer: {
contentBase: 'dist',
port: 3000
},

最后

使用webpack使得任何assets都可以模块化,管理和打包发布变得十分方便。但是有些时候,有些内容需要频繁变更的模块,就不太适合放进去打包到一起了。也许使用经验较深的enginner知道该怎么做吧。