隨遇而安的DAPP開發實踐教程(五)DAPP與數據可視化
1、基於數字地球的遊戲創意
當我們已經基本熟悉了DAPP的整套開發流程(後端使用Solidity,前端使用JS+HTML)之後,下一步就是從創意層面去解放自己了。須知,基於區塊鏈的用戶程序開發,本質上只是實現了一種去中心化的價值流動機制,它不需要建立用戶之間的強信任關係就可以安全地完成交易。在此基礎上,是實現一款遊戲,一個社交平台,一個交易市場,還是行業專屬的某種應用軟體——這個問題並沒有定論,只要用戶手中的資產(虛擬貨幣或者代幣)在這款應用當中有流通的意義,應用本身就有存在和被推廣的需求。
當然我們並不想去深究當前的區塊鏈熱潮下的資本流動趨勢,以及眾多虛擬貨幣的發展前景和藏匿其中的陰謀騙局。無論如何,技術本身都有存在、發展和融合的必要性。我們選擇在本章教程中將DAPP和數據可視化手段相結合,也正是針對這兩種看似截然不同的技術領域做一次結合的嘗試,也許同樣具備相關領域知識或者訴求的讀者能夠從中看到一些契機。
數據可視化本身也是一個相當有深度的話題,與之相關的技術手段包括大數據挖掘、雲計算、計算機視覺、圖形學、人機交互、人工智慧等,而表現形式包括且不限於表格、圖表、動畫、三維模型、數字地球、以及VR和AR等。我們在本章將選取一個非常具有代表性的表現手法,即基於數字地球的三維地理信息展示,這與筆者之前的技術背景也是密切相關的。
數字地球,顧名思義就是我們所居住的地球的三維數字模型的呈現。它將地球上的海量地理信息和環境的時空變化數據保存到網路中,按照實際的經緯度坐標予以整理和分散式存儲。在用戶需要調取這些數據顯示的時候,在網路客戶端上通過實時圖形渲染的技術將所有信息直觀、動態、準確地呈現給用戶,並輔以精緻的光影效果,或者疊加必要的輔助信息(例如國境線、路網等矢量數據)。
在數字地球展示的整個過程中,所涉及到的數據通常需要用TB,PB這樣的量級去衡量。顯而易見,目前我們必然無法將它們全部保存到區塊鏈當中,並且也沒有必要將大量無關資產的數據放在區塊鏈網路中流動,導致很多與之無關的用戶節點也會承載過重的壓力。也許在不遠的將來我們可以用上IPFS或者其它類似的技術來實現這種與區塊鏈整合在一起的分散式存儲方法,不過在本片教程中,我們暫時不考慮這個因素。
那麼我們應當如何將DAPP的開發方法融合到數字地球當中呢?我們不妨學習一下別人的創意,例如:
https://decentraland.org
這個遊戲的目標是構建基於Ethereum的虛擬土地資產,其數量有限,但是可以模擬真實世界一樣去買賣每塊土地的所有權。土地的所有者可以自由構建土地上所呈現的內容,例如靜態的3D場景,或者自定義的建模和動畫腳本。
這個遊戲企劃已經完成了一輪ICO,但是目前還看不到可玩的實體。沒關係,我們還有更簡單一點的選擇:
https://cryptocountries.io/
這款2018年初一度風靡網路的遊戲名叫CryptoCountries,它的遊戲邏輯幾乎可以用一句話來表述清楚:玩家可以使用以太幣(Ether)自由買賣目前世界上所有國家的所有權(當然這是虛構的!)。只要你的出價高於前一個買家,就可以得到一個國家,而你的出資歸前一任「國王」所有。玩家所「持有」的國家充其量只是網上的一張虛擬圖片而已,但是因為交易本身的誘惑性,以及一部分民族自尊心的影響,很多人還是身不由己地投身其中,樂此不疲。
不過這款遊戲本身顯然也給了我們足夠的靈感。我們在本教程中的目標,就是實現一款結合數據可視化前端的DAPP,允許玩家從數字地球上尋找自己感興趣的國家,選中它並使用以太幣(這回可不是什麼代幣Token了)買下其所有權;別的玩家可以選中並看到某個國家的所有者名稱,以及被購買時的價格;如果新的報價高於當前價格,那麼以太幣會被付給前一位所有者,同時國家所有權易主。為了實現這一目標,我們考慮使用Solidity語言編寫智能合約,以及OpenZeppelin來提供必要的輔助功能;同時使用久負盛名的WorldWindJS來實現前端數字地球的呈現。WorldWind的下載地址為:
https://github.com/NASAWorldWind/WebWorldWind
點擊網頁右上方的「Clone or download」按鈕,通過GIT更新或者直接下載並解壓縮到本地的某個目錄中,然後在控制台下執行:
# npm install
安裝WorldWind必需的依賴庫文件。然後筆者建議從下面的地址再下載幾個常用的JS輔助功能庫文件(JS和CSS),以便之後的前端代碼開發中使用:
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js
https://code.jquery.com/jquery.min.js
https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.17/require.min.js
當然,使用類似npm install jquery的指令去安裝也是完全可以的,只是需要變動一下最終的HTML文件中欄位引用的位置。我們這裡簡單起見,將這些文件直接複製到WebWorldWind的工程根目錄下。
那麼,是時候考慮一下如何編寫Solidity端的程序代碼了。
2、編寫後端代碼和測試程序
構建Truffle工程的基本過程這裡不再贅述,使用truffle.cmd init初始化工程目錄之後,我們需要在contracts目錄新建一個名為DecentralEarth.sol的腳本,在其中定義合約介面DecentralEarth,並且在migrations目錄中編寫與之對應的發布腳本。
合約腳本的基本框架依然依賴於OpenZeppelin,因此我們需要提前在工程目錄中使下面的指令安裝該依賴庫:
# npm install zeppelin-solidity
當然在本案例中,我們並沒有用到ERC20之類的標準介面。不過出於規劃開發流程和安全性的考慮,這裡和以後的所有代碼中我們都會嘗試引用這個庫。因此合約腳本文件的基本結構即為:
pragma solidity ^0.4.19;
import "zeppelin-solidity/contracts/ownership/Ownable.sol";
contract DecentralEarth is Ownable {
struct CountryOwnship {
string ownerName; //所有者的名字
address owner; //所有者的地址
uint currentValue; //當前的價格
}
CountryOwnship[255] countries; //所有國家的所有權記錄
mapping (address => uint) ownedCountries; //某個用戶所持有的國家
……
}
注意,我們的合約介面繼承自Ownable,這是OpenZeppelin提供的一個功能介面,可以記錄合約的初始發布者,並據此約束某些函數介面的執行許可權。
因為全世界的國家總數不會超過255個,因此我們可以直接定義一個長度255的數組來記錄每個國家的所有權信息,同時定義一個用戶地址到uint256的映射表,按位存儲每個用戶所擁有的國家信息,以便隨時查詢。
然後我們需要定義幾個必要的介面來完成國家的買賣和信息查詢操作,主要包括:
function getCountryOwner(uint _countryId) public view returns(string) {
//根據提供的國家ID,返回持有者的名稱
require(_countryId
return countries[_countryId].ownerName;
}
function getCountryValue(uint _countryId) public view returns(uint) {
//根據提供的國家ID,返回當前購買的最低標價
require(_countryId
return countries[_countryId].currentValue;
}
function getOwnedCountries(address _user) public view onlyOwner returns(uint) {
//返回指定用戶所持有的國家列表,只有合約發布者可以執行該操作
return ownedCountries[_user];
}
function obtainCountry(string _name, uint _countryId) external payable {
//嘗試購買國家,注意這個函數是payable的,可以執行支付
require(_countryId
//這裡的msg.value表示執行函數的用戶所給出的ETH數量
require(countries[_countryId].currentValue
//執行購買操作,把ETH轉給國家的前一位擁有者,
//或者合約發布者(owner通過Ownable介面定義)
CountryOwnship storage country = countries[_countryId];
if (country.currentValue == 0) owner.transfer(this.balance);
//更新國家所有權數組的內容,msg.sender表示當前執行函數的用戶
country.ownerName = _name;
country.owner = msg.sender;
country.currentValue = msg.value;
ownedCountries[country.owner] |= (uint(1)
}
整個智能合約的內容並不是非常難以理解,然後我們可以快速編寫一個基於JS的前端文件去驗證它的可用性。和之前的DAPP開發流程類似,我們首先還是在js文件中導入必要的庫和合約,並且實現一個全局的App介面,以及在網頁載入時直接連接到測試鏈的代碼:
import { default as Web3 } from "web3";
import { default as contract } from "truffle-contract"
import decentralearth_artifacts from "../../build/contracts/DecentralEarth.json"
var DecentralEarth = contract(decentralearth_artifacts);
//定義App介面和介面函數
window.App = {
start: function() {
DecentralEarth.setProvider(web3.currentProvider);
},
……
}
window.addEventListener("load", function() {
//載入網頁時,從MetaMask或者指定地址載入測試鏈到web3介面
if (typeof web3 !== "undefined") {
window.web3 = new Web3(web3.currentProvider);
} else {
window.web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:7545"));
}
//初始化完成後直接執行App.start()函數
App.start();
});
我們在JS端同樣可以直接實現對合約的調用操作,同時我們也有必要調用web3的通用介面,查詢用戶持有的ETH總量,以免自不量力的事情發生:
showBalance: function(user) { //顯示指定用戶所持的ETH總量
web3.eth.getBalance(user, function(err, balance) {
//暫時留空,之後通過web3.fromWei(balance.valueOf())讀取
});
},
showCountryOwner: function(id) {//顯示國家所有者的信息
var decentralEarth;
DecentralEarth.deployed().then(function(instance) {
decentralEarth = instance;
return decentralEarth.getCountryOwner.call(id); //所有者姓名?
}).then(function(ownerName) { //暫時留空
return decentralEarth.getCountryValue.call(id); //目前價格?
}).then(function(ownerValue) { //暫時留空
});
},
setCountryOwner: function(user, name, countryID, value) { //設置國家所有權
DecentralEarth.deployed().then(function(instance) {
return instance.obtainCountry(name, countryID, // value即用戶ETH報價
);
}).catch (function(e) { //考慮到購買很可能失敗(ETH不足),catch是必要的
alert("Failed to set country owner");
});
},
至於HTML端,測試階段我們大可簡化起見,顯示一行標題再直接執行App介面的幾個函數即可,稍後我們會將打包好的JS文件放到WorldWind中執行。打包所需的其他文件,包括css文件,package.json和webpack.config.js文件,都可以和上一個教程中完全相同。執行npm run build對JS介面打包,然後把新的app.js文件從build目錄拷貝到之前解壓縮WebWorldWind的工程目錄下,我們即將開始最後的整合工作了。
3、基於WorldWind構建前端
如果之前的操作都沒有差錯的話,我們現在可以在WebWorldWind的工程目錄下看到5個手動拷貝而來的文件,如圖:
現在我們可以動手編寫數字地球的前端代碼了,首先建立名為MyEarth.html的網頁文件。
……
……
Your browser does not support HTML5 Canvas.
在部分將所有的css和js文件依次導入,部分則負責定義交互界面,以及用作WebGL地球繪製的標籤。
然後建立MyEarth.js文件,其中有關WorldWind數字地球的建立過程相對比較繁瑣,這裡只截取核心和關鍵的代碼段進行講解。完整的文件可以參閱本系列教程的GitHub地址(附在本文最後):
//聲明幾個重要的全局變數,依次為用戶餘額,當前選中的國家ID,
// WorldWind全局對象,以及被選中的國家Placemark列表
var userBalance, selectedCountry, wwd, highlightedItems = [];
//使用requirejs來驅動WorldWind啟動,並繪製初始圖層
requirejs(["./examples/WorldWindShim", "./examples/LayerManager"],
function(WorldWind, LayerManager) {
"use strict";
WorldWind.Logger.setLoggingLevel(WorldWind.Logger.LEVEL_WARNING);
wwd = new WorldWind.WorldWindow("canvasOne"); //在上建立
wwd.deepPicking = true; //允許用戶在圖層上用滑鼠選取
//用於處理用戶選取的函數
var handlePick = function (o) {
var x = o.clientX, y = o.clientY;
……//選擇某個Placemark後,查詢所有權並允許買賣
}
wwd.addEventListener("click", handlePick); //監聽滑鼠點擊事件
//添加圖層
var layers = [
, //衛星影像
, //大氣層
, //宇宙星空
,
];
for (var l = 0; l
layers[l].layer.enabled = layers[l].enabled;
wwd.addLayer(layers[l].layer);
}
//聲明Placemark的屬性模板
var placemarkAttributes = new WorldWind.PlacemarkAttributes(null);
……//設置屬性模板的過程從略
//添加Placemark(即用來標識每個國家名稱的地標)層
var placemarkLayer = new WorldWind.RenderableLayer("Placemarks");
wwd.addLayer(placemarkLayer);
//用來處理矢量數據載入過程的函數
var shapeConfigurationCallback = function(attributes, record) {
……//根據輸入的矢量數據,添加各個國家的Placemark,以供用戶選取
}
//添加矢量層,從.shp文件中讀取所有國家的數據以及國境線,並繪製到3D地球上
var worldLayer = new WorldWind.RenderableLayer("Countries");
var worldShapefile = new WorldWind.Shapefile("./shp/ne_110m_admin_0_countries.shp");
worldShapefile.load(null, shapeConfigurationCallback, worldLayer);
wwd.addLayer(worldLayer);
//建立圖層管理器並重繪場景
var layerManager = new LayerManager(wwd);
wwd.redraw();
}
我們需要知道地球上所有國家的名稱,國境線範圍,並且能夠在數字地球上選中它,才能夠進一步查詢它的所有權(是否被其他用戶花費一定ETH購買了),以及消耗ETH去購買這個國家。這一過程是通過讀取外部的國界線矢量數據來完成的,我們已經在教程的GitHub倉庫中提供了代碼中的shp文件,有興趣深入研究的讀者也可以在下面的地址里選擇下載:
https://worldwind.arc.nasa.gov/web/examples/data/shapefiles/naturalearth/
如果我們此時需要看一下數字地球渲染的效果,可以先在WebWorldWind的工程根目錄下建立簡單的伺服器(直接雙擊運行可能會導致某些信息無法顯示,例如星空),比如使用Python在本地的1234埠建立伺服器:
# python.exe -m SimpleHTTPServer 1234
然後在瀏覽器端打開MyEarth.html文件,隨意操作看看。
我們甚至可以通過滑鼠滾輪、右鍵的配合衝到靠近地表的位置,並改變觀察的俯仰角度。
在var shapeConfigurationCallback = function(attributes, record) {}一段中,我們需要載入shp文件的每一個圖元並且將它們轉換到代表每個國家的Placemark,核心代碼為:
……
var latitude = (record.boundingRectangle[0] + record.boundingRectangle[1]) / 2;
var longitude = (record.boundingRectangle[2] + record.boundingRectangle[3]) / 2;
var placemark = new WorldWind.Placemark(new WorldWind.Position(latitude, longitude, 1e2), false, null);
……
placemarkLayer.addRenderable(placemark);
……
這裡的國家名稱被設置給Placemark的label欄位(顯示名),而國家的唯一ID則設置給它的用戶屬性userProperties,以便在國家被選中時調取使用。使用Placemark表示每個國家之後的數字地球近景如圖所示:
在handlePick()函數中,我們需要處理每個Placemark被選中之後的操作,即:
……
var pickList = wwd.pick(wwd.canvasCoordinates(x, y));
for (var p = 0; p
selectedCountry = parseInt(pickList.objects[p].userObject.userProperties);
if (isNaN(selectedCountry)) continue; //從userProperties得到的ID是否合法
App.showCountryOwner(selectedCountry); //執行DAPP的介面函數!
highlightedItems.push(pickList.objects[p].userObject); //加入列表
}
}
現在我們第一次將DAPP的前端介面(即App介面)與WorldWind數字地球的介面放在一起使用了,當然,我們也有必要修改一下app.js中的內容,以便直接反饋到MyEarth.js中。這一過程可以通過修改之前的JS文件,然後重新npm run build打包來迅速完成:
showCountryOwner: function(id) { //獲取所有權信息
var decentralEarth, countryOwner, countryValue;
DecentralEarth.deployed().then(function(instance) {
decentralEarth = instance;
return decentralEarth.getCountryOwner.call(id);
}).then(function(ownerName) {
countryOwner = ownerName;
return decentralEarth.getCountryValue.call(id);
}).then(function(ownerValue) {
countryValue = web3.fromWei(ownerValue.valueOf());
updateSelectedCountry(countryOwner, countryValue); //將獲取的數值傳遞迴去!
});
},
setCountryOwner: function(user, name, countryID, value) { //設置新的所有者
DecentralEarth.deployed().then(function(instance) {
return instance.obtainCountry(name, countryID,
);
}).then(function() {
updateSelectedCountry(name, value); //將更新的數值傳遞迴去!
}).catch (function(e) {
alert("Failed to set country owner");
});
}
顯然,updateSelectedCountry()函數應該在MyEarth.js中完成,它的實現過程為:
function updateSelectedCountry(countryOwner, countryValue) {
var redrawRequired = false;
for (var h = 0; h
var label = highlightedItems[h].label;
if (label.indexOf("
") > 0) label = label.substr(0, label.indexOf("
"));
highlightedItems[h].label = label + "
Owned by " + countryOwner
+ "
Price: " + countryValue + "eth"; //顯示所有權信息
highlightedItems[h].highlighted = true;
redrawRequired = true;
}
if (redrawRequired) wwd.redraw(); //必要的話,重新繪製3D地球
}
此時,如果我們有一個簡單一些的界面來完成選中某個國家之後的買賣操作,例如:
可以看到USA還沒有被任何人購買,那就花費5ETH(反正是測試鏈),點擊「Buy country」按鈕好了:
再看看Ganache的界面:
正如我們所料想的,第二位用戶花費了5ETH去購買一個虛擬的USA,而錢落到了合約發布者的腰包里。如果現在還有第三位用戶打算花費更高價格(例如8ETH)來買進USA,那麼當前用戶將成為這次交易的受益者了:
當然,我們在這裡給出的工作流並不一定適合真正的DAPP開發團隊使用。本文更多的是希望展示給讀者一個信息:即智能合約和它的前端介面的開發,與具體可視化程序和界面的開發可以是完全獨立的兩個部分。使用npm將合約介面部分打包之後,直接在另一個工程中就可以調用它,只要確保區塊鏈網路是可以連接使用的,以及同一合約已經正確發布在鏈上即可。如果讀者希望建立一個適合團隊開發DAPP的體系,那麼不妨繼續深入挖掘npm的潛力,例如將WorldWind也納入到package.json當中進行管理。
本工程GitHub地址:https://github.com/xarray/ethereum_dapp_demos
GIF
GIF
歡迎隨便轉載。
TAG:Xarray的旁門左道集 |