1. ES6

前言

這個示例是《區塊鏈技術進階與實踐》提供的例子,書不錯。

前提:在學習這個例子前要安裝好ganache、truffle等區塊鏈開發環境,對智慧合約基本語法和DAPP如何開發有基本概念。

執行環境: ganache-cli v6.1.6    truffle v4.1.13  solidity ^0.4.24

因為原始的原始碼可能過時,編譯會報錯,我自己進行過修改。

注意:下載之後如果直接truffle migrate會報錯

把build資料夾刪掉,再在本地重新進行編譯部署即可。

附註:這個專案基於truffle的例子webpack,可以新建一個資料夾,cd到資料夾內,在命令列中輸入 truffle unbox webpack,下載並學習這個例子,與本積分例子對比。例子中的前臺JavaScript程式碼很值得學習。

一、背景及基礎知識(只需瞭解即可,也可不看)

1. ES6

ECMAScript 6.0(以下簡稱 ES6)是 JavaScript 語言的下一代標準。

ECMAScript 和 JavaScript 的關係是,前者是後者的規格,後者是前者的一種實現。

ES6 既是一個歷史名詞,也是一個泛指,含義是 5.1 版以後的 JavaScript 的下一代標準,涵蓋了 ES2015、ES2016、ES2017 等等

2. babel轉碼器

Babel 是一個廣泛使用的 ES6 轉碼器,可以將 ES6 程式碼轉為 ES5 程式碼,從而在現有環境執行。這意味著,你可以用 ES6 的方式編寫程式,又不用擔心現有環境是否支援。

Babel 的配置檔案是.babelrc,存放在專案的根目錄下。使用 Babel 的第一步,就是配置這個檔案。

3. ESLint

二、專案需求及專案結構圖

      這個系統有客戶、銀行、商戶組成,進行積分的交易,具體需求如下圖:

三、原始碼分析

1. 檔案簡要解釋(弄懂每個檔案大概做什麼的即可)

用sublime text開啟資料夾,可以看到組織目錄

app是前端html程式碼和一些指令碼程式

    stylesheets裡面是app.css,設定網頁各個部件的樣式

    html是前臺程式碼,index.html裡面有客戶註冊、登陸、商戶註冊、登陸、銀行登陸和測試功能。

    客戶登陸後頁面跳轉到customer.html,頁面連結中有當前登陸賬戶的地址,可以實現查詢資訊、購買商品、轉讓積分等功能

    商戶登陸後頁面跳轉到merchant.html,頁面連結中有當前登陸賬戶的地址,可以實現查詢資訊、新增商品、轉讓積分、積分清算等功能

    銀行登陸後頁面跳轉到bank.html,頁面連結中有當前登陸賬戶的地址,可以實現發行積分、檢視資訊等功能

    JavaScript程式碼的解釋見程式碼註釋,主要用到web3來與節點通訊,獲取節點的賬戶列表等資訊,用到truffle-constract 來生成合約例項,呼叫合約的方法。這兩個js庫都可以在專案目錄下 npm install 進行下載,會根據package.json確定版本

build 是在compile之後生成的資料夾

contracts、migrations、test 是truffle自動生成的資料夾

node_modules是前端需要的一些依賴檔案,在下載web3.js之後有的

以點號開頭的檔案都是用於JavaScript的轉碼,可以暫時不管

package.json 前端開發,測試需要依賴的一些庫的配置資訊,這個檔案是 npm init 時建立的一個檔案,會記錄當前整個專案中的一些基礎資訊。package.json裡面定義的是版本範圍(比如^1.0.0),具體跑npm install的時候安的什麼版本,要解析後才能決定,這裡面定義的依賴關係樹,可以稱之為邏輯樹(logical tree)。

package-lock.json 這個檔案卻是 node_modules 資料夾或者 package.json 檔案發生變化時自動生成的。這個檔案主要功能是確定當前安裝的包的依賴,以便後續重新安裝的時候生成相同的依賴,而忽略專案開發過程中有些依賴已經發生的更新。

node_modules資料夾下才是npm實際安裝的確定版本的東西,這裡面的資料夾結構我們可以稱之為物理樹(physical tree)。

安裝過程中有一些去重演算法,所以你會發現邏輯樹結構和物理樹結構不完全一樣。

package-lock.json可以理解成對結合了邏輯樹和物理樹的一個快照(snapshot),裡面有明確的各依賴版本號,實際安裝的結構,也有邏輯樹的結構。

其最大的好處就是能獲得可重複的構建(repeatable build),當你在CI(持續整合)上重複build的時候,得到的artifact是一樣的,因為依賴的版本都被鎖住了。

 連結:https://www.zhihu.com/question/62331583/answer/257972185

truffle.js如下:

// Allows us to use ES6 in our migrations and tests.
require('babel-register')

module.exports = {
  networks: {
    truffle: {
      host: '127.0.0.1',
      port: 9545,
      network_id: '*' // Match any network id
    },
    develop: {
      host: '127.0.0.1',
      port: 8545,
      network_id: '*' // Match any network id
    }
  }
}
  • require: 用於載入babel模組,實現JavaScript的轉碼,以相容。
  • networks: 指定在移植(Migration)時使用哪個網路。當在某個特定的網路上編譯或執行移植時,合約會快取起來方便後續使用。這裡開發使用的是本地的ganache-cli客戶端。

webpack.config.js

webpack在執行時,除在命令列傳入引數,還可以通過指定的配置檔案來執行。預設會搜尋當前目錄下webpack.config.js。這個檔案是一個node.js模組,返回一個json格式的配置物件,或者通過--config選項來指定配置檔案。

2. app.js檔案註釋(重點)

// Import the page's CSS. Webpack will know what to do with it.
import '../stylesheets/app.css'

//在node中使用babel支援ES6,也僅僅是將ES6轉碼為ES5再執行,import語法會被轉碼為require

/*引入customer.js等模組,以使用其中的方法
 require相當於module.exports的傳送門,module.exports後面的內容是什麼,require的結果就是什麼
 customer.js等模組中exports的都是方法,可以在這裡被呼叫,如 customer.newCustomer(ScoreInstance, account) */
const customer = require('./customer')
const bank = require('./bank')
const merchant = require('./merchant')

// Import libraries we need.
/* import是編譯時的(require是執行時的),它必須放在檔案開頭,而且使用格式也是確定的。
 它不會將整個模組執行後賦值給某個變數,而是隻選擇import的介面進行編譯,這樣在效能上比require好很多。*/
import { default as Web3 } from 'web3'      //等同於 import Web3 from 'web3';
import { default as contract } from 'truffle-contract'    //等同於 import contract from 'truffle-contract';

// Import our contract artifacts and turn them into usable abstractions. 
//匯入智慧合約編譯之後生成的abi的json檔案 Score.json
import ScoreArtifacts from '../../build/contracts/Score'

// ScoreContract is our usable abstraction, which we'll use through the code below. 
//初始化智慧合約,實際上就是為你的智慧合約建立一個對應的js物件ScoreContract,方便後續呼叫
//The input to the contract function is a JSON blob defined by truffle-contract-schema. 
let ScoreContract = contract(ScoreArtifacts)
let ScoreInstance
// The following code is simple to show off interacting with your contracts.
// As your needs grow you will likely need to change its form and structure.
// For application bootstrapping, check out window.addEventListener below.
let accounts
let account

window.App = {
  // 進行初始化
  init: function () {
    // 設定智慧合約的web3連線,使其能與以太坊節點通訊,truffle-contract中的方法
    ScoreContract.setProvider(window.web3.currentProvider)
    // 類似於geth客戶端查詢以太坊賬戶資訊,Get the initial account balance so it can be displayed.
    // web3.eth.getAccounts(callback(error, result){ ... }) 
    window.web3.eth.getAccounts(function (err, accs) {
      if (err != null) {        //查詢有問題
        window.App.setStatus('There was an error fetching your accounts.')
        return
      }

      if (accs.length === 0) {  //沒有以太坊賬戶
        window.App.setStatus('Couldn\'t get any accounts! Make sure your Ethereum client is configured correctly.')
        return
      }
      accounts = accs           //把獲取的以太坊賬戶全部存在變數accounts中,連的是ganache-cli的rpc模擬服務,預製了幾個有eth的帳號
                                //如果安裝了MetaMask外掛,應該獲得的就是MetaMask裡的帳號
      account = accounts[0]     //第一個以太坊賬戶存在account中
    })

    //Create an instance of ScoreContract that represents the default address managed by ScoreContract.
    //Creates an instance of the contract abstraction representing the contract at its deployed address. 
    // truffle-contract中的方法
    ScoreContract.deployed().then(function (instance) {
      ScoreInstance = instance  //獲取合約的例項,不用重複deploy,都用這一個
    }).catch(function (e) {
      console.log(e, null)
    })
  },
  //===================================== 客戶方法 ==========================================
  // 新建客戶
  newCustomer: function () {
    customer.newCustomer(ScoreInstance, account)
  },
  // 客戶登入
  customerLogin: function () {
    customer.customerLogin(ScoreInstance, account)
  },
  // 當前客戶資訊,currentAccount在url中擷取,在status框顯示當前登陸賬戶
  getCurrentCustomer: function (currentAccount) {
    customer.showCurrentAccount(currentAccount)
  },
  // 當前客戶餘額
  getScoreWithCustomerAddr: function (currentAccount) {
    customer.getScoreWithCustomerAddr(currentAccount, ScoreInstance, account)
  },
  // 客戶購買商品
  buyGood: function (currentAccount) {
    customer.buyGood(currentAccount, ScoreInstance, account)
  },
  // 檢視已經購買的物品
  getGoodsByCustomer: function (currentAccount) {
    customer.getGoodsByCustomer(currentAccount, ScoreInstance, account)
  },
  // 客戶轉讓積分
  transferScoreToAnotherFromCustomer: function (currentAccount) {
    customer.transferScoreToAnotherFromCustomer(currentAccount, ScoreInstance, account)
  },
  //===================================== 商家方法 ==========================================
  // 商家註冊
  newMerchant: function () {
    merchant.newMerchant(ScoreInstance, account)
  },
  // 商家登入
  merchantLogin: function () {
    merchant.merchantLogin(ScoreInstance, account)
  },
  // 當前商家賬戶
  getCurrentMerchant: function (currentAccount) {
    merchant.getCurrentMerchant(currentAccount)
  },
  // 當前商家餘額
  getScoreWithMerchantAddr: function (currentAccount) {
    merchant.getScoreWithMerchantAddr(currentAccount, ScoreInstance, account)
  },
  // 商家積分轉讓
  transferScoreToAnotherFromMerchant: function (currentAccount) {
    merchant.transferScoreToAnotherFromMerchant(currentAccount, ScoreInstance, account)
  },
  // 商家新增商品
  addGood: function (currentAccount) {
    merchant.addGood(currentAccount, ScoreInstance, account)
  },
  // 顯示商家的所有商品
  getGoodsByMerchant: function (currentAccount) {
    merchant.getGoodsByMerchant(currentAccount, ScoreInstance, account)
  },
  // 商家清算積分
  settleScoreWithBank: function (currentAccount) {
    merchant.settleScoreWithBank(currentAccount, ScoreInstance, account)
  },
  //===================================== 銀行方法 ==========================================
  // 發行積分
  sendScoreToCustomer: function () {
    bank.sendScoreToCustomer(ScoreInstance, account)
  },
  // 銀行登入
  bankLogin: function () {
    bank.bankLogin(ScoreInstance, account)
  },
  // 檢視已經發行的積分
  getIssuedScoreAmount: function () {
    bank.getIssuedScoreAmount(ScoreInstance, account)
  },
  // 已經清算積分總數目
  getSettledScoreAmount: function () {
    bank.getSettledScoreAmount(ScoreInstance, account)
  },
  //===================================== 除錯方法 ==========================================
  // 查詢所有的區塊鏈賬戶
  allAccounts: function () {
    let allAccount = ''
    window.web3.eth.accounts.forEach(e => {
      allAccount += e + '\n'
    })
    window.App.setConsole(allAccount)  
  },
  // 狀態列顯示
  setStatus: function (message) {
    const status = document.getElementById('status')
    status.innerHTML = message
  },
  // 顯示console
  setConsole: function (message) {
    const status = document.getElementById('console')
    status.innerHTML = message
  }
}

//在頁面載入後,初始化web3,建立一個基於Http的provider,可以與以太坊節點進行通訊
//這裡用到的就是用ganache-cli啟動所提供的對外的rpc服務,因為ganache-cli啟動的時候繫結的是localhost,所以測試所使用的瀏覽器也要在本機。
window.addEventListener('load', function () {
  // 設定web3連線 http://127.0.0.1:8545 即以太坊節點
  window.web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'))
  window.App.init()   //呼叫初始化函式
})

四、試執行專案

在命令列中開啟ganache-cli客戶端,命令為:ganache-cli  執行埠是 8545

另外開一個命令列,cd到contracts目錄下,輸入truffle compiletruffle migrate --network develop 命令,把合約部署到區塊鏈上,並使用truffle.js中配置的開發網路

可以看到Score合約地址是0x0e2789開頭的。。 檢視ganache-cli的命令列,其中打印出除錯資訊,生成的第三個區塊中包含了Score合約地址,也是0x0e2789...

部署成功後,輸入 npm run dev 命令,會執行 package.json裡面的 dev 後面的指令碼,即 "webpack-dev-server" 在本地開啟伺服器,現在可以訪問客戶端web頁面。如果這裡報錯,說明要安裝 webpack-dev-server ,可自行百度如何安裝。

在瀏覽器中輸入網址: http://localhost:8000

五、測試合約功能

注意:不同電腦賬戶不同,每次重啟ganache-cli生成的賬戶也不同

註冊客戶和商戶:我們就用第二個和第三個分別註冊為客戶和商戶

商戶賬戶 0x14d4409c0e31a983ee831e04d5d72f635076b8b5

密碼 123

登入後跳轉,頁面連結末尾是商戶地址

客戶賬戶 0x93cbed31e02568a1ef3a2bc2591f74a8920c14ee

密碼 空 

註冊成功後,登入之後頁面跳轉,客戶賬戶在連結末尾

銀行賬戶預設是第一個賬戶

輸入銀行地址後,密碼不用管,後臺沒有比對密碼,點登陸即可跳轉到銀行介面

0x6abc6d15fbb1f8d83d73912ab33f042d2e4d73a0