【初心者向け】イーサリアムとReactで作るTwitterライクなDapps
# 目次
- 目次
# 何を作るか
Twitterのように、すべてのユーザーが共有の1つの巨大なスレッドに投稿していくようなメッセージアプリを作ります.
投稿すると全ユーザーに表示されます.
UIはReactで作成し、メッセージの送信とデータベースにイーサリアムブロックチェーンを使用します.
# デモ
https://shunsukehondo.github.io/dt-sample/
Ropstenネットワーク上で動作します.
メッセージ送信手数料の支払いにMetaMaskが必要ですので、事前にインストールと入金、ネットワーク切り替えなどの設定をして下さい.
(参考: MetaMaskのインストールと基本操作)
# 完成版ソースコード
完成版ソースコードは以下のレポジトリのmasterブランチにあります.
https://github.com/shunsukehondo/DTwitter"
# 使用する技術
Dappsはユーザーに表示するウェブページとデータ保存先としてのブロックチェーンから構成されます.
ウェブページはHTML、CSSとJavascriptのみで作成します.
今回は使用しませんが、プライベートなデータやサイズの大きいデータの保存先が必要な場合は、別途サーバーを用意します.
# ウェブページ
最終的にユーザーに表示されるウェブページはHTML、CSSとJavascriptから構成されますが、それらを生成するために多くの技術を使用します.
HTMLはJavascriptを読み込むための最小限のテンプレートのみです.
実際のページの描画はJavascript(React)で行います.
コーディングしたJavascriptがウェブページに表示されるindex.jsになるまでの流れは以下の通りです.
- ES20XXの新しい記法+ReactでJavascriptを書き
- Babelで古いブラウザ向けに変換
- Parcelでファイル間の依存関係を解消し1ファイルにパッキング
CSSも同様です.
- ES20XX – Javascriptの新しい記法.クラスやジェネレータなど。
- Babel – ES2015などの記法を旧来のブラウザでも表示できるように変換するコンパイラ.
- React – データが変更された際に自動でUIを更新したり、HTMLをパーツごとに分けて書けるようになるライブラリ.
- Redux – Reactでの状態管理をサポートするライブラリ.
- Saga – Reduxでは扱えない非同期処理や副作用のある処理をサポートするライブラリ.
- web3 – Javascriptからブロックチェーンにアクセスするためのライブラリ.
- Stylus – 人に読みやすい記法のCSS.
- Parcel – HMTLで使用する画像やJavascript/CSSの依存関係を解決し、ReactやStylusをJavascriptやCSSに変換する.開発用サーバーも同梱されている。
# ブロックチェーン
イーサリアムブロックチェーン上では「スマートコントラクト」(明確な定義は無い)と呼ばれる任意のロジックを保存し、いつでも実行可能な状態にすることができます.
例えば2つの数字を足し算するような簡単なロジックから、ユーザーごとに残高の保存と送金をする銀行のような複雑なロジックまで実行可能です.
このスマートコントラクトを使って今回はユーザーがメッセージを送信・保存・取得できるロジックを書いていきます.
スマートコントラクトの実装には、現状一番ポピュラーなSolidityという言語を使います.
# 事前準備
# NodeJSの準備
Javascriptで開発しますので、手元で動かせるようにNodeJSをインストールして下さい.
- NodeJS 8以上
- NPM 5.6以上
# Ubuntu
以下の通り1行ずつコマンドを実行していって下さい.
sudo apt-get clean && apt-get update && apt-get install -qy curl perl g++ git python make
curl -L curl -L git.io/nodebrew | perl - setup
export PATH $HOME/.nodebrew/current/bin:$PATH
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> $HOME/.bashrc
nodebrew install-binary 9.9.0 && nodebrew use 9.9.0
# Mac
以下のサイトを参考にインストールして下さい.
https://qiita.com/taketakekaho/items/dd08cf01b4fe86b2e218
# Windows
以下のサイトを参考にインストールして下さい.
https://qiita.com/satoyan419/items/56e0b5f35912b9374305
# Dappsテンプレートの準備
Dapps作成のためのテンプレートを用意しています.
それをダウンロードし、起動してみましょう.
以下のコマンドを1行ずつ順に実行して下さい.
git clone git@github.com:shunsukehondo/DTwitter.git
cd DTwitter
git checkout v3-initial
npm install
npm start
※git clone
がエラーになる方は、以下のコマンドに置き換えて実行して下さい.
git clone https://github.com/shunsukehondo/DTwitter.git
開発用サーバーがうまく起動したようであれば、
にアクセスしてデモと同じUIが表示されることを確認して下さい.
見た目はそっくりですが、実はただのHTMLなので送信ボタンを押してもアラートが出るだけの状態です.
# テンプレートのフォルダ構成
先程開発用サーバーを立ち上げてUI表示したプロジェクトの構成をみてみましょう.
DTwitter
|-src
|-components // UIのパーツ
|-constants // 定数、特にReduxのアクション
|-contracts // ブロックチェーンへのアクセス情報
|-helpers // 便利な関数など
|-reducers // Reduxのアクションに応じて状態を変更する
|-sagas // Reduxのアクションに応じて非同期処理などを実行する
|- index.html // エントリーポイント.ユーザーはここにアクセスする。
|- index.js // index.htmlから読み込まれる唯一のJS.他すべてのJSをとりまとめる。
|- store.js // アプリ全体で共有の状態格納場所
|-dist // コンパイル後のファイルの格納先.実際にサーバーに配置するもの。
|-contracts // スマートコントラクトのソースコード格納先.
|- package.json // npm installでインストールされる依存ライブラリやnpm startで起動するコマンドなどが記述されている.
# コントラクトの実装
# 設計
今回作成するスマートコントラクトでは
- メッセージの送信・保存
- メッセージ一覧の取得
機能が最低限必要です.
単純化のためにメッセージは最大で最新20件を取得することとします.
メッセージには以下の情報をもたせます.
- ID(1以上、連番)
- 送信者
- テキスト
- 送信時間(unixtime)
その他にも新着のメッセージがあった時に自動でUIが更新出来るようにイベントがあると便利です.
Solidityではevent
とemit
という構文でそれができ、Javascript側で監視することができます.
また、Solidityでは現状コントラクトは構造体や配列の配列を返すことが出来ません.そのため、メッセージ一覧の取得は、
- メッセージIDのリストを取得
- メッセージIDからメッセージ本体を取得
する2つのインターフェイスに分けます.
以上をまとめると合計4つのインターフェイスを定義します.
- sendMessage – メッセージ送信
- getMessages – 最新のメッセージID20件が入った配列を取得
- getMessage(id) – メッセージIDからメッセージ本体を取得
- MessageSent – メッセージが送信された時に発火するイベント
# IDE Remix
今回はスマートコントラクトの実装にRemixを使用します.
ブラウザ上で動作するSolidityのIDEです.
エディタ、コンパイラ、デバッガ兼デプロイヤーが含まれます.
つまりこれ一つでコーディングから公開まで完了します.
以下のリンクから開いて下さい.
# 新規スマートコントラクトの作成
Remixを開くとデフォルトのファイルが表示されるのでクローズします.
画面左上の「+」ボタンからファイルの新規作成をします.
ファイル名は「DTwitter.sol」として下さい.
特に拡張子は「sol」で無いと動作しない可能性があります.
# コントラクトの作成
今回はコンパイラのバージョンを0.4.23で固定します.
pragma solidity ^0.4.23;
contract Dtwitter {
}
# メッセージ構造体
...skip...
contract Dtwitter {
struct Message {
address sender;
string text;
uint created_at;
}
}
address
型はイーサリアムユーザーやコントラクトのアドレスを格納する変数型です.
例えば0xe31c5b5731f3Cba04f8CF3B1C8Eb6FCbdC66f4B5
のような値です.
string
は文字列、uint
は0以上の整数です.
# メッセージ配列
...skip...
struct Message {
address sender; // 送信者
string text; // メッセージ本文
uint created_at; // マイニングされた時間
}
// 全メッセージです.配列上のインデックスがIDになります。
Message[] public messages;
}
関数内ではなく、コントラクト直下に定義する変数を状態変数といいます.
ある型の配列は[]
で宣言します.
状態変数にpublic修飾子をつけるとゲッタが自動で定義されます. 状態変数はデフォルトでstorageです。
# memoryとstorageについて
Solidity変数の格納先にはmemoryとstorageの2つがあります.
memory変数は実行時にメモリ内に展開され実行終了とともに破棄されます.
一方で、storage変数はブロックチェーン上へ書き込まれ永続化されます.
変数をmemoryにするかstorageにするかはmemory
、storage
修飾子をつけることで上書きできます.
コントラクト直下の状態変数はデフォルトですべてstorage、関数内の変数は構造体と配列がすべてstorage、その他がmemoryとなっています.
ブロックチェーンへの書き込みは手数料が伴います.
手数料はトランザクションを発行したユーザーが支払うことになるので、コントラクト実装者は細心の注意が必要です.
# コンストラクタ
...skip...
// 全メッセージです.配列上のインデックスがIDになります。
Message[] public messages;
constructor() public
{
// Id=0を欠番用にするためのダミーメッセージ
messages.push(Message(0, "", now));
}
}
コントラクトのコンストラクタは公開時の1度だけ呼ばれる関数です.
公開者だけが呼び出せる唯一の関数なので、公開者だけが特権的に実行したい処理などをここに書きます.
ここではIDが0のメッセージを新規作成して配列に(ブロックチェーンに)保存しています.
これはSolidityで配列の中身が無い場合に0が入るので、IDが0なのか配列の中身が空なのか区別出来ません.
ID 0が使われないように潰してしまいます.
now
は定義済みの変数で、最後にマイニングされたブロックのタイムスタンプが取得できます.
今回はわかりやすさのため、これをメッセージ送信時間に使っています.
# メッセージ送信関数 (1/4)
...skip...
// Id=0を欠番用にするためのダミーメッセージ
messages.push(Message(0, "", now));
}
// textというメッセージを送ります.
function sendMessage(string text) external returns(uint)
{
// 空メッセージを送ろうとすると処理が停止します
require(bytes(text).length > 0);
// pushは配列長を返します. -1すると最後のインデックスになります.
uint id = messages.push(Message(msg.sender, text, now)) - 1;
return id;
}
string型のテキストを受け取ってuint型のメッセージIDを返します.
require
は条件式がfalse
になる場合に処理をその場で中断させられるので、バリデーションに使用します.
bytes(text).length
というのは文字列長を取得するための書き方です.
msg.sender
は定義済み変数で、トランザクション発行者のアドレスが取得できます.
ウェブページから送信ボタンを押したユーザーのアドレスが格納されます.
messages.push
でブロックチェーンに書き込まれますが、この時に配列の長さが返ります.
配列上のインデックスをメッセージIDとするので、-1
するとIDになります.
# メッセージIDリストの取得関数 (2/4)
...skip...
return id;
}
// 最新20件のメッセージを取得します.
// viewを指定することでブロックチェーンへの書き込みが出来なくなり、手数料がなくなります.
function getMessages() external view returns (uint[20])
{
uint[20] memory results;
// 全メッセージが20件未満の場合はそこでループ停止
uint max = messages.length > 20 ? 20 : messages.length - 1;
for (uint i=0; i < max; i++)
{
uint msgId = messages.length - 1 - i;
results[i] = msgId;
}
return results;
}
}
# public、external、internal、private
関数につけるアクセス修飾子には4種類あります.
アクセス制限がゆるい順に以下のようになっています.
- public – コントラクト外部・内部から呼べる
- external – コントラクト外部からしか呼べない制限をする分、publicよりも手数料が安い
- internal – コントラクト内部または継承先コントラクト内部
- private – コントラクト内部
# pure、view
関数につけるブロックチェーンアクセスの修飾子は3種類あります.
制限がゆるい順に以下のようになっています.
- 何もつけない – ブロックチェーン読み書き可.送金可。
- view – ブロックチェーン読み出しのみ可.書き込み不可。送金不可。
- pure – ブロックチェーン読み書き不可.送金不可。
ちなみに、EVMのオペコードの中でブロックチェーン書き込みは最も手数料の高いオペレーションです.
# メッセージ本体取得関数 (3/4)
...skip...
return results;
}
// メッセージIDからメッセージデータを取得します.構造体は返せないので多値を返しています。
function getMessage(uint id) external view returns(uint, address, string, uint)
{
// IDが不正ならここで処理が停止します
require(id < messages.length);
// 注意:関数内の構造体と配列はデフォルトでブロックチェーンに書き込まれます(実行ごとに手数料がかかります).
// memoryを指定することでそれを避けられます.
Message memory message = messages[id];
return (id, message.sender, message.text, message.created_at);
}
}
新しく出てきたのは多値です.
配列と異なるのは、複数の型を混ぜられることです.
構造体はそのまま返せないので、メンバー変数を並べて返しています.
Javascript側からは[id, sender, text, create_at]
という長さ4の配列として取得できます.
実はブロックチェーン書き込みよりもメモリ割当が高くつくケースもあります.リリース前には必ずテスト環境で手数料を確認しましょう。
# メッセージ送信イベント (4/4)
ユーザーがメッセージを送信した時に全ユーザーがそれを検知してUIを更新出来るようにイベントを定義します.
まずはイベント本体です.
...skip...
struct Message {
address sender; // 送信者
string text; // メッセージ本文
uint created_at; // マイニングされた時間
}
// メッセージ送信時に発火させるイベントです.
// indexedを指定することで、Javascript側からフィルタ出来るようになります.
event MessageSent ( uint indexed id, address indexed sender, string text, uint created_at);
...skip...
MessageSentの中に引数を定義しておくことで、イベント発火時にそれらの値を参照することが出来ます.
indexed
を指定すると、イベントをフィルタ出来るようになります.
次にイベント発火部分を定義します.
...skip...
// pushは配列長を返します. -1すると最後のインデックスになります.
uint id = messages.push(Message(msg.sender, text, now)) - 1;
// メッセージが送信されたというイベントを発火します.
emit MessageSent(id, msg.sender, text, now);
return id;
}
}
これでスマートコントラクトの実装は以上です.
完成版のソースコードは、プロジェクトのcontracts/DTwitter.solに格納されています.
# コンパイルとデプロイ
「Start to compile」をクリックしてコンパイルを実行します.
(自動で実行されている可能性もあります.)
緑色の背景でDTwitterと表示されたら完了です.
「Run」タブに移動します.
ネットワークが「Ropsten」に接続され、アカウントの残高があることを確認します.
設定出来ていない場合は、以下のページを参考にMetaMaskの設定をしておいて下さい.
確認が終わったら「Deploy」をクリックします.
MetaMaskが自動で立ち上がる(立ち上がらない場合は手動で開いて下さい)ので、トランザクション手数料である「Gas Price」を1以上に変更します.
「SUBMIT」をクリックしてトランザクションを送信します.
世界中の誰かがマイニングしてくれるまでしばし待ちます(通常30秒程度).
画面中央下部に以下のようなログが出たらマイニング完了です.
# コントラクトの動作確認
ウェブページから接続する前に、スマートコントラクトが正しく動作するかRemixからデバッグしておきましょう.
トランザクションがマイニングされるとコントラクトが全世界に公開されます.
そのコントラクトのアドレスが「Deploy」ボタンの少し下方に表示されるので開くボタンをクリックします.
コントラクトに定義されているメソッド一覧が表示されます.
メッセージを送信してみましょう.
sendMessage右の開くボタンをクリックします.
textに"Hello"
と適当なメッセージを入力して、「transact」をクリックします.
先程と同様にMetaMaskが立ち上がるので、Gas Priceを設定して「SUBMIT」します.
コンソールに先程と同じマイニング完了のログが表示されるまで待ちます.
完了したら、送信したメッセージが取得できるか確認してみましょう.
getMessage右の開くボタンをクリックします.
IDに1
と入力し、「call」ボタンをクリックします.
"Hello"
と表示されていたらOKです.
コントラクトの動作確認は以上です.
# ウェブページからコントラクトへのアクセス情報の設定
以上でコントラクトは全世界に公開されました.
誰でもメッセージの送信や閲覧が出来る状態です.
コントラクトにアクセスするには、
- コントラクトアドレス
- コントラクトABI(Application Binary Interface)
の2つの情報が必要です.
ABIにはそのコントラクトが持っている関数の情報などが記載されています.
これらはRemixから取得できます.
# コントラクトアドレスの設定
まずはアドレスから設定しましょう.
以下のボタンをクリックするとアドレスがクリップボードにコピーされます.
プロジェクトのsrc/contracts/index.jsに情報をまとめるためのオブジェクトを予め作成しておきました.
address
の値に今コピーしたアドレスを貼り付けて下さい.
以下のようになります.
※アドレスは当然異なります
export const dtwitterContract = {
address: "0x88fe4377be9d2c26f4ca1420a6d10b5c3728c91c",
abi: []
}
# ABIの設定
次にABIを設定していきます.
「Compile」タブに移動します.
「Detail」ボタンをクリックして下さい.
上から4つ目のABIの欄に情報が書いてあります.
コピーボタンをクリックして、クリップボードにコピーして下さい.
先程のsrc/contracts/index.jsを開いて下さい.
abi
の値の[]
を消し、その場でクリップボードの内容をペーストして下さい.
以下のような状態になります.
※100行以上あるので以下省略
export const dtwitterContract = {
address: "0x88fe4377be9d2c26f4ca1420a6d10b5c3728c91c",
abi: [
{
"anonymous": false,
"inputs": [
...skip...
コントラクトへの接続情報の設定は以上で完了です.
# ウェブページの設計
ここからウェブページの作成に入っていきます.
まずは設計を見ていきます.
Reactでは画面に必要なパーツに応じてComponentという単位に分けます.
さらにReduxでは、Componentは状態を表示するだけのPresentational Componentと、ロジックを担うContainer Componentに分けて設計するのが主流です.
この記事でLogic Componentと呼んでいるのは後者の方です.
このアプリでは3つのコンポネントとして実装します.
- メッセージ入力部分 – MessageInput コンポネント(Presentational)
- メッセージ一覧部分 – Timeline コンポネント(Presentational)
- ロジック – App コンポネント(Logic)
※本来はLogic ComponentにHTMLを含めないのが望ましいですが、今回は簡単のため、入れ物となる最低限のHTMLを含めています.
# ウェブページのデータフロー
ウェブページがブロックチェーンからデータを取得し、それがページ上に表示されるまでの流れを説明します.
# ①Logic Component->Dispatcher
Reduxではアプリ全体の状態が1つのStoreで管理されます.
そしてこのStoreを変更する権利をもっているのはReducerだけです.
ReducerはDispatcherからActionを受取り、Actionに応じてStoreの状態を変更します.
DispatcherにActionを送ることが出来るのは、Logic ComponentまたはSaga(後述)だけです.
# ②Dispatcher->Saga
通信や副作用を伴う処理はReduxでは出来ないので、Sagaが受取ります.
# ③Saga<->MetaMask+web3
ブロックチェーンとの通信のActionを受け取ったSagaは、イーサリアムブロックチェーンと通信するためのライブラリであるweb3を使ってブラウザ拡張であるMetaMaskにアクセスします.
# ④MetaMask<->Blockchain
イーサリアムブロックチェーンのネットワークに参加しているノードと通信することによって、ブロックチェーンの情報を読み書きできます.
MetaMaskには予めノードが登録してあるので、特に意識せずに通信出来ます.
# ⑤Saga->Dispatcher
ブロックチェーンから情報を取得してもSagaはStoreを書き変えることが出来ません.
これでは情報をただ捨てることになります.
そこでStore書き換え用のActionを、ブロックチェーンから取得した情報と一緒にDispatcherに送ります.
# ⑥Dispatcher->Reducer
状態を書き換えるだけの処理はReducerが受取ります.
# ⑦Reducer->State
受け取ったアクションに応じてStoreの中のStateを書き換えます.
※実際にはStateは不変なので、状態を変更した全く新しいStateが生成されます.
# ⑧State->Logic Component
Stateのいかなる変更があった場合もLogic Componentに通知されます.
新しい状態を受け渡します.
# ⑨Logic Component->Presentational Component
受け取った新しい状態を、実際に画面に表示されるPresentational Componentに対して渡して画面を更新させます.
以上が、ブロックチェーンから通信でデータを取得して画面を更新するまでの流れです.
# HTMLをコンポネントに分割する
先程説明したようにHTMLを3つのコンポネントに分割していきます.
# Timelineコンポネントの実装
src/componentsディレクトリにTimeline.jsというファイルを新規作成し、以下の内容を入力して下さい.
import React from 'react'
class Timeline extends React.Component {
render () {
return (
// TODO
)
}
}
export default Timeline
ReactのComponentはReact.Component
を継承することで作成します.
render
メソッドの中でHTMLとよく似たJSXというものを返すと、画面に表示されます.
src/index.htmlの以下の部分を切り取り、Timelineの// TODO
の行を消してペーストして下さい.
<div id="timeline">
<ul class="collection">
<li class="collection-item" key="1">
<span class="title">From user1</span>
<p>
<i class="prefix tiny material-icons">alarm</i>
<span class="message-date">10/07/2018 10:00</span>
<br />
<span>Hello, Im user1.</span>
</p>
</li>
<li class="collection-item" key="2">
<span class="title">From user2</span>
<p>
<i class="prefix tiny material-icons">alarm</i>
<span class="message-date">10/07/2018 9:00</span>
<br />
<span>Hi Im user2.</span>
</p>
</li>
</ul>
</div>
今ペーストしたものから、<!-- -->
というHTMLのコメント行は削除して下さい.
以下のようになります.
...skip...
class Timeline extends React.Component {
render () {
return (
<div id="timeline">
<ul class="collection">
...skip...
</ul>
</div>
)
}
}
...skip...
ReactのJSXではclass
の代わりにclassName
を使います.
そのためTimeline.jsファイル中のすべてのclass=
をclassName=
に置換して下さい.
※注:class
で置換すると予期せぬところまで置換してしまいます.
次に、このコンポネントに適用するスタイルシートをimportします.
以下のimport文を追記して下さい.
import React from 'react'
import "./Timeline.styl"
class Timeline extends React.Component {
// ...skip...
# MessageInputコンポネントの実装
Timelineと全く同じ要領で実装していきます.
src/components/MessageInput.jsを新規作成し、以下の内容を入力します.
import React from 'react'
import "./MessageInput.styl"
class MessageInput extends React.Component {
render () {
return (
// TODO
)
}
}
export default MessageInput
次にsrc/index.htmlから以下を切り取り、renderメソッドの// TODO
の箇所にペーストします.
HTMLのコメント行は削除して下さい.
以下のようになります.
import React from 'react'
import "./MessageInput.styl"
class MessageInput extends React.Component {
render () {
return (
<div id="messageinput" class="blue">
...skip...
</div>
)
}
}
export default MessageInput
こちらもTimelineの時と同様にclass=
をclassName=
に置換して下さい.
# App コンポネントの実装
最後にLogic ComponentであるAppを実装していきます.
すでに雛形はsrc/components/App.jsに用意してあります.
App.jsを開いて、2箇所修正し以下のようにして下さい.
...skip...
import * as actionTypes from '../constants/actionTypes'
import MessageInput from './MessageInput'
import Timeline from './Timeline'
class App extends Component {
...skip...
render() {
return (
<div className="App">
<MessageInput />
<Timeline />
</div>
)
}
}
...skip...
Reactではこのように自分の作ったコンポネントをまるでHTMLのタグのように表示することが出来ます.
# index.htmlの修正
さて、1枚のHTMLからReactのコンポネントに分割してきたので、不要になった箇所などを修正します.
以下の2行を削除して下さい.
<link rel="stylesheet" href="components/Timeline.styl">
<link rel="stylesheet" href="components/MessageInput.styl">
これらはコンポネントのファイルにimportするようにしましたね.
idがrootのdivの中身をすべて削除して空にして下さい.
...skip...
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
...skip...
Reactがこの中に動的にコンポネントを組み立てていきます.
HTMLからindex.jsを読み込んでいる箇所がコメントアウトしてあるので、最後にそれを有効化します.
以下のようになります.
...skip...
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
index.jsというのはすべてのJavascriptファイルの大本になるファイルで、React、Storeなどはすべてこの中で読み込まれ設定されています.
ここまで実装したら開発用サーバーを立ち上げて、HTMLの時と同じ内容が表示されていることを確認しましょう.
npm start
# MessageInputにMetaMaskから取得したユーザーアドレスを表示する
ここまではHTMLとほとんど変わらずDapps感が無いので、MessageInputにMetaMaskから取得したアドレスを表示させるようにしましょう.
状態(=UI上で変化する可能性のあるもの)を追加する際の手順は以下の通りです.
- INITIAL_STATE(reducers/index.jsで定義してあります)に変数追加
- 状態を変化させるActionを定数に追加
- Actionに応じて状態を書き換える処理をReducerに追加
- Logic ComponentからPresentational Componentに状態を渡す
- Presentational Componentで状態を表示する
今回使っているDappsテンプレートでは、ユーザーアドレスは多くのDappsで使われるため、userAddress
という変数名で定義済みです.
src/reducers/index.jsに以下のように定義してあります.
const INITIAL_STATE = {
userAddress: "0x0",
web3: null,
contractInstance: null,
}
1〜3が完了している状態です.
4と5を実装していきます.
# Logic ComponentからPresentationalに渡す
src/components/App.jsを開き、renderの中身を以下のように修正して下さい.
...skip...
<div className="App">
<MessageInput userAddress={ this.props.userAddress }/>
<Timeline />
</div>
...skip...
Stateの中身はthis.propsで取得できます.
# Presentationalで状態を表示する
src/components/MessageInput.jsのrenderの中身を以下のように修正して下さい.
user1
とハードコードされていた部分を{ this.props.userAddress }
に置き換えます.
<div className="input-field col s10">
<i className="prefix material-icons">chat</i>
<input ref="txtMessage" type="text" placeholder="Type your message" />
<span className="chip left white">
<span>You: { this.props.userAddress }</span>
</span>
</div>
http://localhost:3000を開き、MetaMask上のアドレスと同じ値が表示されているのが確認出来たらOKです.
# タイムラインをブロックチェーンから取得する
まずはsrc/reducers/index.jsのINITIAL_STATEにメッセージ一覧を保存する変数として、messages
を追加して下さい.
const INITIAL_STATE = {
userAddress: "0x0",
messages: [],
web3: null,
contractInstance: null,
}
次にActionを追加します.
タイムライン用に必要なアクションは以下の2つです.
- FETCH_TIMELINE_REQUESTED: タイムライン取得通信開始
- FETCH_TIMELINE_SUCCESS: タイムライン取得完了・状態更新
1は非同期なのでSagaに処理させ、2は同期の状態更新なのでReducerに処理させます.
src/constants/actionTypes.jsを開き、上記2つを追加して下さい.
export const FETCH_TIMELINE_REQUESTED = "App/FETCH_TIMELINE_REQUESTED"
export const FETCH_TIMELINE_SUCCESS = "App/FETCH_TIMELINE_SUCCESS"
次にReducerを追加します.
以下のようにcase文を1つ追記して下さい.
...skip...
case actionTypes.FETCH_TIMELINE_SUCCESS:
return Object.assign({}, state, {
messages: action.payload
})
...skip...
Object.assign
というのは既存のstateをコピーして、指定したパラメータだけを更新するという関数です(stateは不変なのを覚えていますか).
payloadという変数にメッセージ一覧が渡ってくるので、それをそのままtimelineに入れています.
次にSagaを実装していきます.
src/saga/index.jsを開いて下さい.
まずは通信処理のコルーチンを、ジェネレータを使って実装していきます.
コルーチンを使うことで、通信のコールバックのような前後関係の複雑な処理を同期処理のように上から下へ一直線に書くことが出来ます.
メッセージ一覧の取得には2段階ありましたね.
まずIDの一覧を取得して、その後それぞれの内容を取得します.
まずはJavascript側からgetMessagesを呼び出す関数を作成します.
ファイル内のどこでも構いません.
const getMessages = (contract, userAddress) => {
return new Promise(function(resolve, reject) {
contract.getMessages.call({from: userAddress}, (err, messageIds) => {
resolve(messageIds)
})
})
}
contract.getMessages.call
がコントラクト呼び出しの本体です.
引数のfrom
には必ず呼び出したユーザーのアドレスを入れます.
ブロックチェーンとの通信が終わるとコールバックが実行されてresolve(messageIds)
が実行されます.
resolveにmessageIdsを渡すと、後述のジェネレータ側から取得できます.
getMessageもほとんど同様です.
違いはメッセージIDの分だけ引数が1つ増えていることぐらいです.
const getMessage = (contract, userAddress, msgId) => {
return new Promise(function(resolve, reject) {
contract.getMessage(msgId).call({from: userAddress}, (err, msg) => {
resolve(msg)
})
})
}
それではいよいよこれまで定義した2つの関数を使ってジェネレータでコルーチンを実装していきます.
function*
で関数がジェネレータになります.
ジェネレータとは途中で処理を中断してまた同じ位置から再開できる関数です.
以下のジェネレータを先程の関数の下に追加して下さい.
function* fetchTimelineAsync() {
const { contractInstance, userAddress } = yield select()
const msgIds = yield getMessages(contractInstance, userAddress)
const getMessageWorkers = []
for (var i=0; i<msgIds.length; i++) {
// 0はそれ以上メッセージが無いということ
if (msgIds[i] == 0) break;
getMessageWorkers.push(call(getMessage, contractInstance, userAddress, msgIds[i]))
}
// すべての通信完了を待つ
const messages = yield all(getMessageWorkers)
// フォーマットを整える
const msgObjs = messages.map(msg => {
return {
Id: msg[0],
Who: msg[1],
What: msg[2],
When: msg[3]
}
})
// 投稿時間順にソート
const sorted = msgObjs.sort(messageComparer)
yield put({ type: actionTypes.FETCH_TIMELINE_SUCCESS, payload: sorted})
}
call, all, select, putなどの関数がSagaの関数です.
- select – stateの内容を読み出し
- call – 関数呼び出し
- all – すべてのジェネレータが完了するまで待機
- put – DispatcherにActionを送信
例えば、以下の箇所で通信を待機していますが、普通の関数ならば通信を投げた直後に処理は先に進んでしまい、通信完了後に次の行へ進むというようなことが出来ません.
const msgIds = yield getMessages(contractInstance, userAddress)
...skip...
const messages = yield all(getMessageWorkers)
今回は必ずメッセージID一覧を取得した後でないとメッセージ本体を取得出来ないので通信完了まで次の行へ進まずに待機していてほしいのです.
ところが普通の関数で次の行に進まないということは、プログラム全体が停止するということなので、ユーザーから見ると画面がフリーズするということです.
ジェネレータは途中で中断してまた同じ場所から再開できる関数でしたね.
中断と再開を高速に繰り返すことで、通信完了をチェックしながら待機しつつ、同時に、プログラム全体が停止してしまわないようにしているのです.
次に、今定義したfetchTimelineAsyncが、FETCH_TIMELINE_REQUESTEDが来たら起動するようにしましょう.
ファイル一番下のrootSagaに以下のように1行追記して下さい.
export default function* rootSaga ()
{
yield takeLatest(actionTypes.FETCH_TIMELINE_REQUESTED, fetchTimelineAsync)
yield fork(fetchWeb3ConnectionAsync)
yield fork(watchAndLog)
}
あとはLogic ComponentからPresentational Componentにメッセージ一覧を渡して、表示するだけです.
src/component/App.jsを開いて、以下のように修正して下さい.
<div className="App">
<MessageInput userAddress={ this.props.userAddress }/>
<Timeline messages={ this.props.messages }/>
</div>
src/components/Timeline.jsを開いて、以下のように修正して下さい.
これまでダミーのメッセージ2件表示していたものを、messagesを取得しmapでそれぞれ<li>
要素に変換しています.
...skip...
return (
<div id="timeline">
<ul className="collection">
{
this.props.messages.map((msgObj) => {
const msgDate = new Date(msgObj.When * 1000)
const msgDateTime = msgDate.toLocaleDateString() + ' at ' + msgDate.toLocaleTimeString()
return (
<li className="collection-item" key={msgObj.Id}>
<span className="title">From: {msgObj.Who}</span>
<p>
<i className="prefix tiny material-icons">alarm</i>
<span className="message-date">{msgDateTime}</span>
<br />
<span>{msgObj.What}</span>
</p>
</li>
)
})
}
</ul>
</div>
)
...skip...
http://localhost:3000でここまでの動作を確認してみましょう.
ページを開いた時にRemix上から動作確認時に送信したメッセージが表示されていたらOKです.
ブロックチェーンからのデータ取得に成功です.
# メッセージを送信する
メッセージ送信機能を実装していきます.
新しい状態は不要なので、INITIAL_STATEはそのまま変えません.
メッセージ送信の通信Action(Sagaが受け取る)を追加します.
加えて、メッセージマイニング中に何も表示されないと本当に送信されたかユーザーが不安になるので、ダミーのメッセージを1件追加するためのActionも追加します.
src/constants/actionTypes.js
export const SEND_MESSAGE_REQUESTED = "App/SEND_MESSAGE_REQUESTED"
export const ADD_MESSAGE = "App/ADD_MESSAGE"
メッセージを1件追加するためのReducerを追加します.
src/reducers/index.jsを開いて以下のように追記して下さい.
...skip...
case actionTypes.ADD_MESSAGE:
// リストの先頭に追加
return Object.assign({}, state, {
messages: [action.payload].concat(state.messages)
})
default:
...skip...
次に、Sagaにメッセージ送信用のコルーチンを追加します.
src/sagas/index.jsを開いて以下を追記して下さい.
function* sendMessageAsync(action) {
const { contractInstance, userAddress } = yield select()
const text = action.payload
// 結果は使わないので捨てる
yield sendMessage(contractInstance, userAddress, text)
// マイニング完了まで表示する仮メッセージ
const dummyMsg = {
Id: Math.floor(Math.random() * 100000000),
Who: userAddress,
What: text,
When: (new Date().valueOf()) / 1000
}
yield put({ type: actionTypes.ADD_MESSAGE, payload: dummyMsg})
}
const sendMessage = (contract, userAddress, msg) => {
return new Promise(function(resolve, reject) {
contract.sendMessage.sendTransaction(msg, {from: userAddress}, (err, result) => {
resolve(result)
})
})
}
さらにSEND_MESSAGE_REQUESTEDが送信された際に、sendMessageAsyncを実行するように登録します.
src/sagas/index.jsの最下部のrootSagaを以下のように修正します.
export default function* rootSaga ()
{
yield takeEvery(actionTypes.SEND_MESSAGE_REQUESTED, sendMessageAsync)
yield takeLatest(actionTypes.FETCH_TIMELINE_REQUESTED, fetchTimelineAsync)
yield fork(fetchWeb3ConnectionAsync)
yield fork(watchAndLog)
}
次にユーザーが画面上の送信ボタンを押した時にSEND_MESSAGE_REQUESTEDが実行されるようにします.
src/components/App.jsを開き、もともと空になっているmapDispatchToPropsを以下のように修正して下さい.
function mapDispatchToProps (dispatch, ownProps) {
return {
sendMessage: (msg) => dispatch({ type: actionTypes.SEND_MESSAGE_REQUESTED, payload: msg })
}
}
mapDispatchToPropsはthis.props
に関数を登録するためのものです.
その直後のconnect
でAppと接続しています.
MessageInputに今定義したsendMessageを渡します.
Appのrenderメソッドを以下のように修正して下さい.
<div className="App">
<MessageInput userAddress={ this.props.userAddress } sendMessage={ this.props.sendMessage }/>
<Timeline messages={ this.props.messages }/>
</div>
MessageInputに送信ボタンクリック時の処理を書いていきます.
Presentational Componentなので複雑なロジックは書かずに、見た目の制御とクリックイベントをAppに渡すことだけに専念します.
...skip...
class MessageInput extends React.Component {
onSubmit(e) {
// デフォルトの送信ボタンの挙動(ブラウザリロード)を禁止
e.preventDefault()
// Inputに入力されているテキストを取得
const msg = this.refs.txtMessage.value
this.props.sendMessage(msg)
// 送信後は空にする
this.refs.txtMessage.value = ""
// 再び入力欄にフォーカスを戻す
this.refs.txtMessage.focus()
}
render () {
...skip...
Reactではフォームに入力されている内容は、this.refs
で取得できます.
HTMLでは<input ref="txtMessage" ...skip... />
と書いておくと、this.refs.txtMessage
で取得出来るようになります.
送信ボタンをクリックした際にonSubmit
を呼び出すように設定します.
formのonSubmitを以下のように修正して下さい.
render () {
return (
<div id="messageinput" className="blue">
<form className="container" onSubmit={ this.onSubmit.bind(this) }>
bind(this)
はonSubmitの中でthis
にアクセスするために必要です.
Reactが用意している関数、componentWillMount、renderなどはthis
が予め定義されていますが、自分で作成したonSubmitのような関数ではデフォルトでthis
が使用できません.
bind(this)
を実行することで、this
にアクセス可能になります.
http://localhost:3000にアクセスしてメッセージが送信出来るようになったことを確認しましょう.
# 新着メッセージがあったらリストを更新する
このアプリでは2つのタイミングでメッセージ一覧をブロックチェーンから取得します.
- ウェブページを開いて、ブロックチェーンとの接続が完了した直後
- 誰かのメッセージがマイニングされた時
src/sagas/index.jsのfetchWeb3ConnectionAsyncを以下のように修正します.
これはMessageSentイベントが発生したら、FETCH_TIMELINE_REQUESTEDをディスパッチャーに送るように監視する、という意味です.
...skip...
instance.MessageSent()
.watch((err, result) => {
store.dispatch({ type: actionTypes.FETCH_TIMELINE_REQUESTED })
})
...skip...
そしてもう1つ、fetchWeb3ConnectionAsyncの一番下に
...skip...
})
yield put({ type: actionTypes.FETCH_TIMELINE_REQUESTED })
}
...skip...
http://localhost:3000にアクセスして、デモと同じ動作をすることを確認して下さい.
お疲れ様でした!
以上で完成です.
最後までお読みいただきありがとうございました.
Dapps関連情報の発信をしているので、ぜひ@shunsukehondoのフォローをお願いします.