できあがったのはこちら。
Reactを触ってみたいと思って、チュートリアルをやってみた。翻訳してくださっているかたがいて、訳もわかりやすくてとても助かった。
チュートリアルでは JSXTransformer.js を使っているので、JSXの変換などを考える必要はない。手軽なんだけど、Reactを使おうとしたらけっきょくコンパイルしなきゃいけない。
コンパイルにはBabelが必要になる。Babel使うならES6も使いたい。ES6の変換は自動的にやりたいし、ファイルはminifyしたいし、minifyしたらsource mapが欲しいし、変更したら自動的にブラウザを更新したい。って感じであれこれやってみた。
Environment
ディレクトリ構成はこんな感じ。
react-tutorial-es6
├── LICENSE
├── README.md
├── comments.json
├── dist
│ ├── app.js
│ └── app.js.map
├── gulpfile.js
├── index.html
├── package.json
├── public
│ └── index.html
├── server.js
└── src
└── app.jsx
package.jsonはこんな感じ。
gulpfile.jsはこんな感じ。
var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require('babelify');
var uglify = require('gulp-uglify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var sourcemaps = require('gulp-sourcemaps');
var filter = require('gulp-filter');
var nodemon = require('gulp-nodemon');
var browserSync = require('browser-sync').create();
gulp.task('browserify', function() {
browserify('./src/app.jsx', { debug: true })
.transform(babelify)
.bundle()
.on("error", function (err) { console.log("Error : " + err.message); })
.pipe(source('app.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}))
.pipe(uglify())
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('./dist'))
});
gulp.task('watch', function() {
gulp.watch('./src/*.jsx', ['browserify'])
});
gulp.task('browser-sync', ['nodemon'], function() {
browserSync.init(null, {
proxy: 'http://localhost:3000',
port: 8000
});
gulp.watch(["public/**", "dist/**"], function() {
browserSync.reload();
});
});
gulp.task('nodemon', function() {
return nodemon({
script: 'server.js'
}).on('restart', function() {
setTimeout(function() {
browserSync.reload();
}, 500);
});
});
gulp.task('default', ['browserify', 'watch', 'browser-sync']);
browserifyでES6を変換したり、source map作ったり、minifyしたりする。これだけでもいろいろはまった。
チュートリアルではサーバが必要になる。いろいろな言語でのサーバのサンプルが付いている。Node.jsのサーバ用のスクリプトをnodemonで起動する。サーバ起動後にbrowser-syncを動かす。ここでもいろいろはまった。
ふぃー。
ES6
JavaScriptをES6に書きなおしていく。
import
必要なライブラリの読み込みはimportで行う。bowerは使わないことにした。
これを
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/jsx">
</script>
</body>
</html>
こんな感じにapp.jsにまとめる。
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello React</title>
</head>
<body>
<div id="content"></div>
<script src="./dist/app.js"></script>
</body>
</html>
importはこんな感じ。JSXTransformer.jsはコンパイルするからいらなくなる。markedはあとで使う。
import React from 'react';
import $ from 'jquery';
import marked from 'marked';
React.Component
React.createClassはReact.Componentを継承したクラスとして書きなおす。
これを
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
React.render(
<CommentBox />,
document.getElementById('content')
);
こんな感じにする。他の部分も同様に書きなおす。
class CommentBox extends React.Component {
render() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
}
React.render(
<CommentBox />,
document.getElementById('content')
);
let
varはletにする。これを
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
こんな感じにする。他の部分も同様に書きなおす。
let rawMarkup = marked(this.props.children.toString(), {sanitize: true});
getInitialState
getInitialStateはコンストラクタに書きなおす。これを
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
こんな感じにする。
class CommentBox extends React.Component {
constructor(props) {
super(props);
this.state = {data: []};
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
}
thisのあれこれ
thisが迷子になった。
コールバックをArrow Functionにする。bind(this)が不要になる。これを
var CommentBox = React.createClass({
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
});
こんな感じにする。他の部分も同様に書きなおす。
class CommentBox extends React.Component {
componentDidMount() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
type: 'POST',
success: data => this.setState({data: data}),
error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
});
}
}
setInterval
こちらもthisが変わってしまう。これを
var CommentBox = React.createClass({
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
});
こんな感じに書きなおす。
class CommentBox extends React.Component {
componentDidMount() {
this.loadCommentsFromServer();
setInterval( () => this.loadCommentsFromServer(), this.props.pollInterval );
}
}
this.loadCommentsFromServer
だとthisが変わって loadCommentsFromServer
で this.props.url
が見つからなくなるので
setInterval( this.loadCommentsFromServer, this.props.pollInterval );
こんな感じになった。
setInterval( () => this.loadCommentsFromServer(), this.props.pollInterval );
こんな感じでもよい。
setInterval( this.loadCommentsFromServer.bind(this), this.props.pollInterval );
onSubmit
こちらもthisが変わってしまうので、onSubmit={this.handleSubmit}
を
var CommentForm = React.createClass({
handleSubmit: function(e) {
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
onSubmit={this.handleSubmit.bind(this)}
に書きなおす。
class CommentForm extends React.Component {
handleSubmit(e) {
}
render() {
return (
<form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
}
onCommentSubmit
onCommentSubmitのthisも変わってしまうので onCommentSubmit={this.handleCommentSubmit}
を
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
onCommentSubmit={this.handleCommentSubmit.bind(this)}
に書きなおす。
class CommentBox extends React.Component {
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>
);
}
}
app.jsx
最終的にはこんなコードになった。
"use strict";
import React from 'react';
import $ from 'jquery';
import marked from 'marked';
class CommentBox extends React.Component {
constructor(props) {
super(props);
this.state = {data: []};
}
loadCommentsFromServer() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: data => this.setState({data: data}),
error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
});
}
handleCommentSubmit(comment) {
let comments = this.state.data;
let newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: data => this.setState({data: data}),
error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
});
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval( this.loadCommentsFromServer.bind(this), this.props.pollInterval );
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>
);
}
}
class CommentList extends React.Component {
render() {
let commentNodes = this.props.data.map( comment => {
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
}
class CommentForm extends React.Component {
handleSubmit(e) {
e.preventDefault();
let author = React.findDOMNode(this.refs.author).value.trim();
let text = React.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
React.findDOMNode(this.refs.author).value = '';
React.findDOMNode(this.refs.text).value = '';
return;
}
render() {
return (
<form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
}
class Comment extends React.Component {
render() {
let rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={{__html: rawMarkup}} />
</div>
);
}
}
React.render(
<CommentBox url="comments.json" pollInterval={2000} />,
document.getElementById('content')
);
ふぃー。
まとめ
完成したチュートリアルをがっと書きなおしてみたら、いろいろ動かなくて、けっきょく step by step で書きなおした。ES6力が足りなかった…。
実際に手を動かして、たくさんはまって、悩んで、解決していくという過程は大切だと思った。Arrow Functionのthisの動作は知っていたけど、書いてみたらはまった。やっぱり知っていることとできることは違うよね。
Reactさわってみたかったし、gulpもそのうち使ってみたかったし、ES6も試してみたかったから、まあいいんだけど、予備知識なくはじめるのは大変だなぁと思った。とはいえ情報はたくさんあるし(古い情報も多いけど)、2日くらい触れば簡単なことはできるようになる。学習コストよりはメリットのほうが大きい。当然だけど。
前々から思っていたけど、最近のフロントエンドの環境構築はサーバサイドのそれに似ている。Railsの環境構築みたいな。でもいまのフロントエンドの環境構築はいろいろ過渡期で大変だと思う。Railsの環境構築も同じような時期があったけど、ある程度落ち着いてきた気がする。
ちょうどいい粒度の内容で、書いていて楽しかった。やっぱりプログラミングはいいな。
参考URL