QCon 演講:為什麼前端工程師更應該掌握區塊鏈 DApp 開發(下)
摘要
魔法哥在 “QCon 全球軟體開發大會” 2018 上海站的演講廣受好評。為讓這個演講發揮更大的價值,本文嘗試以圖文配合的方式還原現場體驗。
在實戰環節中,我們將和小明一起做出自己的第一款 DApp。實戰案例清晰簡潔,覆蓋完整的開發流程,帶你輕鬆跨入區塊鏈應用開發的大門。
(本文是演講的下半部分,建議您先讀完 ofollow,noindex" target="_blank">上半部分 再閱讀本文。)
在做好準備工作之後,小明終於開始動手了!
我們回顧一下傳統 Web App 和 DApp 的架構圖。
在開發傳統 Web 應用時,我們一般會先確定後端介面。同理,在開發 DApp 時,我們通常也會先把合約端的介面準備好。
首先,我們需要了解合約程式碼的編寫模式。
講到這裡,終於可以給大家看前面那段 “被打碼” 的程式碼了。這是一段最小化的星雲鏈智慧合約程式碼。我們來看一看它有哪些要素:
-
CommonJS 模組(必須)
先看一眼最後一行,前端同學們肯定秒懂。整個合約其實就是一個 CommonJS 模組。
-
匯出一個類(必須)
這個模組匯出的
MyContract
是一個類,這個類正是合約的主體。 -
構造器方法(可選)
既然合約主體是一個類,而類本身並不能直接執行,那麼這就意味著合約每次被呼叫時,這個類都會被例項化。類可以有(也可以沒有)一個構造器方法,這個方法在每次例項化時執行(這是類的基本行為,大家應該都很熟悉了)。它並不是必需的,我們可以根據實際需要編寫構造器方法。
-
初始化方法(必須)
這個類需要有一個
init()
方法,這個方法只會在合約部署成功後被自動執行一次,以後就再也無法被呼叫了。因此,這個初始化方法通常用來完成整個 “應用” 級別的初始化工作。它是必需的,哪怕你的應用不需要初始化,也需要提供一個空的init()
方法。 -
私有方法(可選)
這個類可以有一些私有方法,私有方法可以在類的內部被呼叫,但它不會暴露成為合約的公開介面。私有方法的特徵是下劃線開頭,比如上圖程式碼中的
_helper()
。我們可以根據實際需要定義私有方法,便於合約邏輯的實現,但它不是必需的。這個 “下劃線” 的命名約定並不是 JS 語言本身的特性,而是星雲鏈智慧合約執行環境的設計。這個設計也很符合前端開發者的命名習慣。
-
公開介面(必須)
除去上述特定名稱的方法和私有方法以外,剩下的所有方法都會作為合約的公開介面暴露出來,以供客戶端呼叫。比如上圖程式碼中的
method()
。
只要滿足以上條件,就是一個合法的智慧合約了。也就是說,你可以按照自己的開發習慣來組織合約程式碼,只要滿足上述條件就可以了。
其次,另一項重要的基礎知識是 “合約儲存區”。
前面提到過,每個合約都有自己的獨立儲存區,那麼智慧合約自然也提供了操作儲存區的 API。星雲鏈合約儲存區提供了多種操作方式,本文為方便講解,只介紹其中最基礎的用法。
在智慧合約的執行環境中,有一個 LocalContractStorage
全域性物件,它提供了三個最基本的 API(參見上圖)。
前端工程師看到這裡應該會會心一笑,因為這跟瀏覽器裡的本地儲存 API 幾乎一致。我們在瀏覽器端要實現持久化儲存,最常用的就是本地儲存(localStorage);而在智慧合約中,實現持久化儲存也是通過類似的 API 和類似的思路來實現的。
值得一提的是, .set()
方法接受的 value
引數可以是複合結構,比如陣列或物件等。也就是說,相對於瀏覽器端的 localStorage.setItem()
API,合約儲存區的 .set()
方法更方便,不需要手動處理 value
的序列化和反序列化問題。
接下來,我們就要開始實現合約介面了。
既然智慧合約要提供與傳統後端功能相當的介面,那不妨先來看盾,傳統的後端介面是怎樣的。
在小明的網站中,後端至少要提供兩個介面,分別對應以下兩項功能:
-
獲取預言列表
/api/getAllItems
這是一個 GET 介面;不需要引數;返回所有預言組成的陣列。每個陣列成員都是一個物件,用於記錄某條預言的基本資料——內容(
content
)和釋出時間(published_at
)。 -
釋出預言
/api/create
這顯然是一個 POST 介面。在向它提交資料時,需要提供一個
content
引數,以便傳入預言內容;並不需要提供釋出時間,因為服務端會記錄時間。在設計這種 “新增” 操作的介面時,我們通常會把釋出成功的這一條資料返回,以便客戶端立即把新內容渲染出來。
如何在合約端實現這兩個介面的功能?彆著急,慢慢來。小明先把合約的大致結構寫好:
小明開始寫程式碼,先把合約的大致結構寫好:
這個合約似乎並不需要在構造器裡做什麼事兒,留空就行。
然後需要做一些初始化工作。小明需要做的就是在合約儲存區裡建一個 key,取名 'items'
,用來儲存所有預言。在初始化階段,顯然啥預言也沒有,就存一個空陣列進去。
隨後就要進入重點了,小明開始寫兩個合約介面。
第一個介面是 “獲取預言列表”,即 getAllItems()
方法。
實現這個方法其實非常簡單——從儲存區裡讀出 'items'
這個 key 的值,返回就行了。寫出的程式碼也是一目瞭然。
第二個介面是 “釋出預言”,即 create()
方法。
這個介面稍稍複雜一些,我們一步一步來。
我們先把這條新預言的資料準備好,存到 newItem
變數中備用。
接下來更新儲存區:我們先取出所有預言的列表,然後把新預言 push 進去,最後把預言列表寫回儲存區。
在這個方法的最後,我們把這一條新預言的資料作為函式返回值,以便客戶端在拿到呼叫結果後立即把新預言渲染出來——這與傳統後端介面 /api/create
的設計保持一致。
好,兩個介面已經全部實現,合約程式碼就寫好了。
合約需要部署到鏈上才能真正發揮作用。這裡請出 “星雲 Web 錢包” 來完成合約的部署和測試。
部署合約的操作比較簡單,這裡就不贅述了。
部署成功之後,還可以測試一下合約行為是否符合預期。(如果發現合約程式碼有 bug,則需要重新部署。)
準備好合約端之後,小明接下來開始實現客戶端。
網頁形態的客戶端開發大家都很熟悉了,這裡不贅述,只著重講一下 DApp 客戶端獨有的行為——合約呼叫。
呼叫合約有兩種情況,或者說,有兩種方式:
-
不需要寫資料時
有些合約介面是不需要向儲存區寫入資料的,比如僅僅讀資料或純計算。此類呼叫可以直接呼叫,並可立即得到結果。
-
需要寫資料時
有些合約介面是需要向儲存區寫入資料的,即需要 “上鍊”。此類介面需要通過發起交易來呼叫,具體過程在 demo 環節已經展示過了。
在實現這兩種型別的合約呼叫時,需要用到客戶端 SDK。它的作用是幫助我們與鏈互動,我們不用操心區塊鏈網路的具體地址、介面和引數等細節,直接使用 SDK 提供的介面就可以完成呼叫合約、轉賬、查詢網路狀態等操作了。
星雲鏈官方的客戶端 SDK 功能完備,但設計風格偏底層,在實際開發中略顯繁瑣。因此這裡推薦一款第三方的 SDK——Nasa.js。
Nasa.js 的安裝方式很簡單,直接用 npm 就可以。
載入方式也很簡單,在頁面中引入 nasa.js
檔案,隨後就可以使用它提供的各種 API 了。
Nasa.js 提供了近 30 個 API,在日常開發中,最常用的是以下三個:
Nasa.query() Nasa.call() Nasa.getTxResult()
這三者都與合約呼叫相關。第一項用於 “不需要寫資料” 型別的合約呼叫,後兩項涉及 “需要寫資料” 的情況。具體用法會在下面的程式碼中詳細講解。
小明開始寫網頁端程式碼了!他先實現 “展示預言列表” 這個功能。
第一行很簡單,就是把合約地址儲存好。
接下來,小明需要呼叫合約的 getAllItems
介面,以獲得所有的預言資料,並在頁面中渲染出來。這個介面並不需要寫資料,因此使用 Nasa.query()
API 來呼叫它。
這個 API 接受三個引數,分別是合約地址、合約介面的函式名、傳給函式的引數。
getAllItems
介面不需要引數,小明傳了空陣列進去。
通過 Nasa.query()
發生的合約呼叫,可以很快得到呼叫結果,這個過程就好像是在呼叫一個 Ajax 介面。 Nasa.query()
的返回值是一個 Promise,Promise 包裹的值就是合約呼叫結果——這個設計和常見的 Ajax 庫也是十分類似的。
小明在 Promise 的 .then()
回撥中拿到合約介面的呼叫結果。呼叫結果有一個 execResult
欄位,用來儲存合約介面函式的返回值。小明通過它可以得到一個包含所有預言資料的陣列,然後在網頁上就可以渲染出預言列表了。
小明接著實現第二個功能——“釋出預言”。
釋出頁面有一個表單,小明需要給表單的提交事件繫結事件處理函式(程式碼略),並在這個事件處理函式中實現釋出預言的操作(以下程式碼均寫在事件處理函式中)。
前兩行是準備工作,把合約地址和已經輸入的文字儲存好。
接下來,小明需要呼叫合約的 create
介面,以完成新預言的釋出。這個介面需要通過發起交易來完成上鍊,於是小明使用 Nasa.call()
API 來呼叫它。
這個 API 同樣接受三個引數,引數含義與 Nasa.query()
的三個引數完全一致。小明依次傳入合適的值。
這個 API 會喚起錢包外掛,引導使用者完成交易。
這個 API 也返回一個 Promise,但 Promise 包裹的值與 Nasa.query()
不同。因為通過交易來呼叫合約,無法直接得到呼叫結果,只有當這一筆交易被礦工處理完成並打包上鍊,我們才能得到這次合約呼叫的結果。因此,這個 Promise 只能給我們一個交易流水號,我們隨後可以拿著這個流水號向鏈查詢交易的狀態和呼叫結果。
當錢包外掛把交易發出後,小明就可以在 .then()
回撥中拿到交易流水號( payId
)。接下來就輪到 Nasa.getTxResult()
這個 API 出場了。
這個 API 接受一個引數,就是交易流水號;返回一個 Promise,Promise 包裹的值就是本次交易的狀態和本次呼叫的結果。(查詢交易結果是一個輪詢的過程,不過我們不用操心這些細節,Nasa.js 會自動完成這個過程,我們只需要關心這個 Promise 就可以了。)
在 .then()
回撥中,小明把交易流水號傳給 Nasa.getTxResult()
。由於這個 API 也返回 Promise,小明可以把它 return 出去,讓這個 Promise 鏈條繼續往下走。
當查詢發現交易被礦工處理完成之後,可以在下一個 .then()
回撥中拿到本次交易的狀態和本次呼叫的結果。同樣還是在 execResult
欄位中,我們可以獲取合約介面函式的返回值。
當然保險起見,我們最好先判斷一下交易是不是成功了。如果 status
欄位的值為 1
,則表示交易已被正常處理。
小明在這裡拿到合約介面函式 create
的返回值(也就是釋出成功的這條新預言的資料),把它渲染並新增到頁面中就可以了。
至此,整個 DApp 從合約端到客戶端,就全部開發完成了。小明把這兩端的功能跑通,網站順利上線。
這裡順便插一句,對 DApp 來說,甚至可以沒有 “上線” 這個環節。因為整個應用的業務邏輯已經以智慧合約的形式部署上鍊了,如果小明只打算自己用的話,客戶端程式碼儲存為 HTML 頁面,在本地開啟照樣可以用。
網站看起來還是那個網站,不過它的本質已經升級為基於區塊鏈的 DApp 了。
萬事具備,小明靜靜等待下一屆世界盃的到來。
2018 年世界盃,小明又一次正確預測出了冠軍得主。
小明終於成功地在女朋友面前秀了一把。
從此他們幸福地生活在一起……(全劇終)
故事到這裡就講完了。不過在聽故事的過程中,大家頭腦中可能會冒出一些問題。
首先,這個實戰案例是不是太簡單了?
時間關係,我們無法在短短一次演講中實現更多的功能。不過,網際網路產品都是迭代式開發的嘛,小明確實也打算升級一下這個 DApp。他設想的新功能有:
-
支援多使用者:這麼好的應用,只有自己在用太可惜了。小明打算讓它支援多使用者。
-
可點贊:有了多使用者,就可以加入一些社交元素。比如當你看到一條有意思的預言,可以給它點贊。
-
可打賞:不止是點贊,你甚至還可以給預言的作者打賞。
先說 “點贊”,這個功能相對比較好實現,合約端新增一個點贊介面,再給預言資料增加一個點贊數的欄位,差不多就可以了。
再看 “多使用者” 和 “打賞” 功能,乍一看很複雜,但其實在區塊鏈上非常容易實現。
在開發傳統 Web App 的時候,如果要識別不同使用者,往往需要自己做一套賬號系統,或者對接第三方社交賬號。而在區塊鏈上就不用這麼麻煩,因為每個在鏈上的使用者都有自己獨一無二的身份標識——“地址”。DApp 通過地址就可以區分每位使用者。
此外,在傳統應用中如果要實現打賞功能,就需要接入支付平臺,比如銀聯、支付寶或微信支付等。但這對個人開發者來說是極為困難的。別急,這裡又體現出 DApp 的優勢了——區塊鏈的老本行就是記賬,每條公鏈通常都有自己的原生貨幣系統,DApp 直接使用鏈的支付功能就可以了。
區塊鏈 “原生的賬號系統” 和 “原生的支付系統”,堪稱 DApp 的兩大天生神力!
這樣一個 DApp 最終確實被做了出來,叫作 “我是預言帝”。這實際上也是魔法哥自己學習開發 DApp 的第一款作品。
另一個經常被問到的問題是:大家經常提到區塊鏈 “不可篡改”,到底是指什麼?小明的女朋友憑什麼相信他?…………
……
……
完整文章已收錄到 “CSS魔法” 微信公眾號,微信掃碼即可閱讀全文: