初心者(ReactもJSもES6も)の書いた記事なので色々間違えていたりすると思います。そういうところは助言をいただけるとありがたいです。
学習として React のコードをES6(2015)で使えるようになった記法で書いてみることにしました。スタイルは AirbnbのReact/JSXスタイルガイド を参考にやろうと思います。
さらに、出力されるJSをbundle.jsとかいう感じでまとめたくて、 webpack を使いました。コードもある程度補正してもらいたいと思い、 ESLint も使ってみることにしました。
準備
まずは、各種ライブラリを入れたりします。React
npm install --save react react-dom
ESLint
Linterです。細かくルールの設定ができて使っていて気持ち良かったです。最近人気?
$ npm install -g eslint
$ eslint -v
$ eslint --init
eslint --init
すると対話的に環境を聞いてくれてファイルを作ってくれるのでありがたいです。React使いますかとか聞かれたりします。そしてできたファイルを書きながら更新して行ってこんな感じになりました。
ES6を利用するために Babel を使っているのですが、それも考慮した設定です。
{
"rules": {
"indent": [
2,
2
],
"quotes": [
2,
"double"
],
"linebreak-style": [
2,
"unix"
],
"semi": [
2,
"always"
],
"no-unused-vars": [
1, {"vars": "all", "args": "after-used"}
],
"no-console": 1
},
"env": {
"es6": true,
"browser": true
},
"extends": "eslint:recommended",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true,
"modules": true
},
"plugins": [
"react"
]
}
Atomで書いているのですが、 linter-eslint を入れることでライブでチェックしてくれます。ES6も見てくれるということで(?)ESLintを使ってみることにしました。チェックを走らせたくないファイルは .eslintignore
に書いておきます。
node_modules/
test/
webpack.config.js
dist/
server/
Webpack
複数のjavascriptファイルを一つにまとめてくれたり、リソースを良い形にしてくれる(?)ツールです。僕はこれで bundle.js
が作りたかった(憧れ)ので使ってみました。
Babel を利用するための設定も色々必要です。
$ npm install --save-dev webpack babel-loader babel-core babel-preset-react babel-preset-es2015
webpack.config.js は JavaScript - webpack+babel環境でフロントエンドもES6開発 を参考にさてていただきました。といっても、やりながらだいぶ変わりました。最終的にはこのようになりました。testのところが二箇所あるのがちょっと微妙です。
module.exports = {
entry: __dirname + "/src/app.js",
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: "babel",
query:{
presets: ['react', 'es2015']
}
}
]
},
resolve: {
extensions: ['', '.js', '.jsx']
}
};
簡易サーバーと自動リロード
今回はwebpackでjsの変更を監視しつつ、変更があったら簡易サーバーで立ち上げたページが自動更新するという風にしたいと思います。そのために、 lite-server と concurrently を追加します。
$ npm install lite-server concurrently --save-dev
package.json のところに、lite-serverとwebpackとconcurrentlyに関する記述を追加します。 npm start
するとes6をjsにしてさらにウェブページが更新されます。
Gulpを使わないでnpmで簡易的なことができたらいいなと思ってこのようにしましたが、自信は全くないです。素直にGulpを使ったら良いのかもしれません。この辺りは、 How to Use npm as a Build Tool の影響を受けました。
{
"name": "9.react-tutorial-2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"marked": "^0.3.5",
"react": "^0.14.6",
"react-dom": "^0.14.6",
"superagent": "^1.6.1"
},
"devDependencies": {
"babel-core": "^6.4.0",
"babel-loader": "^6.2.1",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"eslint": "^1.10.3",
"eslint-config-standard": "^4.4.0",
"eslint-plugin-react": "^3.14.0",
"eslint-plugin-standard": "^1.3.1",
"lite-server": "^1.3.2",
"webpack": "^1.12.10"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"webpack": "webpack -w",
"lite": "lite-server --verbose --open dist",
"start": "concurrent \"npm run webpack\" \"npm run lite\""
},
"author": "",
"license": "ISC"
}
Reactのチュートリアルを書いてみる
さて、環境が整ったところでReactもう一度チュートリアルを読みながら書いていきます。気になったところだけコメントを書いてそれ以外は、コードをすべて貼り出したのでそれで語ります。ちなみに、サーバー側のコードは Tutorial をそのまま(ちょっとだけ手を入れて)使っています。
Githubにもpushしてます。
index.html
この bundle.js
がやりたかったんですよね。できて良かった!それから、 <script src="...">
ってちょっと前に書いた時は書きまくってたんだけど、それもwebpackを使うことで書く必要がなくなりました。 import React from "react";
というように書いておけばビルドするときに解決してくれる様子!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="dist/bundle.js"></script>
</body>
</html>
</pre>
app.js
チュートリアルにはないけれど、CommentBoxから分離してみました。全体の設定とかこのファイルでできるかな?
import React from "react";
import ReactDOM from "react-dom";
import CommentBox from "./CommentBox";
ReactDOM.render(
<CommentBox url="http://localhost:3001/api/comments" pollInterval={2000}/>,
document.getElementById("app")
);
CommentBox.jsx
通信は、jQueryではなく superagent というのを使ってみました。分かりやすくて良いと思いました。 bind(this)
しないとよばれた関数の中でクラス自体にアクセスできないというのは、たぶん、JS初心者がみな通る道なんでしょうね...。
import React from "react";
import request from "superagent";
import CommentList from "./CommentList";
import CommentForm from "./CommentForm";
export default class CommentBox extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.loadCommentsFromServer = this.loadCommentsFromServer.bind(this);
}
loadCommentsFromServer() {
request
.get(this.props.url)
.end((err, res) => {
if (err) {
throw err;
}
this.setState({data: res.body});
});
}
handleCommentSubmit(comment) {
var comments = this.state.data;
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
request
.post(this.props.url)
.send(comment)
.end((err, res) => {
if (err) {
this.setState({data: comments});
throw err;
}
this.setState({data: res.body});
});
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>
);
}
}
CommentList.jsx
import React from "react";
import Comment from "./Comment";
export default class CommentList extends React.Component {
constructor(props) {
super(props);
}
render() {
var commentNodes = this.props.data.map((comment) => {
return (
<Comment author={comment.author} key={comment.id}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
}
CommentForm.jsx
import React from "react";
export default class CommentForm extends React.Component {
constructor(props) {
super(props);
this.state = {
author: "",
text: ""
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleAuthorChange = this.handleAuthorChange.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
}
handleAuthorChange(e) {
this.setState({author: e.target.value});
}
handleTextChange(e) {
this.setState({text: e.target.value});
}
handleSubmit(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.setState({author: "", text: ""});
}
render() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
}
Comment.jsx
import React from "react";
import marked from "marked";
export default class Comment extends React.Component {
rawMarkup() {
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return { __html: rawMarkup};
}
render() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
}
はまった
import CommentList from "./CommentList.jsx";
import CommentForm from "./CommentForm.jsx";
他のファイルを読み込む際に拡張子を入れないとモジュールが見つからないと言われました。 Suggestion: Webpack require should resolve jsx by default · Issue #16 · newtriks/generator-react-webpack によるとresolve...が必要ということだったのでつけたらうまくいきました(冒頭のwebpackの設定ファイルに反映しています)。
ESLintのおかげで記法もそんなにぶれずに書けたと思います。ただし、正しい記法とかもっと良い記法はもちろんあると思います。JS慣れてないので。。。
今回の学習で利用したサービス・ツールなど
最近やったReactの学習
- React.jsを学んでみたメモ – Morizotter Blog
- TypeScript-ReactのHelloWorldメモ – Morizotter Blog
- はぁはぁ、、、ReactをTypeScriptで書いてみたはぁはぁ… – Morizotter Blog
参考にした記事
ありがとうございました!
- JavaScript - webpack+babel環境でフロントエンドもES6開発 - Qiita
- 時代はESLint。JSLintでもJSHintでもなくESLint。 - Qiita
- JSX - ESLintについての細かいあれこれ - Qiita
- JavaScript - ESLint 最初の一歩 - Qiita
- reactjs - ES6版React.jsチュートリアル - Qiita
終わりに
とりあえず、ほんとうに基本的な部分はやったのですが、繰り返してやって雰囲気つかめてきました。あとは、Awesomeでも見ながら慣れていこうと思います。とりあえず、Radium使ってみたい。