BlockstackでDApp入門 Part 3 ~ブログアプリ作成~
チュートリアルものとしてはお馴染みのブログアプリの作成をやってみました。Rails Tutorialなんかでマイクロブログの章へ取り組んだ方も多いと思いますが、Scaffoldしてユーザ認証を確認、プロフィール表示やブログのポスト機能を追加するというお決まりの手順で実装してみたいと思います。
以下の機能がこのアプリケーションには必要です。
- Blockstackブラウザを使用したユーザ認証(Blockstack ID利用)
- ユーザプロフィールの表示
- ブログの投稿およびその表示(Gaiaストレージへ保存)
- 他ユーザの検索および同様にそのプロフィールとブログの投稿表示
開発はLinux(Ubuntu 16.04.1 LTS)で行い、以下の処理系を使用しました。公式サイトのチュートリアルは全て MacOS High Sierra 10.13.4を基本としているので適宜読み替えて実装しています。
$ node -v
v11.10.0
$ npm -v
6.7.0
アプリケーション雛形の作成
Yeoman
と generator-blockstack
をインストールすることでSPA(シングルページアプリケーション)をScaffoldできます。Ruby on Railsにもありましたが、手っ取り早く全体像を理解するにはScaffoldは便利です。[1]
$ sudo npm install -g yo
...Yeoman Doctor
Running sanity checks on your system? Global configuration file is valid
? NODE_PATH matches the npm root
? Node.js version
? No .bowerrc file in home directory
? No .yo-rc.json file in home directory
? npm version
? yo versionEverything looks all right!
+ yo@2.0.5
added 542 packages from 267 contributors in 46.187s
次に generator-blockstack
をインストールします。
$ sudo npm install -g generator-blockstack
+ generator-blockstack@0.5.0
added 347 packages from 230 contributors in 14.148s
アプリケーション用のフォルダを作成して、yo blockstack:reactコマンドでScaffoldします。Vulnerabilities(脆弱性)の警告が出た場合は、npm audit fixで解消してください。※yo blockstackのみのコマンドも有効なので注意、ここではyo blockstack:react
npm startでアプリケーションをローカル実行します。
http://localhost:8080
へアクセスしてBlockstackのランディングページが表示されることを確認してください。 『Sign in with Blockstack』をクリックして オリジン間リソース共有(cross-origin requests, CORs)のエラーが発生する場合は、Allow CORS: Access-Control-Allow-OriginというChrome拡張のプラグインを導入するかwebpackでビルドしてNetlifyなどでアプリをホスティングしてしまいましょう。[2]
Netlifyでホスティングしておくと、githubなどのリポジトリへプッシュするだけで自動でデプロイされるので便利です。[3]
他同様のサービスやCI(Continuous Intergration)環境をお持ちの方はそれらを利用するといいと思います。以下、Netlifyを使用してデプロイしたアプリケーションです。
BlockstackブラウザでBlockstack IDを設定している方は、アプリケーションへのサインインが完了します。プロフィールに設定している写真、メッセージおよび『Logout』ボタンが表示され、『Logout』をクリックすることでランディングページへと遷移します。
データ公開を許可
サインインやBlockstack IDによる認証部分はScaffoldで既に実装されています。認証時にGaiaストレージを利用するアプリケーションはアプリ情報をprofile.json
へ追加する必要があります。ユーザ認証の際にpublish_data
を追加することで、このアプリケーションでGaiaに保存された各ユーザのファイルがprofile.json
のapps要素内で確認できるようになり、また公開できるようになります。
認証部分のコードにpublish_data
を追加します。src/components/App.jsx
を開いてhandleSignIn関数を以下のように編集してください。
handleSignIn(e) {
e.preventDefault();
const origin = window.location.origin;
redirectToSignIn(origin, origin + '/manifest.json', ['store_write', 'public_data']);
}
publish_data
はGaiaのデータ公開のために追加しました、store_write
はGaiaへのデータ保持のために必要です。handleSignIn関数はユーザがサインインしていない状態の場合に呼び出されます。
render() {
return (
<div className="site-wrapper">
<div className="site-wrapper-inner">
{ !isUserSignedIn() ?
<Signin handleSignIn={ this.handleSignIn } />
: <Profile handleSignOut={ this.handleSignOut } />
}
</div>
</div>
);
}
ブログの実装
Gaiaストレージ対するファイルの授受にはblockstack.js
のgetfile()
およびputfile()
を利用します。GaiaではSQL、JSON、カスタムフォーマットなどタイプを問わずファイルを保存することが可能です。※純粋にファイルベースのストレージということです
またBlockstack IDを使用してユーザ情報を得るためにlookupProfile()
を追加します。src/components/Profile.jsx
で以下のようにgetfile() putfile()
lookupProfile()
をインポートしてください。
import React, { Component } from 'react';
import {
isSignInPending,
loadUserData,
Person,
getFile,
putFile,
lookupProfile
} from 'blockstack';
コンストラクタのthis.stateにユーザのプロフィールで使用されるプロパティの初期値を定義していきます。
constructor(props) {
super(props); this.state = {
person: {
name() {
return 'Anonymous';
},
avatarUrl() {
return avatarFallbackImage;
},
},
username: "",
newPost: "",
posts: [],
postIndex: 0,
isLoading: false
};
}
render部分は以下のように修正してください。Blockstack IDやユーザのプロフィール情報に加えて、ブログポストを入力するテキストエリアも描画するようになっています。
personおよびusernameの値をcomponentWillMountコンポーネントの中のsetStateで設定します。
componentWillMount() {
this.setState({
person: new Person(loadUserData().profile),
username: loadUserData().username
});
}
ブログテキストの入力および保存に使用する関数をsrc/components/Profile.jsx
内で定義します。
handleNewPostChange(event) {
this.setState({newPost: event.target.value})
}handleNewPostSubmit(event) {
this.saveNewPost(this.state.newPost)
this.setState({ newPost: "" })
}saveNewPost(postText) {
let posts = this.state.posts
let post = {
id: this.state.postIndex++,
test: postText.trim(),
created_at: Date.now()
} posts.unshift(post)
const options = { encrypt: false }
putFile('posts.json', JSON.stringify(posts), options)
.then(() => {
this.setState({ posts: posts })
})
}
ここまででputfile()
を使用したブログテキストのポストおよびBlockstack IDとユーザプロフィールの表示ができました。次はgetfile()
でGaiaへ保存したブログポストの読み込みおよびレイアウトの修正を行います。
src/components/Profile.jsx
の<div className=”new-status”>以下にgetfile()
で取得したブログポストを表示するコードを準備します。posts.mapで各ポストを展開しています。
<div className="col-md-12 posts">
{ this.state.isLoading && <span>Loading...</span> }
{ this.state.posts.map((post) => (
<div className="post" key={post.id}>
{post.text}
</div>
)
)}
</div>
ユーザのposts.jsonを取得してpostsへ格納するfetchData関数を定義します。isLoadingは真の間 this.state.isLoading && <span>Loading…</span> として読み込み中のステータスを表示するために作成しました。
fetchData() {
this.setState({ isLoading: true })
const options = { decrypt: false }
getFile('posts.json', options)
.then((file) => {
var posts = JSON.parse(file || '[]')
this.setState({
person: new Person(loadUserData().profile),
username: loadUserData().username,
postIndex: posts.length,
posts: posts,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
}
componentDidMountの中でfetchDataを呼び出します。componentWillMountやcomponentDidMountなどのReact Componentのライフサイクルについては以下記事が分かりやすかったです。[4]
componentDidMount() {
this.fetchData()
}
src/styles/styel.css
を以下の内容に置き換えてください。レイアウトを変更するためのスタイルシートです。CSSは好きなスタイルでも大丈夫です。
デプロイするとユーザプロフィールの下にポストの内容を入力するテキストエリアと『Submit a post』というボタンが表示されます。サブミットすると新たに入力した内容がポストとして画面上にインタラクティブに反映されていくはずです。以下は『Test post』『Lorem ipsum』『Decentralization』と3つのポストを追加した図。
他ユーザのプロフィール検索と表示
src/components/Profile.jsx
でlookupProfile()
をインポートしました。lookupProfile()
はBlockstack IDから対象ユーザのプロフィールオブジェクトを走査してオブジェクトを返します。http://localhost:8080/other_user.id
という形で他ユーザのプロフィールへアクセスできるようにreact-router
を使用していきます。パッケージをインストールしてください。
$ npm install --save react-router-dom
src/index.js
内でreact-router-domのインポートを行います。
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render()を以下のように編集します。
ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'));
renderはこちらです。
render() {
return (
<div className="site-wrapper">
<div className="site-wrapper-inner">
{ !isUserSignedIn() ?
<Signin handleSignIn={ this.handleSignIn } />
:
<Switch>
<Route
path='/:username?'
render={
routeProps => <Profile handleSignOut={this.handleSignOut} {...routeProps} />
}
/>
</Switch>
}
</div>
</div>
);
}
http://localhost:8080/other_user.id
のother_user.idのドットを処理できるようにアプリケーションルート配下のwebpack.config.jsを編集します。
historyApiFallback: {
disableDotRule: true
}
src/components/Profile.jsx
へ戻り、isLocalという関数を追加します。ユーザが自身のプロフィールを見ているか、他のユーザのプロフィールを見ているかを真偽で返す関数です。他のユーザのプロフィールを見ている場合の処理をfetchData関数へ追加します。
isLocal() {
return this.props.match.params.username ? false : true
}
isLocalによる分岐とfalseであった場合のusernameによる他のユーザプロフィールとブログポストの取得です。
fetchData() {
this.setState({ isLoading: true }) if (this.isLocal()) {
const options = { decrypt: false }
getFile('posts.json', options)
.then((file) => {
var posts = JSON.parse(file || '[]')
this.setState({
person: new Person(loadUserData().profile),
username: loadUserData().username,
postIndex: posts.length,
posts: posts,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
} else {
const username = this.props.match.params.username lookupProfile(username)
.then((profile) => {
this.setState({
person: new Person(profile),
username: username
})
})
.catch((error) => {
console.log('could not fetch profile')
}) const options = { username: username, decrypt: false }
getFile('posts.json', options)
.then((file) => {
var posts = JSON.parse(file || '[]')
this.setState({
postIndex: posts.length,
posts: posts
})
})
.catch((error) => {
console.log('could not fetch the posts')
})
.finally(() => {
this.setState({ isLoading: false })
})
}
}
他のユーザのプロフィール閲覧時においてはログアウトとポスト投稿のテキストエリアは不要なのでrenderの内容を修正します。{isLocal() && … }でHTMLをラップすることで他のユーザのプロフィール画面から非表示にできます。
まとめ
- React.jsを使用するBlockstackアプリケーションの作成ができた
- Blockstack IDによるユーザ認証が行えた
- Gaiaストレージを利用してユーザ所有のデータ保管ができた
- Blockstackを使用して簡単なブログアプリが開発できた