可以简单的把以太坊虚拟机理解为一个公开使用的后端服务器,用户在其上拥有有限的操作空间,每次操作以交易的形式产生,并会根据修改数据的大小向用户收费,称为 Gas,以此支持服务器维护人员 (即矿工)。并且,服务器上的数据会被永久记录,操作后端数据的代码 (对于EVM,是 Solidity 代码) 也存放在服务器上不能被修改。
矿工可以根据 Gas 和数据操作量选择将哪一笔交易加入区块。因此 Solidity 编程要求存储尽量少的数据,节省空间和操作量,以此得到更快速的响应。
使用 Remix 和 Ganache
Remix 是以太坊官方提供的 Solidity 开发环境。在 Remix 内写好代码后可以编译,编译会得到程序的二进制程序接口,称为 ABI。ABI 会提供给 web3.js 来提供操作接口。在编译后可以部署合约,Remix 本身提供了一些 JavaScript 虚拟机来运行智能合约。为了观察到用户和交易的情况,我们可以部署私有链,利用 Ganache。部署后会得到合约地址,你也可以直接在 Remix 和合约交互。
Ganache 可以在本地设置好私有区块链,即开即用。用户可以自行设置端口号,而网络ID为5777而区块链ID为1337。在本地部署好之后,使用 MetaMask 钱包添加私有链。MetaMask 也可以使用其他测试链,虽然其他测试链可以有更多的用户,但在这些链上我们难以观察交易情况。添加私有链后,也记得添加私有链的账户。
说到账户,对于以太坊地址来说,所有链上的地址格式都完全一致。因此,你的任何一个账户都可以在任何链上使用(当然代币不会跟随)。可以尝试把 Ganache 里生成的账户的钱转入一个你主链的地址,操作将会成功且主链地址在私有链上的 ETH 会增加。注意 Ganache 不会追踪除了它生成的账户以外的账户(因为无穷多),但是你仍然可以看到 TX 信息,就在 Ganache 的操作面板。这到底是怎么做到的?得益于钱包地址的随机和海量,生成的两个地址相同的概率几乎是不可能的。
Solidity 代码
设计一个 Solidity 代码,对于每个账户存放一个数据,并且允许用户查看数据。
(很遗憾,Hexo 对于 Solidity 代码暂时没有语法高亮,我也不想使用 hightlight.js,所以我随便选用了一种代码高亮模式。)
1 | // 声明 License |
具体内容可以查看 Solidity 的文档。函数发射的事件需要在前端进行监听。关键字 view
代表了这个函数不会操作数据。全局变量 msg
存储了一些当前合约的信息,例如 msg.sender
就是调用合约的地址。
Web 代码
Web 端我们使用以太坊 JavaScript API和后端交互,叫做 web3.js,使用此库类似于使用 jQuery 的 ajax。实际上,包括部署合约在内的操作都可以用 web3.js 完成,但本次我们不这样做。
在网页端,首先用简单 HTML 完成界面,给用户提示信息,显示一个可变文本并设置两个按钮:
1 | <div class="container"> |
接下来在 <script>
部分设计网页的逻辑。别忘记把 web3.js 和 jQuery 加入到网页中。首先配置好我们的 web3 库:
1 | if (typeof web3 !== 'undefined') { |
这声明了,我们的 web3 是由 HTTP 在本地8545端口提供,也就是我们的 Ganache。接下来:
1 | web3.eth.defaultAccount = web3.eth.accounts[0]; |
第一行代码说明了我们默认交互账户是 Ganache 里的第一个账户。我们现在还不将 MetaMask 链接功能提供在前端。MetaMask 要求前端网页不能是本地打开的,必须运行在一个服务器上 (Python 搭建的简易 HTTP 服务器就可以),并且需要更复杂且完善的代码。第二行代码需要我们写入之前在 Remix 得到的 ABI,这是我们交互的基础。第三行就是合约本身,从语义上可以理解为:某个 ABI 位于某个确定的地址上。这个地址可以在 Remix 处看到,或在 Ganache 的 TX 信息里找到。
接下来我们来看一下在前端如何调用 Solidity 函数,并用 jQuery 改变前端显示。首先演示一下按钮如何获得数据:
1 | $("#button_update").click(function() { |
对于 jQuery,简单的语法是利用 $("#id")
选中 HTML 对应 ID 的内容,在此是按钮类型。按钮类型具有 click()
方法,我们在内部写出一个函数,意思是“如果按钮被按下,那么执行这个函数,函数的内容是使用合约内的 setNumber() 函数,函数的参数是 HTML 中 ID 为 data 的元素的值”。对于下面获得值的操作,同理。
再来看看怎么监听我们发出的事件:
1 | var eventSet = myContract.numberSetted() |
基本原理同以上相同,我们将合约的 numberSetted 事件声明成一个变量,对这个变量进行 watch() 方法。里面的函数和刚才的差不多,它具有了两个参数。因为 web3 要求我们进行异常处理,即需要有 if(!error) {} elese {}
的形式。另一个参数 result
里面包含了我们在 Solidity 中写过的两个返回值。如果不太明白,可以在浏览器打开控制台观察一下 log
,会完整展现 result
的结构。

总结
目前我们的前端已经可以在本地运行了。不过像这样直接链接私有链,利用索引选择调用合约的用户以及函数的处理方式都非常原始,只能作演示用。之后会尽量设计更完善的代码。