JavaScript
webpack
React
ESLint

ReactのチュートリアルをES6で書いてwebpackとESLintも使ってみる

More than 1 year has passed since last update.

初心者(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 を使っているのですが、それも考慮した設定です。

.eslintrc.json
{
  "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 に書いておきます。

.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.jsJavaScript - webpack+babel環境でフロントエンドもES6開発 を参考にさてていただきました。といっても、やりながらだいぶ変わりました。最終的にはこのようになりました。testのところが二箇所あるのがちょっと微妙です。

webpack.config.js
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-serverconcurrently を追加します。

$ 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 の影響を受けました。

package.json
{
  "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"; というように書いておけばビルドするときに解決してくれる様子!

index.html
<!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から分離してみました。全体の設定とかこのファイルでできるかな?

app.js
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初心者がみな通る道なんでしょうね...。

CommentBox.jsx
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

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

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

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>
    );
  }
}

はまった

CommentBox.jsx
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の学習

参考にした記事

ありがとうございました!

終わりに

とりあえず、ほんとうに基本的な部分はやったのですが、繰り返してやって雰囲気つかめてきました。あとは、Awesomeでも見ながら慣れていこうと思います。とりあえず、Radium使ってみたい。