日時: | 2017-01-02 |
---|---|
作: | @voluntas |
バージョン: | 2.1.0 |
URL: | https://voluntas.githu.io/ |
突っ込みは Twitter @voluntas まで。
Contents
Erlang/OTP でミドルウェアを作っていると、組み込みの UI が欲しくなる。 ただビジネスの視点からみると開発コストが見合わないことが多く、ついつい後回しになりがちだった。
一念発起して、まずは自分で組み込み UI のプロトタイプを作れる技術を一通り学ぶことにした。
ここでは組み込みの UI を作る際に React を採用した際のノウハウをまとめていきたい。
ミドルウェア開発者も少しはフロントエンドができるようになってもいいんじゃないか?という考えで勉強してみている。
専門家にはなれないだろうけど、フロントエンドエンジニアと会話ができるようになるのが目標。
この資料は定期的に更新され、その時の最新版の情報にアップデートされている。
この資料に出てくるコードは以下の URL で取得可能
voluntas/react-dojo: React コトハジメ
- 全体的に新しいライブラリにアップデート
- Step7 として Flow を導入するを追加
- 大規模商用利用での Erlang/OTP によるプロダクション開発経験あり
- Erlang/OTP コントリビューター
- WebSocket チョットデキル
- JS/CSS/HTML 経験なし
つまり、書いてる本人 (@voluntas) が対象。
利用したいと考えている技術
- React
- Babel
- Material-UI
- FlowType
- ESLint
- Webpack
- WebSocket
- Alt
ベストプラクティスが、有識者から想定するフレームワークなどでいろいろ変わるというアドバイスを貰ったので事前に定義しておく。
- 通信は WebSocket 99% で非同期通信前提
- サーバ側は Erlang/OTP 前提
- サーバの管理画面として利用される
- リアルタイムにデータが流れてくる
- ログイン/ログアウト以外は非同期で行う
- デザインは Material-UI ベース
- React の仕組みを理解する
- Erlang/OTP で書かれたミドルウェアの UI を React + `Material-UI`_ を利用して開発、提供できるようになる
- ES6 前提で書く
- 必要になるまで新しい機能は覚えない
- 覚える前にベストプラクティスをできる限り確認する
- 困ったら有識者に教えてもらう
- Alt - Flux - Managing your state
- A Javascript Library For Building Composable And Declarative Charts | React-D3
- グラフはこれがいいのかなーと思っている
- Grommet コトハジメ
- Grommet をざっと調べてみた
- Reusable Components | React
- Flow やれば自動的にやることになりそう
- reactjs/react-router: A complete routing library for React
- まだ複数画面を管理するところにはいたっていないので、様子見
- mizchi/flumpt
- redux よりわかりやすかったのでこれを使う予定
- 子から親の state を更新する際にうまいことできるしくみ、 redux は名前規則でやるが、こちらは親に持たせたメソッドを呼び出すという仕組みらしい
- UI-Router
まず一番最初に困ったのは、そもそも何をしたらいいのかわからなかったこと。とっかかりすら不明。ということで、いろいろ聞いたり調べたりしたので、有識者に相談しつつ自分がこれが良さそうというのを書いていくことにする。
基本的には以下のサイトをなぞっているだけ。
まずは Node.JS とパッケージマネージャである npm 環境を用意する必要がある。まずはここから。 普段 Erlang しか書いていないので、初 Node.JS 、初 npm 。
MacPorts 派なのでそれでインストール。
$ sudo port install nodejs6 npm4
react-dojo というフォルダを作って npm init -y で初期環境を作る
$ mkdir react-dojo $ cd react-dojo $ mkdir step1 $ cd step1 $ npm init -y
ビルドは今回は webpack を利用する、さらに開発サーバとして webpack-dev-server を利用したいので、インストール。
$ npm install --save-dev webpack webpack-dev-server
なにか困ったら:
$ webpack --display-error-details
webpack.config.js は rebar.config のようなものだろう。ということで、調べて参考にしたのをそのまま活用させて頂く。
query に .babelrc 部分を書けるのがポイント。
module.exports = {
devtool: "eval",
entry: {
js: "./src/app.js",
html: "./index.html"
},
output: {
path: __dirname + "/dist",
filename: "./bundle.js"
},
module: {
loaders: [
{
test: /\.html$/,
loader: "file?name=[name].[ext]"
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
query: {
presets: ["es2015", "react"]
}
}
]
}
};
app.js というのがベースとなる JS という感じで設定してみた。これで準備ができた。
次は今回の目的である React を使うための環境を用意する。
- React
- Babel
npm install --save は実際にパッケージングされる際に利用するもの。 --save-dev は開発時に利用するものとわけるようだ。
$ npm install --save react \ react-dom
$ npm install --save-dev babel-preset-react \ babel-loader \ babel-core \ babel-preset-es2015 \ file-loader
webpack や webpack-dev-server を使おうとするとこのままではパスが通ってないので利用できない。そんな時は npm init 時に作成した packege.json の scripts をうまく活用するとよいらしい。
以下は 2017-02 の時点で作ったサンプル。
{
"name": "step2",
"version": "2.0.0",
"scripts": {
"build": "webpack --optimize-minimize",
"dev-server": "webpack-dev-server --progress --color"
},
"license": "Apache-2.0",
"devDependencies": {
"babel-core": "^6.21.1",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"file-loader": "^0.9.0",
"webpack": "^1.14.0",
"webpack-dev-server": "^1.16.2"
},
"dependencies": {
"material-ui": "^0.16.6",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-tap-event-plugin": "^1.0.0"
}
}
これで npm run build でパッケージング。 npm run dev-server で開発サーバが立ち上がるようになった。おかげでグローバルを汚さなくてすむようになった。
新しい仕組みを覚えるときに、良く抱える問題の一つとして フォルダ構成のベストプラクティス がある。これを一番最初に書いて欲しいと良く思ってしまう。
今回は React.js ということで、そのフォルダ構成を考えてみることにした。まずは最小限に app.js と commponents にその App 用の React コンポーネントをいれる components というフォルダ構成がよさそうということで、以下の構成になった。
- index.html - src/ - app.js - components - App.js
<html>
<head>
<title>React.js Dojo</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="content">Content</div>
<script src="bundle.js" charset="utf-8"></script>
</body>
</html>
React.js では特定のタグに対して、そこにデータを入れていくという仕組みらしいので、 HTML は最小限でいいらしい。
これがベースとなる JS でここはレンダリングして戻すだけのシンプルな仕組みになる。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App.js';
ReactDOM.render(
<App />,
document.getElementById('content')
);
- import
- 'react-dom' の ReactDOM を呼び出すといった、よくある import をしてくれる仕組み
- Erlang では使われない import
- from
- ここはなぜか '' で包む
- ./components/App.js って無理矢理感あってすごい、名前空間とは一体 ...
- ReactDOM.reander
- これがよくわかっていないが、トップレベルで JSX をレンダリングしてくれる何か
- <App />
- components ディレクトリに作った App.js
どうもサンプルの多くがここで Hello, world. をやっているのだが、それだとイメージがつきにくいのであえて components フォルダに App.js ファイルを作ってそちらで Hello, world をしてみることにした。
JSX は今のところは React.Component の render 部分で使われるなんか便利そうな言語という理解することにした。
賛否両論あるようだが、個人的にはまぁ覚えればいいんでしょくらいのスタンス。
import React from 'react';
export default class App extends React.Component {
render() {
return (
<h1>Hello, world.</h1>
);
}
}
- export
- 外から呼ぶときに使う、 import/export の仕組み
- default
- 外から呼ぶとき default を設定しておくと自動的にこれが呼ばれるとのこと
- class
- ES6 で使える class の仕組み、便利
- App
- この class の名前
- Extends
- ES6 で使える継承と理解した
- React.Component
- React コンポーネントを作る場合はこれを継承する
- render()
- このコンポーネントの JSX レンダリング部分
この部分だが、 const を使って書き直せるようだ。 render しかなくて、 state を持たないコンポーネントはステートレスコンポーネントと呼ばれるらしい。
import React from 'react';
const App = () => {
return <h1>Hello, world.</h1>
};
export {App as default}
export と default の設定は const では使えないため別途設定する必要がある。
webpack-dev-server は watchdog と差分ビルドと自前サーバを用意してくれるようだ。これは WebSocket や WebRTC を使う身としては大変ありがたい。
$ webpack-dev-server --progress --color
http://localhost:8080/webpack-dev-server/ にアクセスしてみると Hello, world. が表示されています。
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step1 |
---|
- app.js はほとんど何も書かなくていい
- JSX は HTML っぽいけど、独自定義なので間違えないようにする
- コンポーネントをたくさん作ってうまく render で表示していく仕組み
- Header コンポーネントや、Body コンポーネントを作っていくとイメージしやすいらしい
- webpack の自動ビルドとブラウザリロードはなかなか便利
Material-UI を利用する場合、 React.js の react-tap-event-plugin をインストールして app.js で呼ぶ必要がある。 この作業は React.js がバージョンが上がれば不要になるようだ。
$ npm install --save material-ui \ react-tap-event-plugin
{
"name": "step2",
"version": "2.0.0",
"scripts": {
"build": "webpack --optimize-minimize",
"dev-server": "webpack-dev-server --progress --color"
},
"license": "Apache-2.0",
"devDependencies": {
"babel-core": "^6.21.1",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"file-loader": "^0.9.0",
"webpack": "^1.14.0",
"webpack-dev-server": "^1.16.2"
},
"dependencies": {
"material-ui": "^0.16.6",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-tap-event-plugin": "^2.0.1"
}
}
`Material-UI`_ はボタンだけを置くことを目標にします。まずはボタンを置いてみることから。
App.js を書き換えていく。必要になりそうな Material-UI のパーツを render() に書いていけば良い。
まずボタンを配置するだけ。
import React from 'react';
import ReactDOM from 'react-dom';
// この二行を追加
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import App from './components/App.js';
ReactDOM.render(
<MuiThemeProvider>
<App />
</MuiThemeProvider>,
document.getElementById('content')
);
あとは MuiThemeProvider で <App /> を挟む。まずはこれで Material-UI を使う準備はできた。 MuiThemeProvider はテーマらしく、好きに変えられる仕組みらしい。
import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
export default class App extends React.Component {
render() {
return (
<div>
<h1>Hello, world.</h1>
<RaisedButton label="Hello, world." />
</div>
);
}
}
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step2 |
---|
いくつかコンポーネントを追加してみる
TextField と Snackbar だ。
TextField の説明は省略。Snackbar は簡易的なポップアップで時間が立ったら消える。 まずは完全な App.js のコードをここに貼っておく。
import React from 'react';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import Snackbar from 'material-ui/Snackbar';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
value: '',
};
}
handleTouchTap() {
this.setState({
open: true,
});
}
handleRequestClose() {
this.setState({
open: false,
});
}
handleChange(e) {
this.setState({
value: e.target.value,
});
}
render() {
return (
<div>
<h1>Hello, world.</h1>
<TextField
hintText="message"
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
<br/>
<RaisedButton
onTouchTap={this.handleTouchTap.bind(this)}
label='Hello, world'
/>
<Snackbar
open={this.state.open}
message={this.state.value}
autoHideDuration={1000}
onRequestClose={this.handleRequestClose.bind(this)}
/>
</div>
);
}
}
このコンポーネントの初期化した時に呼ばれる値。props は親コンポーネントから送られてくる値が入っているのでそのまま使うための super 。
this.state はこのコンポーネント自身が持つ状態。gen_server の #state{} みたいなもの。 init/1 で #state{} 定義するのと同じ。
import TextField from 'material-ui/TextField';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
value: '',
};
}
...
}
ここでは Snackbar を出現させるかどうかの open: boolean() と TextField に入力した値を持っておく value: string() を用意する。それぞれ初期値も与えておく。
TextField は実際の入力した値の部分と、その値が変化した部分がコンポーネントと連携している
<TextField
hintText="message"
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
- hintText は TextField に後ろにうっすらと何を入力すればいいか、書かれているメッセージを指定できる
- value は constructor で定義した値をコンポーネントから状態を引っ張ってきている
- onChange はコンポーネントに定義した handleChange を呼び出している
- このテキストが変化したらこの関数を呼び出せという内容
handleChange(e) {
this.setState({
value: e.target.value,
});
}
handleChange を見ていくと event.target.value という感じで先程の TextField に値が入力されるたびに setState されている。 this.handleChange だが、 bind(this) は JSX の世界とコンポーネントの世界を連動させているものという認識でまずよさそうだ。
Snackbar は起動する部分と、閉じるタイミングの部分、そしてメッセージの部分がコンポーネントと連動している。
<Snackbar
open={this.state.open}
message={this.state.value}
autoHideDuration={1000}
onRequestClose={this.handleRequestClose.bind(this)}
/>
- open はこの Snackbar を開く部分
- this.state.open で初期値は false になっている、ここが true になると Snackbar が起動する
- message は Snackbar に表示される文字列
- autoHideDuration は自動で消える時間、ここでは 1000 を指定してる
- onRequestClose は閉じるタイミングで実行する処理を指定できる
handleRequestClose() {
this.setState({
open: false,
});
}
handleRequestClose では setState で open を false にしている、ここを true にするとまた上がってきてしまう。ここでも this.handleRequestClose.bind(this) だが、用意されたメソッド以外はこうやって bind していく必要がある。
RaisedButton はそのボタンがクリックされたタイミングがコンポーネントと連動している
<RaisedButton
onTouchTap={this.handleTouchTap.bind(this)}
label='Hello, world'
/>
- onTouchTap はそのボタンが押されたら handleTouchTap を実行する
handleTouchTap() {
this.setState({
open: true,
});
}
setState で open を true にすることで Snackbar を起動している
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step3 |
---|
- Material-UI のコンポーネントを追加するのは難しくない
- Material-UI コンポーネント同士の連携も、App.js の State に集約されているため、そこが共通の値として動作しているので扱いやすい
- App.js のメソッドと Material-UI のコンポーネントを連携させる場合は .bind(this) を利用すればよい
コンポーネント同士の連携が確認できたところで、これをサーバサイドとのやり取りに置き換えていく。
具体的にはボタンを押したら WebSocket で TextField の値がサーバに送られ、サーバは echo でそのまま値を返してきて、その値を Snackbar に表示させるというものだ。
結果はコンポーネントの連携と変わらないが、通信が含まれるので、書き方が変わってくる。
今回 WebSocket サーバを用意するのを省略するため wss://echo.websocket.org を利用させてもらうことにする。
React には Component Lifecycle というものがあるようで、よく使うのが componentDidMount と componentWillUnmount のようだ。
これらは起動時にと終了時に一回だけ呼ばれるもので、 WebSocket の場合は componentDidMount で new して componentWillUnmount で close する感じになるのだと思う。
constructor(props) {
super(props);
this.state = {
// ws を定義する
ws: null,
// 送られてきたメッセージ格納用
message: '',
open: false,
value: '',
};
}
componentDidMount() {
var ws = new WebSocket("wss://echo.websocket.org");
ws.onmessage = this.handleMessage.bind(this);
this.setState({ws: ws});
}
componentDidMount には WebSocket を new し、その後 onmessage メソッドを WebSocket のコールバックに登録している。
handleMessage(msg) {
this.setState({
// 送られてきたメッセージを格納し
message: msg.data,
// 状態を open へ
open: true,
});
}
componentWillUnmount はこのコンポーネントが DOM から削除されるタイミングに呼ばれるので、丁寧に WebSocket を close しておく。
componentWillUnmount() {
this.state.ws.close();
}
ボタンを押したら、 WebSocket 経由でメッセージをサーバへ送る。
handleTouchTap() {
this.state.ws.send(this.state.value);
}
handleTouchTap が実行されたら state にいる WebSocket から state にある TextField に入力した値を送るというのを handleTouchTap に書く。
Snackbar の変更点は一つだけで、サーバから送られてきたメッセージを表示するようにする。
<Snackbar
open={this.state.open}
message={this.state.message}
autoHideDuration={1000}
onRequestClose={this.handleRequestClose.bind(this)}
/>
message を this.state.message に変更するだけだ。これでサーバから送られてきたメッセージを読み込み、表示するようになる。
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step4 |
---|
まずは全体のコードを。 App.js のみ変更している。 動作の見た目は全然変わらないが、WebSocket サーバ送ったメッセージを非同期で受け取り、そのメッセージが Snackbar に表示されるという仕組み。
import React from 'react';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import Snackbar from 'material-ui/Snackbar';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
ws: null,
message: '',
open: false,
value: '',
};
}
componentDidMount() {
var ws = new WebSocket("wss://echo.websocket.org");
ws.onmessage = this.handleMessage.bind(this);
this.setState({ws: ws});
}
componentWillUnmount() {
this.state.ws.close();
}
handleMessage(msg) {
this.setState({
message: msg.data,
open: true,
});
}
handleChange(e) {
this.setState({
value: e.target.value,
});
}
handleTouchTap() {
this.state.ws.send(this.state.value);
}
handleRequestClose() {
this.setState({
open: false,
});
}
render() {
return (
<div>
<h1>Hello, world.</h1>
<TextField
hintText="message"
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
<br/>
<RaisedButton
onTouchTap={this.handleTouchTap.bind(this)}
label='Hello, world'
/>
<Snackbar
open={this.state.open}
message={this.state.message}
autoHideDuration={1000}
onRequestClose={this.handleRequestClose.bind(this)}
/>
</div>
);
}
}
- this.state は便利で、そこにおいておけばあとはその変更によって各コンポーネントが動作をしてくれるのはとても良い
- コールバックというのをあまり意識しないのはおそらく state の値の変更での発火だからかもしれない
コーディング規約は便利なので、それに簡単に気付くための ESLint を導入することにした。
ESLint 関連で今回 npm install したのは以下の通り
$ npm install --save-dev eslint \ babel-eslint \ eslint-loader \ eslint \ eslint-plugin-babel \ eslint-plugin-material-ui \ eslint-plugin-react
.eslintrc は eslint --init で質問に答えて設定してみた。途中で形式を聞かれたので、 js にすればコメントかけてよさそうという判断で js にしてみた。
パスが通ってないので直接実行する:
$ node_modules/eslint/bin/eslint.js --init ? How would you like to configure ESLint? Answer questions about your style ? Are you using ECMAScript 6 features? Yes ? Are you using ES6 modules? Yes ? Where will your code run? Browser ? Do you use CommonJS? Yes ? Do you use JSX? Yes ? Do you use React Yes ? What style of indentation do you use? Spaces ? What quotes do you use for strings? Single ? What line endings do you use? Unix ? Do you require semicolons? Yes ? What format do you want your config file to be in? JavaScript Successfully created .eslintrc.js file in react-dojo/step5
生成された .eslintrc.js ファイル
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
};
webpack から ESLint が使えるようにするには module と eslint という項目を増やしておく。 preLoaders なのは、 ESLint がお勧めしていたのでそのまま使った。
webpack.config.js で追加したのは以下の通り。
module.exports = {
devtool: "eval",
entry: {
js: "./src/app.js",
html: "./index.html"
},
output: {
path: __dirname + "/dist",
filename: "./bundle.js"
},
module: {
// ここを追加
preLoaders: [
{test: /\.js$/, loader: "eslint-loader", exclude: /node_modules/}
],
loaders: [
{
// file-loader を利用している
test: /\.html$/,
loader: "file?name=[name].[ext]"
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
query: {
presets: ["es2015", "react"]
}
}
]
},
// ここを追加
eslint: {
configFile: '.eslintrc.js'
}
};
{
"name": "step5",
"version": "2.0.0",
"scripts": {
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"build": "webpack --optimize-minimize",
"dev-server": "webpack-dev-server --progress --color"
},
"license": "Apache-2.0",
"devDependencies": {
"babel-core": "^6.21.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"eslint": "^3.12.2",
"eslint-loader": "^1.6.1",
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-material-ui": "^1.0.1",
"eslint-plugin-react": "^6.8.0",
"file-loader": "^0.9.0",
"webpack": "^1.14.0",
"webpack-dev-server": "^1.16.2"
},
"dependencies": {
"material-ui": "^0.16.6",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-tap-event-plugin": "^1.0.0"
}
}
jsx の部分は Material-ui の .eslintrc.js を参考にしてみた。
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"babel",
"react",
"material-ui",
],
"rules": {
'react/jsx-boolean-value': [
'error',
'always'
],
'react/jsx-closing-bracket-location': 'error',
'react/jsx-curly-spacing': 'error',
'react/jsx-equals-spacing': 'error',
'react/jsx-filename-extension': [
'error', {
extensions: ['.js']
}
],
'react/jsx-first-prop-new-line': [
'error',
'multiline'
],
'react/jsx-handler-names': 'error',
'react/jsx-no-comment-textnodes': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': 'error',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-wrap-multilines': 'error',
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
};
eslint --fix を利用すればある程度までは自動で修正してくれるらしい。package.json の scripts に仕込んでおいた。
$ npm run lint:fix
`WebSocket を導入する`_ のコードに対して修正してみた。
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step5 |
---|
- とても細かく設定可能なので、大変そうだがコピペで済みそう
- 導入までの距離がとても短い
- webpack-dev-server ともさくっと連絡して良い
- lint を採用するときのスタンダードがわかりにくいので、どうするべきか
- eslint-config-airbnb
- この辺をお手本にするといいのかもしれない
今回は Erlang/OTP で書いた WebSocket サーバを用意した。接続すると 1 秒毎に値を返してくる。これを画面にだらだらと表示していく仕組みを実現していきたい。
ちなみに、今回は Material-UI は利用していない。
Erlang/OTP で、 WebSocket サーバを用意しておいた。
https://github.com/voluntas/react-dojo/tree/develop/step6/step6-server
Erlang/OTP 19.1 がインストールされていれば make dev で起動するようになっている。
このサーバは接続すると 1 秒毎に値が送られてくる。送られてくるのは以下のような値だ。
{
"count": 1,
"uuid": "dfb79b00-fff9-42fc-9f72-6dfe1ae26a5f"
}
count は毎秒毎に +1 され、 UUID は毎回変わる。この count は接続毎にカウントされていく。
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(
<App />,
document.getElementById("content")
);
import React from "react";
import ReactDOM from "react-dom";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
ws: null,
messages: [],
};
}
componentDidMount() {
let ws = new WebSocket("ws://127.0.0.1:8000/ws");
ws.onmessage = this.handleMessage.bind(this);
this.setState({ws: ws});
}
componentWillUnmount() {
this.state.ws.close();
}
componentDidUpdate() {
let node = ReactDOM.findDOMNode(this.refs.messages);
// XXX: これは .. ありなのだろうか
window.scrollTo(0, node.scrollHeight);
}
handleMessage(event) {
let message = JSON.parse(event.data);
this.state.messages.push(message);
this.setState({
messages: this.state.messages
});
}
render() {
let messages = this.state.messages.map(v => {
return <div key={v.uuid} >{v.uuid}: {v.count}</div>;
});
return (
<div ref="messages">{messages}</div>
);
}
}
- constructor で state を ws と messages を定義
- messages は送られてきた JSON を格納する
- componentDidMount で WebSocket を接続している
- ws://127.0.0.1:8000/ws は Erlang/OTP で設定した値
- setState で ws を保持
- componentDidUpdate でコンポーネントが更新された時にスクロールを一番したにする
- ReactDOM.findDOMNode で messages の DOM を取得する
- this.refs で render (JSX) 部分で名前を付けて引っ張れる
- <div ref="messages"> は this.refs.messages で引っ張れる
- ちなみに window.scrollTo でいいのかわからない
- handleMessage は onmessage のコールバックで、 WebSocket からメッセージが送られてきたら呼ばれる
- 送られてくる JSON をパースして、そのまま this.state.messages に push で突っ込む
- render は JSX で表示する部分
- messages を map で <div /> にする
- key を指定することで差分更新になる
- return で ref を指定しているのは this.refs で引っ張れるようにしているため
これで、あとは webpack-dev-server を起動すれば、画面に WebSocket サーバから送られてくる値が淡々と表示され、続ける。スクロールは一番下に固定され続ける。
console.debug を使おうと思ったら eslint でエラーになった。
eslint の設定に以下を追加する必要があるらしい。
'no-console': [
"error",
{
allow: [
"debug"
]
}
],
- JSX に慣れていない、 JSX の癖をもっとつかまないと上手く利用するのは難しそう
- JS の DOM を意識することはあまりなかったがはやり少しでも動きが入ると必要になる
- そろそろこちらからリクエストを送って何かハンドリングをさせてみるという処理が必要になりそう
Flow は静的型解析ツールで OCaml で書かれている。既存のアプリに導入しやすい後出しできる型解析ツール。
/* @flow */ を入れるだけで始められるのはとても良いらしい、ということで試してみることにする。
package.json は step5 からの流用、そこに二つほどライブラリを追加。
$ npm install --save-dev flow-bin \ babel-plugin-transform-flow-strip-types
scripts 部分に flow コマンドを追加。
package.json:
{ "name": "step5", "version": "2.0.0", "scripts": { "flow": "flow", "lint": "eslint src", "lint:fix": "eslint src --fix", "build": "webpack --optimize-minimize", "dev-server": "webpack-dev-server --progress --color" }, "license": "Apache-2.0", "devDependencies": { "babel-core": "^6.21.0", "babel-eslint": "^7.1.1", "babel-loader": "^6.2.10", "babel-plugin-transform-flow-strip-types": "^6.21.0", "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "eslint": "^3.12.2", "eslint-loader": "^1.6.1", "eslint-plugin-babel": "^3.3.0", "eslint-plugin-material-ui": "^1.0.1", "eslint-plugin-react": "^6.8.0", "file-loader": "^0.9.0", "flow-bin": "^0.37.4", "webpack": "^1.14.0", "webpack-dev-server": "^1.16.2" }, "dependencies": { "material-ui": "^0.16.6", "react": "^15.4.1", "react-dom": "^15.4.1", "react-tap-event-plugin": "^1.0.0" } }
.flowconfig を生成する:
$ node_modules/flow-bin/flow-osx-v0.37.4/flow init
.flowconfig の ignore は最低限でやるべきっぽい。 node_modules を全部対象にすると、そんなライブラリみつからんって怒られる
.flowconfig:
[ignore] .*node_modules/fbjs.* [include] [libs] [options]
このタイミングではよくわからなかったので、省略する。 とりあえず npm run flow コマンドを叩いて利用するで終わらせることにする。
flow の loader の上手い使い方がわかったらそれを後で書く
babel-plugin-transform-flow-strip-types これをセットアップして webpack に設定を入れる必要があるようだ。 おそらく flow 型設定が書いてあってもエラーにならないようにするためのプラグインだろう。
plugins を追加して transform-flow-strip-types を追加することにした。
module: { preLoaders: [ {test: /\.js$/, loader: "eslint-loader", exclude: /node_modules/} ], loaders: [ { // file-loader を利用している test: /\.html$/, loader: "file?name=[name].[ext]" }, { test: /\.js$/, loader: "babel-loader", exclude: /node_modules/, query: { presets: ["es2015", "react"], plugins: ["transform-flow-strip-types"] } } ] },
公式に書いてあるので、まずは src/app.js と src/components/App.js に /* @flow */ を付けてみることにする。
実行してみたら、凄い勢いでエラーになった。
書いている途中です ...
ここは毎回変わってしまうので参考程度に見てもらえれば
ライブラリのアップデートの追従はとても大切なことだ、ただやり方がわからないので調べることにした。
package.json をアップデートするには npm update するというのはわかったのだが、最新版に追従したいとき一つ一つチェックしていくのは面倒くさい。 npm update を実行すれば良いということがわかった。
この際、 npm update -D と npm update -S という順番にやる必要がある。 -D は --save-dev の略で、 -S は --save の略だ。
-S を先にしてしまうと devDependencies でも dependencies に入ってしまうとのことを Twitter で教えて頂いた。ありがたい。
$ npm update -D - [email protected] node_modules/babel-plugin-syntax-async-functions - [email protected] node_modules/source-map-support/node_modules/source-map [email protected] /react-dojo/step1-1 ├── [email protected] ├── [email protected] ├── [email protected] └── [email protected]
% npm update -S [email protected] /react-dojo/step1-1 ├── [email protected] └── [email protected] npm WARN [email protected] No description npm WARN [email protected] No repository field.
無事 package.json がアップデートされている。
diff --git a/step1-1/package.json b/step1-1/package.json index 70b371f..a1734fd 100644 --- a/step1-1/package.json +++ b/step1-1/package.json @@ -7,16 +7,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "babel-core": "^6.14.0", + "babel-core": "^6.17.0", "babel-loader": "^6.2.5", - "babel-preset-es2015": "^6.14.0", - "babel-preset-react": "^6.11.1", + "babel-preset-es2015": "^6.16.0", + "babel-preset-react": "^6.16.0", "file-loader": "^0.9.0", "webpack": "^1.13.2", - "webpack-dev-server": "^1.15.2" + "webpack-dev-server": "^1.16.2" }, "dependencies": { - "react": "^15.3.1", - "react-dom": "^15.3.1" + "react": "^15.3.2", + "react-dom": "^15.3.2" } }
ということで、無事期待する結果を得る方法を学ぶことができた。
ソース URL: | https://github.com/voluntas/react-dojo/tree/develop/step1-1 |
---|
標準でちゃんとチェックツールが入っているのでとても便利。管理が膨大になるとどこかのバージョンのライブラリがおかしくなってしんどくなりそうな感じもあるが、それはそのとき考える。
メジャーバージョンアップに関しては自前でやる必要がある
ローカルでやるのが面倒だなと感じる人は、自動でアップデートを見て Pull-Request を送ってくれるサービスとのことを Twitter にて教えて頂いた。
これは大変賢いサービスだし、とても良いように思える。もし必要になったら導入していきたい。