[教程] 使用 Embark 开发投票 DApp

这是一篇Embark使用教程:通过本文可以学习到:1. 使用 Embark 创建项目 2. 利用 EmbarkJS 与合约交互 3. Embark 如果部署合约到主网(利用Infura节点)

前面我们基于[Embark Demo](https://learnblockchain.cn/article/566) 介绍了 Embark 框架,今天使用 Embark 来实实在在开发一个 DApp: 从零开发开发一个投票DApp。 > 之前我们也使用[Truffle 开发过投票DApp](https://learnblockchain.cn/2019/04/10/election-dapp),大家可以自行对比两个框架的优劣。 通过本文可以学习到: 1. 使用 Embark 创建项目 2. 利用 EmbarkJS 与合约交互 3. Embark 如果部署合约到主网(利用Infura节点) > 本文使用的 Embark 版本是 5.2.3 ## 创建Embark项目 ``` > embark new embark-election ``` 会在当前目录下生成一个 embark-election 目录,并创建好了相应的项目框架文件:如: `app/`、`contracts/`、`config/`、`embark.json`等。 我们需要在对应的目录中,添加相应的实现。 ## 编写合约 在`contracts/`中添加合约Election.sol: ```js pragma solidity ^0.6.0; contract Election { // Model a Candidate struct Candidate { uint id; string name; uint voteCount; } mapping(address => bool) public voters; mapping(uint => Candidate) public candidates; // Store Candidates Count uint public candidatesCount; // voted event event votedEvent ( uint indexed _candidateId ); constructor () public { addCandidate("Tiny 熊"); addCandidate("LearnBlockChain.cn"); } function addCandidate (string memory _name) private { candidatesCount ++; candidates[candidatesCount] = Candidate(candidatesCount, _name, 0); } function vote (uint _candidateId) public { // require that they haven't voted before require(!voters[msg.sender]); // require a valid candidate require(_candidateId > 0 && _candidateId <= candidatesCount); // record that voter has voted voters[msg.sender] = true; // update candidate vote Count candidates[_candidateId].voteCount ++; // trigger voted event emit votedEvent(_candidateId); } } ``` 之前有使用过[Truffle开发过投票DApp](https://learnblockchain.cn/2019/04/10/election-dapp),合约的代码完全一样,就不在解释。 ## Embark 合约编译部署 Embark 合约部署的配置在 `config/contracts.js`, 在 `deploy` 字段加入 Election 合约: ```json deploy: { Election: { } } ``` 现在运行 `embark run` , Embark 会自动编译及部署`Election.sol`到 `config/blockchain.js` 配置的 `development` 网络。 > `embark run` 等价 `embark run development` 。 `blockchain.js` 中 `development` 网络是使用 ganache-cli 启动的网络,其配置如下: ``` development: { client: 'ganache-cli', clientConfig: { miningMode: 'dev' } } ``` embark启动后,我们可以在 COCKPIT 或 DashBoard 看到`Election.sol`合约的部署日志,大概类似下面: ``` deploying Election with 351122 gas at the price of 1 Wei, estimated cost: 351122 Wei (txHash: 0x9da4dfb951149...d5c306dcabf300a4) Election deployed at 0x10C257c76Cd3Dc35cA2618c6137658bFD1fFCcfA using 346374 gas (txHash: 0x9da4dfb951149ea4...d5c306dcabf300a4) finished deploying contracts ``` ## 编写前端代码 ### Embark Artifacts Embark提供了一个 EmbarkJS的JavaScript库,来帮助开发者和合约进行交互。 在使用web3.js 时,和合约交互需要知道合约的ABI及地址来创建JS环境中对应的合约对象,一般代码是这样的: ``` // 需要ABI及地址创建对象 var myContract = new web3.eth.Contract([...ABI...], '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'); ``` Embark 在编译部署后,每个合约会生成一个对应的`构件Artifact`(可以在`embarkArtifacts/contracts/` 目录下找的这些文件),我们可以直接使用 `Artifact` 生成的合约对象调用合约。 一个构件通常会包含:合约的ABI、部署地址、启动代码及其他的配置数据。 查看一下`Election.sol` 对应的构件`Election.js` 代码就更容易理解: ```js import EmbarkJS from '../embarkjs'; let ElectionJSONConfig = {"contract_name":"Election","address":"0x10C257c76Cd3Dc35cA2618c6137658bFD1fFCcfA","code":"...", ... ,"abiDefinition":[...]}; let Election = new EmbarkJS.Blockchain.Contract(ElectionJSONConfig); export default Election; ``` `Election.js` 最后一行导出了一个与合约同名的JavaScript 对象,下面看看怎么使用这个对象。 ### 修改前端index.html 在使用`embark new embark-election`创建项目时, 前端目录`app/`下生成了一个 `index.html`: ```html <html> <head> <title>Embark</title> <link rel="stylesheet" href="css/app.css"> <script src="js/app.js"></script> </head> <body> <h3>Welcome to Embark!</h3> </body> </html> ``` 这里有一个地方**需要注意一下**,第5 6行引入了 `css/app.css`, `js/app.js`,而其实`app/`下并没有这两个文件,这两个文件其实是按照 `embark.json` 配置的规程生成的。 `embark.json` 关于前端的配置如下: ``` "app": { "css/app.css": ["app/css/**"], "js/app.js": ["app/js/index.js"], "images/": ["app/images/**"], "index.html": "app/index.html" }, ``` `"css/app.css": ["app/css/**"]` 表示所有在`app/css/`目录下的文件会被压缩到 dist目录的 `css/app.css` ,`app/js/index.js`则会编译为`js/app.js`,其他的配置类似。 > 我猜测embark 这样统一 css 及 js代码,可能是为了在IPFS之类的去中心化存储上访问起来更方便,在IPFS上传整个目录时,只能以相对路径去访问资源。欢迎留言和我交流。 接下来修改前端部分的代码,主要是在`index.html`的`body`加入一个`table`显示候选人,以及加入一个投票框,代码如下(节选): ```html <table class="table"> <thead> <tr> <th scope="col">#</th> <th scope="col">候选人</th> <th scope="col">得票数</th> </tr> </thead> <tbody id="candidatesResults"> </tbody> </table> <div class="form-group"> <label for="candidatesSelect">选择候选人</label> <select class="form-control" id="candidatesSelect"> </select> </div> ``` 前端,我们使用了 bootstrap css ,把文件拷贝到`app/css`目录下,接下来,看看关键的一步:前端如何与与合约交互。 ### 使用 Artifacts与合约交互 #### EmbarkJS 连接 Web3 创建项目时生成的`app/js/index.js` 生成了如下代码: ```js import EmbarkJS from 'Embark/EmbarkJS'; EmbarkJS.onReady((err) => { // You can execute contract calls after the connection }); ``` 这段代码里,EmbarkJS为我们准备了一个onReady回调函数,这是因为**EmbarkJS会自动帮我们完成与web3节点的连接与初始化**,当这些就绪后(调用onReady),前端就可以和链进行交互了。 大家也许会好奇EmbarkJS怎么知道我们需要连接那个节点呢?其实在`config/contracts.js` 有一个 `dappConnection` 配置项: ``` dappConnection: [ "$EMBARK", "$WEB3", // 使用浏览器注入的web3, 如MetaMask等 "ws://localhost:8546", "http://localhost:8545" ], ``` `$EMBARK` : 是Embark在DApp和节点之前实现的一个代理,使用`$EMBARK`有几个好处: 1. 可以在`config/blockchain.js` 配置于DApp交互的账号 `accounts`。 2. 可以更友好的的看到交易记录。 EmbarkJS 会从上到下,依次尝试 `dappConnection`提供的连接,如果有一个可以连接上,就会停止尝试。 #### 获取合约数据渲染界面 当 EmbarkJS 环境准备 onReady后,就可以使用构件`Election.js`获取合约数据,如获取调用合约获取候选人数量: ```js import EmbarkJS from 'Embark/EmbarkJS'; import Election from '../../embarkArtifacts/contracts/Election.js'; EmbarkJS.onReady((err) => { Election.methods.candidatesCount().call().then(count => console.log(" candidatesCount: " + count); ); }); ``` 代码中直接使用构件导出的`Election`对象,调用合约方法 `Election.methods.candidatesCount().call()`, 调用合约方法与web3.js 一致。 了解了如何与合约交互,接下来渲染界面就简单了,我们把代码整理下,分别定义3个函数: `App.getAccount()`、`App.render()`、`App.onVote()` 来获取当前账号(需要用来判断哪些账号投过票)、界面渲染、处理点击投标。 ```js EmbarkJS.onReady((err) => { App.getAccount(); App.render(); App.onVote(); }); ``` `App.getAccount()` 的实现如下: ```js import "./jquery.min.js" var App = { account: null, getAccount: function() { web3.eth.getCoinbase(function(err, account) { if (err === null) { App.account = account; console.log(account); $("#accountAddress").html("Your Account: " + account); } }) }, } ``` 在代码中,我们直接使用了web3对象,就是因为EmbarkJS帮我们进行了web3的初始化。 另外,我们引入`jquery.min.js` 来进行UI界面的渲染。 `App.render()` 的实现(主干)如下: ```js render: function () { Election.methods.candidatesCount().call().then( candidatesCount => { var candidatesResults = $("#candidatesResults"); var candidatesSelect = $('#candidatesSelect'); for (var i = 1; i <= candidatesCount; i++) { Election.methods.candidates(i).call().then(function(candidate) { var id = candidate[0]; var name = candidate[1]; var voteCount = candidate[2]; // Render candidate Result var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"; candidatesResults.append(candidateTemplate); // Render candidate ballot option var candidateOption = "<option value='" + id + "' >" + name + "</ option>"; candidatesSelect.append(candidateOption); }); } }); } ``` `App.onVote()` 的实现(主干)如下: ```js onVote: function() { $("#vote").click(function(e){ var candidateId = $('#candidatesSelect').val(); Election.methods.vote(candidateId).send() .then(function(result) { App.render(); }).catch(function(err) { console.error(err); }); }); } ``` ## 部署 使用 `embark run` 时,会为我们启动一个Geth 或 `ganache-cli` 的本地网络部署合约,以及在`8000`端口上启用一个本地服务器来部署前端应用,我们在浏览器输入`http://localhost:8000/` 就可以看到DApp界面,如图: ![](https://img.learnblockchain.cn/2020/02/DE84283FF0.jpg) 当我们的DApp 在测试环境通过后,就可以部署到以太坊的主网。 ## 利用Infura部署到主网 要部署到主网,需要在`blockchain.js` 中添加一个主网网络,这里以测试网Ropsten网络为例: ``` ropsten: { endpoint: "https://ropsten.infura.io/v3/d3fe47c...4f", accounts: [ { mnemonic: " 你的助记词 ", hdpath: "m/44'/60'/0'/0/", numAddresses: "1" } ] } ``` 如果我们没有自己的主网节点,可以使用 endpoint 来指向以个外部节点,最常用的就是[Infura](https://infura.io/)。 添加好配置之后,使用`build`命令来构建主网发布版本: ``` embark build ropsten # 最后是网络参数 ``` 所有的文件在生成在`dist`目录下,把他们部署到线上服务器就完成了部署。 也可以使用`embark upload ropsten` 上传到IPFS。 本文配套代码已经到[集市](https://learnblockchain.cn/goods/11),可前往下载。

前面我们基于Embark Demo 介绍了 Embark 框架,今天使用 Embark 来实实在在开发一个 DApp: 从零开发开发一个投票DApp。

之前我们也使用Truffle 开发过投票DApp,大家可以自行对比两个框架的优劣。

通过本文可以学习到:

  1. 使用 Embark 创建项目
  2. 利用 EmbarkJS 与合约交互
  3. Embark 如果部署合约到主网(利用Infura节点)

    本文使用的 Embark 版本是 5.2.3

创建Embark项目

> embark new embark-election

会在当前目录下生成一个 embark-election 目录,并创建好了相应的项目框架文件:如: app/contracts/config/embark.json等。

我们需要在对应的目录中,添加相应的实现。

编写合约

contracts/中添加合约Election.sol:

pragma solidity ^0.6.0;

contract Election {
    // Model a Candidate
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    mapping(address => bool) public voters;

    mapping(uint => Candidate) public candidates;
    // Store Candidates Count
    uint public candidatesCount;

    // voted event
    event votedEvent (
        uint indexed _candidateId
    );

    constructor () public {
        addCandidate("Tiny 熊");
        addCandidate("LearnBlockChain.cn");
    }

    function addCandidate (string memory _name) private {
        candidatesCount ++;
        candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
    }

    function vote (uint _candidateId) public {
        // require that they haven't voted before
        require(!voters[msg.sender]);

        // require a valid candidate
        require(_candidateId > 0 && _candidateId &lt;= candidatesCount);

        // record that voter has voted
        voters[msg.sender] = true;

        // update candidate vote Count
        candidates[_candidateId].voteCount ++;

        // trigger voted event
        emit votedEvent(_candidateId);
    }
}

之前有使用过Truffle开发过投票DApp,合约的代码完全一样,就不在解释。

Embark 合约编译部署

Embark 合约部署的配置在 config/contracts.js, 在 deploy 字段加入 Election 合约:

    deploy: {
      Election: {
      }
    }

现在运行 embark run , Embark 会自动编译及部署Election.solconfig/blockchain.js 配置的 development 网络。

embark run 等价 embark run development

blockchain.jsdevelopment 网络是使用 ganache-cli 启动的网络,其配置如下:

  development: {
    client: 'ganache-cli',
    clientConfig: {
      miningMode: 'dev' 
    }
  }

embark启动后,我们可以在 COCKPIT 或 DashBoard 看到Election.sol合约的部署日志,大概类似下面:

deploying Election with 351122 gas at the price of 1 Wei, estimated cost: 351122 Wei (txHash: 0x9da4dfb951149...d5c306dcabf300a4)
Election deployed at 0x10C257c76Cd3Dc35cA2618c6137658bFD1fFCcfA using 346374 gas (txHash: 0x9da4dfb951149ea4...d5c306dcabf300a4)
finished deploying contracts

编写前端代码

Embark Artifacts

Embark提供了一个 EmbarkJS的JavaScript库,来帮助开发者和合约进行交互。

在使用web3.js 时,和合约交互需要知道合约的ABI及地址来创建JS环境中对应的合约对象,一般代码是这样的:

// 需要ABI及地址创建对象
var myContract = new web3.eth.Contract([...ABI...], '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe');

Embark 在编译部署后,每个合约会生成一个对应的构件Artifact(可以在embarkArtifacts/contracts/ 目录下找的这些文件),我们可以直接使用 Artifact 生成的合约对象调用合约。

一个构件通常会包含:合约的ABI、部署地址、启动代码及其他的配置数据。

查看一下Election.sol 对应的构件Election.js 代码就更容易理解:

import EmbarkJS from '../embarkjs';

let ElectionJSONConfig = {"contract_name":"Election","address":"0x10C257c76Cd3Dc35cA2618c6137658bFD1fFCcfA","code":"...", ... ,"abiDefinition":[...]};

let Election = new EmbarkJS.Blockchain.Contract(ElectionJSONConfig);

export default Election;

Election.js 最后一行导出了一个与合约同名的JavaScript 对象,下面看看怎么使用这个对象。

修改前端index.html

在使用embark new embark-election创建项目时, 前端目录app/下生成了一个 index.html

&lt;html>
  &lt;head>
    &lt;title>Embark&lt;/title>
    &lt;link rel="stylesheet" href="css/app.css">  
    &lt;script src="js/app.js">&lt;/script>
  &lt;/head>
  &lt;body>
    &lt;h3>Welcome to Embark!&lt;/h3>
  &lt;/body>
&lt;/html>

这里有一个地方需要注意一下,第5 6行引入了 css/app.css, js/app.js,而其实app/下并没有这两个文件,这两个文件其实是按照 embark.json 配置的规程生成的。

embark.json 关于前端的配置如下:

  "app": {
    "css/app.css": ["app/css/**"],
    "js/app.js": ["app/js/index.js"],
    "images/": ["app/images/**"],
    "index.html": "app/index.html"
  },

"css/app.css": ["app/css/**"] 表示所有在app/css/目录下的文件会被压缩到 dist目录的 css/app.cssapp/js/index.js则会编译为js/app.js,其他的配置类似。

我猜测embark 这样统一 css 及 js代码,可能是为了在IPFS之类的去中心化存储上访问起来更方便,在IPFS上传整个目录时,只能以相对路径去访问资源。欢迎留言和我交流。

接下来修改前端部分的代码,主要是在index.htmlbody加入一个table显示候选人,以及加入一个投票框,代码如下(节选):

&lt;table class="table">
  &lt;thead>
    &lt;tr>
      &lt;th scope="col">#&lt;/th>
      &lt;th scope="col">候选人&lt;/th>
      &lt;th scope="col">得票数&lt;/th>
    &lt;/tr>
  &lt;/thead>
  &lt;tbody id="candidatesResults">
  &lt;/tbody>
&lt;/table>

&lt;div class="form-group">
&lt;label for="candidatesSelect">选择候选人&lt;/label>
&lt;select class="form-control" id="candidatesSelect">
&lt;/select>
&lt;/div>

前端,我们使用了 bootstrap css ,把文件拷贝到app/css目录下,接下来,看看关键的一步:前端如何与与合约交互。

使用 Artifacts与合约交互

EmbarkJS 连接 Web3

创建项目时生成的app/js/index.js 生成了如下代码:

import EmbarkJS from 'Embark/EmbarkJS';

EmbarkJS.onReady((err) => {
  // You can execute contract calls after the connection
});

这段代码里,EmbarkJS为我们准备了一个onReady回调函数,这是因为EmbarkJS会自动帮我们完成与web3节点的连接与初始化,当这些就绪后(调用onReady),前端就可以和链进行交互了。

大家也许会好奇EmbarkJS怎么知道我们需要连接那个节点呢?其实在config/contracts.js 有一个 dappConnection 配置项:

dappConnection: [
  "$EMBARK",
  "$WEB3",  // 使用浏览器注入的web3, 如MetaMask等
  "ws://localhost:8546",
  "http://localhost:8545"
],

$EMBARK : 是Embark在DApp和节点之前实现的一个代理,使用$EMBARK有几个好处:

  1. 可以在config/blockchain.js 配置于DApp交互的账号 accounts
  2. 可以更友好的的看到交易记录。

EmbarkJS 会从上到下,依次尝试 dappConnection提供的连接,如果有一个可以连接上,就会停止尝试。

获取合约数据渲染界面

当 EmbarkJS 环境准备 onReady后,就可以使用构件Election.js获取合约数据,如获取调用合约获取候选人数量:

import EmbarkJS from 'Embark/EmbarkJS';
import Election from '../../embarkArtifacts/contracts/Election.js';

EmbarkJS.onReady((err) => {
    Election.methods.candidatesCount().call().then(count => console.log(" candidatesCount: " + count);
    );
});

代码中直接使用构件导出的Election对象,调用合约方法 Election.methods.candidatesCount().call(), 调用合约方法与web3.js 一致。

了解了如何与合约交互,接下来渲染界面就简单了,我们把代码整理下,分别定义3个函数: App.getAccount()App.render()App.onVote() 来获取当前账号(需要用来判断哪些账号投过票)、界面渲染、处理点击投标。

EmbarkJS.onReady((err) => {
  App.getAccount();
  App.render();
  App.onVote();
});

App.getAccount() 的实现如下:

import "./jquery.min.js"

var App = {
  account: null,

  getAccount: function() {
    web3.eth.getCoinbase(function(err, account) {
      if (err === null) {
        App.account = account;
        console.log(account);
        $("#accountAddress").html("Your Account: " + account);
      }
    })
  },  
}

在代码中,我们直接使用了web3对象,就是因为EmbarkJS帮我们进行了web3的初始化。 另外,我们引入jquery.min.js 来进行UI界面的渲染。

App.render() 的实现(主干)如下:

render: function () {
    Election.methods.candidatesCount().call().then(
      candidatesCount => 
      {
        var candidatesResults = $("#candidatesResults");
        var candidatesSelect = $('#candidatesSelect');

        for (var i = 1; i &lt;= candidatesCount; i++) {
          Election.methods.candidates(i).call().then(function(candidate) {

            var id = candidate[0];
            var name = candidate[1];
            var voteCount = candidate[2];

            // Render candidate Result
            var candidateTemplate = "&lt;tr>&lt;th>" + id + "&lt;/th>&lt;td>" + name + "&lt;/td>&lt;td>" + voteCount + "&lt;/td>&lt;/tr>";
            candidatesResults.append(candidateTemplate);

            // Render candidate ballot option
            var candidateOption = "&lt;option value='" + id + "' >" + name + "&lt;/ option>";
            candidatesSelect.append(candidateOption);
          });
        }
      });
  }

App.onVote() 的实现(主干)如下:

  onVote: function() {
    $("#vote").click(function(e){
      var candidateId = $('#candidatesSelect').val();
      Election.methods.vote(candidateId).send()
      .then(function(result) {
        App.render();
      }).catch(function(err) {
        console.error(err);
      });
    });
  }

部署

使用 embark run 时,会为我们启动一个Geth 或 ganache-cli 的本地网络部署合约,以及在8000端口上启用一个本地服务器来部署前端应用,我们在浏览器输入http://localhost:8000/ 就可以看到DApp界面,如图:

[教程] 使用 Embark 开发投票 DApp插图

当我们的DApp 在测试环境通过后,就可以部署到以太坊的主网。

利用Infura部署到主网

要部署到主网,需要在blockchain.js 中添加一个主网网络,这里以测试网Ropsten网络为例:

ropsten: {
    endpoint: "https://ropsten.infura.io/v3/d3fe47c...4f",
    accounts: [
      {
        mnemonic: " 你的助记词 ",
        hdpath: "m/44'/60'/0'/0/",
        numAddresses: "1"
      }
    ]
  }

如果我们没有自己的主网节点,可以使用 endpoint 来指向以个外部节点,最常用的就是Infura。

添加好配置之后,使用build命令来构建主网发布版本:

embark build ropsten  # 最后是网络参数

所有的文件在生成在dist目录下,把他们部署到线上服务器就完成了部署。 也可以使用embark upload ropsten 上传到IPFS。

本文配套代码已经到集市,可前往下载。

区块链技术网。

  • 发表于 2020-02-26 23:20
  • 阅读 ( 1670 )
  • 学分 ( 157 )
  • 分类:DApp

评论