歡迎回到這個很牛的教程系列的第2部分,在教程中我們親手構建我們的第一個分散式應用程式。 在第二部分中,我們將介紹VueJS和Vuex的核心概念,並引入web3js以與metamask進行互動。
如果你錯過了第一部分,你可以在下面找到它:
VueJS是一個用於構建UI的javascript框架。乍一看,它看起來與經典的moustache模板類似,但在底層為了使Vue變得響應式發生了很多事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<div id=”app”> {{ message }} </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) |
這將是一個非常基礎的Vue應用程式的結構。資料物件中的訊息屬性將渲染到id為’app’的元素的螢幕區域,當我們更改此訊息時,它將在螢幕上更新並無需重新整理。你可以在這個jsfiddle中檢視它(須開啟自動執行):https://jsfiddle.net/tn1mfxwr/2/ 。
VueJS的另一個重要特性是元件。元件是小規模的、可重用的和自包含的程式碼片斷。本質上,Web應用程式可以抽象為一顆更小的元件樹。當我們開始編寫我們的前端應用程式時,這將會變得更加清晰。
將網頁抽象成元件的示例。該網頁由三部分組成。其中兩個元件具有子元件。
我們將使用Vuex來管理我們的應用程式的狀態。與redux類似,Vuex實現了一個倉庫,作為我們app相關資料的“唯一真實資料來源”。Vuex允許我們以可預測的方式操作和提供我們的應用程式所使用的資料。
它的工作方式非常簡單。元件在渲染時是需要資料的,它會派發一個動作(action)來獲取它所需要的資料。獲取資料的API呼叫發生在action的async中。 一旦資料被提取完成,action就會將這些資料提交給mutation。 之後mutation會改變我們倉庫中的狀態。當該元件所使用的資料在倉庫中發生更改時,它將重新渲染。
Vuex的狀態管理模式
在第一部分中,我們使用 vue-cli 生成了 Vue 應用程式,同時還安裝了需要的依賴項。 如果你還沒有完成這一部分,請點選的頂部的連結。
新生成的 vue 應用
注意: 如果你打算從這裡複製貼上程式碼,新增/src/ 到 .eslintignore 檔案中,避免縮排錯誤。
你可以在終端輸入‘npm start’來啟動這個App。這個App會包含預設的vue應用,因此我們可以將它放出來。
注意:我們正在使用vue Router儘管只有一個route,我們不需要它,但是因為它太簡單了,我認為將它放到教程裡挺好的。
提示:在.vue檔案將你的atom語法(例如 bottom right)新增到HTML。
現在來清理一下這個新專案:
- 在app.vue中刪除img-tag,同時刪除在style-tags之間的所有東西。
- 刪除components/HelloWorld.vue,新建兩個檔案分別名為casino-dapp.vue(我們的主模組)和hello-metamask.vue(會包括我們的metamask資料)
- 在我們的新檔案hello-metamask.vue中貼上以下程式碼,這些程式碼現在只是在一個p-tag中顯示文字‘Hello’。
12345678910111213141516171819<template><p>Hello</p></template><script>export default {name: 'hello-metamask'}</script><style scoped></style>
- 通過引入檔案,我們現在要載入hello-metamask模組到我們的主模組casino-dapp模組中,然後在我們的vue例項中關聯這個模組,因此我們可以將它作為一個tag加到我們的模板中。將下面程式碼貼上到casino-dapp.vue檔案中:
123456789101112131415161718192021222324252627<template><hello-metamask/></template><script>import HelloMetamask from '@/components/hello-metamask'export default {name: 'casino-dapp',components: {'hello-metamask': HelloMetamask}}</script><style scoped></style>
- 現在如果你開啟了router/index.js,你就會看到我們根目錄下只有一個route,它仍然指向我們已經刪掉的HelloWorld.vue模組。我們需要將它改為指向我們的casino-dapp.vue模組。
12345678910111213141516171819202122232425import Vue from 'vue'import Router from 'vue-router'import CasinoDapp from '@/components/casino-dapp'Vue.use(Router)export default new Router({routes: [{path: '/',name: 'casino-dapp',component: CasinoDapp}]})
關於Vue Router:你可以新增其他的路徑以及繫結模組,當你訪問定義的路徑他們會進行渲染。因為我們App.vue檔案中有route-view tag,正確的模組會被渲染。
- 在src中建立一個名為util的新資料夾。在這個資料夾中建立另一個資料夾名為constants。建立一個名為network.js的新檔案,然後將下面程式碼貼上進去。當我們程式碼在清理的時候,這會讓我們顯示Ethereum 網路名稱而不是它的id。
- 最後但同樣重要的是(現在其實並不重要),在src中建立一個名為store的新資料夾。在下一章我們再來談論這個。
如果你在終端執行‘npm start’,然後在瀏覽器訪問localhost:8080,你應該就能在螢幕上看到‘Hello’。如果是這樣的,你就可以準備繼續了。
在這一章我們要設定我們的store。現在開始在我們的全新的store目錄(上一章的最後一部分)建立兩個檔案:index.js和state.js;我們由state.js開始,這個檔案將會作為一個新的我們檢索資料的代表。
好的,現在我們開始在index.js中配置我們的store。我們要引入vuex庫供vueJS使用。我們還要引入state,然後也加入我們的store中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import Vue from 'vue' import Vuex from 'vuex' import state from './state' Vue.use(Vuex) export const store = new Vuex.Store({ strict: true, state, mutations: {}, actions: {} }) |
最後一步來編輯main.js來包含我們的store:
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 |
import Vue from 'vue' import App from './App' import router from './router' import { store } from './store/' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, store, components: { App }, template: '<App/>' }) |
做得不錯,完成了這麼多的配置,給自己一點表揚。現在我們準備好了開始通過web3 API獲取我們的metamask資料,然後讓他為我們的應用服務。就要實現了!
就像之前提到的,為了將資料獲取到我們的Vue app,我們需要傳送一個action來做非同步API呼叫。我們使用promises將一些呼叫連線到一起,然後將其抽象成一個檔案。因此在util資料夾中建立一個名為getWeb.js的新檔案。將下面的程式碼貼上到裡面,這些程式碼包含了你要遵循的相當多的註解。我們在程式碼塊下面也會提到。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
import Web3 from 'web3' /* * 1. Check for injected web3 (mist/metamask) * 2. If metamask/mist create a new web3 instance and pass on result * 3. Get networkId - Now we can check the user is connected to the right network to use our dApp * 4. Get user account from metamask * 5. Get user balance */ let getWeb3 = new Promise(function (resolve, reject) { // Check for injected web3 (mist/metamask) var web3js = window.web3 if (typeof web3js !== 'undefined') { var web3 = new Web3(web3js.currentProvider) resolve({ injectedWeb3: web3.isConnected(), web3 () { return web3 } }) } else { // web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')) GANACHE FALLBACK reject(new Error('Unable to connect to Metamask')) } }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve network ID result.web3().version.getNetwork((err, networkId) => { if (err) { // If we can't find a networkId keep result the same and reject the promise reject(new Error('Unable to retrieve network ID')) } else { // Assign the networkId property to our result and resolve promise result = Object.assign({}, result, {networkId}) resolve(result) } }) }) }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve coinbase result.web3().eth.getCoinbase((err, coinbase) => { if (err) { reject(new Error('Unable to retrieve coinbase')) } else { result = Object.assign({}, result, { coinbase }) resolve(result) } }) }) }) .then(result => { return new Promise(function (resolve, reject) { // Retrieve balance for coinbase result.web3().eth.getBalance(result.coinbase, (err, balance) => { if (err) { reject(new Error('Unable to retrieve balance for address: ' + result.coinbase)) } else { result = Object.assign({}, result, { balance }) resolve(result) } }) }) }) export default getWeb3 |
首先要注意的是我們使用promises來連線回撥,如果你不知道promises,檢視相關連結。下一步我們檢查使用者是否有Metamask(或者 Mist)在執行。Metamase注入他自己的web3的例項,我們因此確認window.web3(被注入的例項)已經定義了。
如果沒有定義的話,我們要使用Metamask建立一個web3例項作為當前provider,因此我們不依賴被注入的例項的版本。我們將我們新建立的例項傳給下一個promises,在這裡我們做幾個API呼叫:
- web3.version.getNetwork() 會返回我們連線的網路ID。
- web3.eth.coinbase() 返回我們節點屬於的地址,當使用Metamask的時候這會是我們選擇的賬戶。
- web3.eth.getBalance(
) 返回我們作為引數傳過去的地址的balance。
還記得我們說的在我們的Vuex store中需要在一個action產生非同步API呼叫嗎?我們現在會把它hook出來,之後從我們的元件dispatch出去。在store/index.js中我們會引入我們的getWeb3.js檔案,呼叫它然後將它commit給一個mutation,然後在我們的store中儲存。
在你的引用語句中新增
然後在action物件(在你的sotre中)中我們會呼叫getWeb3然後commit結果。我們在邏輯中新增了大量的console.logs,因此我們可以看到程式的步驟,這會讓我們對完全的dispatch-action-commit-mutation-statechange流程瞭解的更加透徹。
現在開始建立我們的mutation,它會在我們的store中將資料儲存到state。通過訪問第二個引數,我們可以在mutation中訪問傳進commit的資料。在mutations物件新增下面的函式。
不錯!現在還要做的就剩下從我們的元件將action dispatch給實際檢索資料,然後把它渲染到我們的應用。為了dispatch我們的actions,我們要利用Vue的生命週期hooks。
在我們的例子中,我們需要從main casino-dapp元件dispatch 我們的action,在元件被建立之前。因此在components/casino-dapp.vue 的name屬性之後新增下面的函式:
好了,現在我們要從hello-metamask元件渲染這些資料,我們所有的賬戶資料都會在這個元件中被渲染。為了從我們的store獲取資料,我們需要將一個getter函式傳入computed。然後我們可以在我們的模板中使用curly-braces關聯資料。
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 27 28 29 30 31 32 33 34 35 36 37 |
<template> <div class='metamask-info'> <p>Metamask: {{ web3.isInjected }}</p> <p>Network: {{ web3.networkId }}</p> <p>Account: {{ web3.coinbase }}</p> <p>Balance: {{ web3.balance }}</p> </div> </template> <script> export default { name: 'hello-metamask', computed: { web3 () { return this.$store.state.web3 } } } </script> <style scoped></style> |
不錯,現在都可以工作了。在你的終端使用‘npm start’啟動專案,然後訪問localhost:8080。我們現在應該可以看到我們的metamask資料。當我們開啟控制檯的時候,我們應該看到在state管理模式下控制檯log輸出的資訊,就像是在文章的vuex部分描述的一樣。
如果你能做到這一點,一切都很好,那麼就認真的做下去。目前這塊是這個系列最難的部分。在下一部分,我們將學習如何輪詢Metamask的更改(如更換賬戶)和將我們在第一部分寫的智慧合約連線到我們的應用。