grails-react-boilerplate で React に入門した - 1日目 ホットリロード, React Bootstrap

普段の仕事はサーバーサイドばっかりだったので、年末年始でフロントエンドの知識をアップデートしたいなぁ、ReactかVue.jsやろうかなぁと思ってました。それで、こちらの記事を読んでると...

uehaj.hatenablog.com

サーバサイドJavaをずっとやってきて、モダンなJSの知識や経験があまりないけど、最近Reactってのが話題になっているのがさすがに気になるので挑戦したい人

これは!Javaだけってわけじゃないけど、ずっとサーバーサイドってまさに自分のことじゃないか!ということでコードを読んでみることに。

リポジトリをcloneしてreact-appディレクトリを一旦削除して、react-appを自分でつくり直しながら学ぶことにしました。コード読んでみて分からないことが色々あったので、一歩一歩分からないところを潰しながらやっていきました。その時の備忘録です。サーバーサイドはGrailsで実装されてるのをそのまま使います。

長くなったので2回に分けて。今回はホットリロードの設定をして、Bootstrapを使ってナビゲーションバーを実装するとこまでです。React Routerやサーバー通信は次回

f:id:bati11:20151231110319p:plain

使用したnodeのバージョンはv5.3.0、npmのバージョンは3.3.12 です。

準備

概念的なところ

まずは、概念的なところを。こちらのスライドが分かりやすいと感じました。

用語

職場のフロントエンジニアからBabelやらWebpackやら話は聞いていたのですが、AdventCalendarにこれまた自分のことじゃないか、という記事があったので読んでおきます。

チュートリアル

公式サイトのチュートリアルをもくもくと写経します。

grails-react-boilerplate に戻る

ディレクトリ構成

公式サイトのチュートリアルが終わったところで、grails-react-boilerplateの記事に戻ります。 React Meets Grails 〜ReactはエンタープライズSPAの夢を見るか?〜 - uehaj's blog

こちらのリポジトリをcloneします。

github.com

ディレクトリ構成は、バックエンドとフロントエンドで分かれてます。バックエンドはgrails-app配下。フロントエンドのソースコードはreact-app配下、それをビルドした結果をweb-app配下に置くようになってます。

PROJECT_ROOT
├── grails-app
├── react-app
└── web-app

Reactアプリケーションを1から作るために、react-appディレクトリ配下全てとweb-app/js/build.jsを削除します。react-appディレクトリ内にReactアプリを自分でつくり直していきます。

package.jsonを見てみる

package.jsonを眺めてみると色々依存ライブラリがあります。babel, bootstrap, react, webpackなどなど。

https://github.com/uehaj/grails-react-boilerplate/blob/master/react-app/package.json

webpack

ひとまず、Reactとwebpackだけを使ってやってみます。Babelは使わない。ひとまずシンプルなReactのコンポーネントをwebpackを使ってweb-app配下に配備できるようにしてみます。

$ cd react-app
$ npm init
$ npm install webpack react react-dom --save-dev
// react-app/package.json
{
  "name": "grails-react-boilerplate",
  "version": "1.0.0",
  "description": "Boilerplate for Grails+React project with hot code reloading",
  "main": "index.js",
  "dependencies": {},
  "devDependencies": {
    "react": "^0.14.3",
    "react-dom": "^0.14.3",
    "webpack": "^1.12.9"
  },
  "author": "KARIYA",
  "license": "MIT"
}

適当なReactのコンポーネントを作ります。

// react-app/src/index.js
var React = require('react');
var ReactDOM = require('react-dom');

var HelloBox = React.createClass({
  render: function() {
    return (
      <div>Hello, World!</div>
    );
  }
});

ReactDOM.render(
  <HelloBox />,
  document.getElementById('root')
);

webpack.config.js の意味を確認しながら作ってみます。

とりあえずこれで最小限。

// react-app/webpack.config.js
var path = require('path');
module.exports = {
  entry: [
    './src/index'
  ],
  output: {
    path: path.join(__dirname, '../web-app/js'),
    filename: 'bundle.js',
    publicPath: '/js/'
  }
}

package.jsonscriptsに、webpackを実行する "deploy": "webpack -p --config webpack.config.js" を追記。

// react-app/package.json
{
  ・・・
  "scripts": {
    "deploy": "webpack -p --config webpack.config.js"
  },
  ・・・
}

実行!

$ npm run-script deploy

・・・

ERROR in ./src/index.js
Module parse failed: ・・・/react-app/src/index.js Line 4: Unexpected token <
You may need an appropriate loader to handle this file type.
|   render: function() {
|     return (
|       <div className="hello">Hello, World!</div>
|     );
|   }
 @ multi main

エラーになりました。JSXの部分が不正だと扱われてるっぽいです。loaderというものが必要です。ここ(using loaders )を読むと、JSXを使うことをwebpackに伝えるためにloaderが使えるよって書いてあります。

grails-react-boilerplageの記事にも書いてあります。

Babel 以前は6to5と呼ばれていたトランスパイラ。JSXも標準でサポート。本稿ではwebpackのローダの一つとして使用

Babelは使ってないので、公式ドキュメントのloader一覧に書いてあったjsx-loaderを使うことにします。

$ npm install jsx-loader --save-dev
// react-app/webpack.config.js
module.exports = {
  ・・・
  module: {
    loaders: [{
      test: /\.js$/,
      loaders: ['jsx'],
      include: path.join(__dirname, 'src')
    }]
  }
}

実行!

$ npm run-script deploy

webpackによってbundle.jsができたかどうかを確認すると、、、できてます!

$ ls ../web-app/js/bundle.js
bundle.js

適当なWebサーバーを起動して動作確認してましょう。

$ cd ../webpack
$ python -m http.server

http://localhost:8000/index.htmlにアクセス!HelloBoxコンポーネントがちゃんと表示されてます。

ここまでで以下のような感じ。

.
├── react-app
│   ├── package.json
│   ├── src
│   │   └── index.js
│   └── webpack.config.js
├── grails-app
└── web-app
    ├── index.html
    └── js
        └── bundle.js

ホットリロード

grails-react-boilerplateはホットリロードにも対応されてます。

webpackが提供するwebpack-dev-serverを使用します。詳しくは説明しませんが、設定しているのは、GRAILS_PROJ/react-app/server.jsとGRAILS_PROJ/react-app/hot.webpack.config.jsです。

webpack-dev-serverっていうのを使うって書いてあります。

webpack dev server

読んでみると、Automatic Refresh と Hot Module Replacement の2つの機能があることが分かります。

Automatic Refresh

Automatic Refresh には2つのモード、Iframe mode と Inline mode があって、grails-react-boilerplate では Inline Mode を使ってる。Inline Mode にするには、webpack.config.js のentryに webpack-dev-server/client?http://localhost:3000 を追加します。

必要なnpmモジュールを追加。

$ npm install webpack-dev-server react-hot-loader --save-dev

webpack-dev-serverを起動するためのserver.jsを追加。

// react-app/server.js
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./hot.webpack.config');

new WebpackDevServer(webpack(config), {
  contentBase: "../web-app",
  publicPath: config.output.publicPath
}).listen(3000, 'localhost', function (err, result) {
  if (err) {
    console.log(err);
  }

  console.log('Listening at localhost:3000');
});

webpack-dev-serverで使用するhot.webpack.config.jsを用意。webpack.config.jsとの違いは、webpack-dev-server/client?http://localhost:3000entryに含んでいるのと、react-hotloaderに含まれていることです。

// react-app/hot.webpack.config.js
var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: [
    'webpack-dev-server/client?http://localhost:3000',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, '../web-app/js'),
    filename: 'bundle.js',
    publicPath: '/js/'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loaders: ['react-hot', 'jsx'],
      include: path.join(__dirname, 'src')
    }]
  }
}

最後にpackage.jsonにwebpack-dev-serverを起動するために"start": "node server.js"を追加します。

// react-app/package.json
・・・
  "scripts": {
    "deploy": "webpack -p --config webpack.config.js",
    "start": "node server.js"
  },
・・・

これでwebpack-dev-serverを起動します。

$ npm start

http://localhost:3000にアクセスすることで動作確認できます。

さらに、HelloBoxコンポーネントの"Hello, World!"を別の文字列に変更してみましょう。ブラウザが自動でリロードされHelloBoxコンポーネントの変更が反映れました。これでAutomatic Refreshが有効になりました。

Hot Module Replacement

次は、Hot Module Replacementの設定をします。Hot Module Replacementを有効にすると、ブラウザのリロードなしにモジュールの変更を反映することができます。

まず、server.jsにhot: trueを追加します。

// react-app/server.js
new WebpackDevServer(webpack(config), {
  contentBase: "../web-app",
  publicPath: config.output.publicPath,
  hot: true
}).listen(3000, 'localhost', function (err, result) {
  ・・・

次に、hot.webpack.config.jsのentrywebpack/hot/only-dev-serverを追加、HotModuleReplacementPluginというプラグインを追加。プラグイン追加のためにvar webpack = require('webpack');も追記してます。

// react-app/hot.webpack.config.js
var webpack = require('webpack');
module.exports = {
  entry: [
    'webpack-dev-server/client?http://localhost:3000',
    'webpack/hot/only-dev-server',
    './src/index'
  ],
  ・・・
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  ・・・

これで設定は完了です。server.jsを起動し直して、http://localhost:3000にアクセスします。そうするとブラウザのコンソールに以下のようなメッセージが出力され、Hot Module Replacementが有効になっていることが分かります。

[HMR] Waiting for update signal from WDS...
Download the React DevTools for a better development experience: https://fb.me/react-devtools
[WDS] Hot Module Replacement enabled.

では、HelloBoxコンポーネントの内容を変更してみましょう。ブラウザのリロードなしに変更が反映されるはずですが、何も変わりません。コンソールを見ると...

[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] The following modules couldn't be hot updated: (They would need a full reload!)
[HMR]  - 76
[HMR] Nothing hot updated.
[HMR] App is up to date.

"モジュールに変更がないですよ"って言われます。Hot Module Replacementはその名の通りモジュールを置き換える機能なんですね。なので実装したHelloBoxコンポーネントもモジュールとして定義してあげる必要があります。

HelloBoxコンポーネントを別ファイルに記述して、module.exportsに代入しましょう。

// react-app/src/components/HelloBox.js
var React = require('react');

module.exports = React.createClass({
  render: function() {
    return (
      <div>Hello, World!</div>
    );
  }
});

index.jsではHelloBoxモジュールをrequireするように書き換えます。

// react-app/src/index.js
var HelloBox = require('./components/HelloBox.js');

再度、http://localhost:3000にアクセスします。その後、HelloBoxコンポーネントの内容を変更してみましょう。

ブラウザのリロードなしに変更が反映されました!コンソールには以下のようなメッセージが出力されます。

[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR]  - 247
[HMR] App is up to date.

これでHot Module Replacementが実現できました。だいぶ捗りますね!

ここで一旦コンポーネントの名前をHelloBoxからTopLevelに変更しておきます。ファイル名をHelloBox.jsからTopLevel.jsに変更。index.jsは以下のようになります。

var TopLevel = require('./components/TopLevel.js');

ReactDOM.render(
  <TopLevel />,
  document.getElementById('root')
);

React-Bootstrap でナビゲーションバーを実装する

ヘッダーとフッターを実装してみます。

// react-app/src/components/TopLevel.js
module.exports = React.createClass({
  render: function() {
    return (
      <div>
        <header>Header</header>
        Hello, World!
        <footer>footer</footer>
      </div>
    );
  }
});

Hot Module Replacementのおかげで自動で反映されます。

今追加したヘッダーをナビゲーションバーのようなものにします。React-Bootstrapというのを使うとBootstrapのUIをReactから使えます。

React-Bootstrap

これを使用するために、まずはwebpackでCSSを扱えるようにしましょう。css-loaderとstyle-loaderを組み合わせて使います。こちらの記事が参考になりました。 style-loaderを使ってstylesheetをrequireする - Qiita

必要なモジュールをインストールして、webpackのloaderを設定します。

$ npm install css-loader style-loader --save-dev
// react-app/hot.webpack.config.js と react-app/webpack.config.js
module: {
  loaders: [{
    ・・・
  {
    test: /\.css$/,
    loader: "style-loader!css-loader"
  }]
}

CSSだけでなく画像も扱えるようにします。

$ npm install file-loader url-loader --save-dev
// webpack.config.js と hot.webpack.config.js
module: {
  loaders: [{
    ・・・
  {
    test: /\.(png|woff|woff2|eot|ttf|svg)$/,
    loader: 'url-loader?limit=100000'
  }]
}

結果、hot.webpack.config.jsのloadersはこうなります(webpack.config.jsとhot.webpack.config.js両方を編集しなきゃいけないのは工夫の余地ありそう)。

// react-app/hot.webpack.config.js
loaders: [{
  test: /\.js$/,
  loaders: ['react-hot', 'jsx'],
  include: path.join(__dirname, 'src')
},
{
  test: /\.css$/,
  loader: "style-loader!css-loader"
},
{
  test: /\.(png|woff|woff2|eot|ttf|svg)$/,
  loader: 'url-loader?limit=100000'
}]

次はBootstrapをインストールします。bootstrapはjQueryに依存しているのでjQueryもインストールします。

$ npm install bootstrap jquery --save-dev

Bootstrapはモジュール内で、jQuery という変数を使用してます。 WebpackのProvidePluginを使うと各モジュール内でのみ使える変数を定義することができます。これを利用して、Bootstrapモジュール内でjQueryを使用できるようにしてあげます。

// webpack.config.js と hot.webpack.config.js
plugins: [
  new webpack.ProvidePlugin({
     $: "jquery",
     jQuery: "jquery"
  })
]

準備できたので、 React-Bootstrap を使っていきましょう。

$ npm install react-bootstrap --save-dev

React-Bootstrapによって提供されてるReactのコンポーネントを利用します。今回はNavBarモジュールなどを利用します。 https://react-bootstrap.github.io/components.html#navigation

var React = require('react');

var Navbar = require('react-bootstrap').Navbar;
var Nav = require('react-bootstrap').Nav;
var NavItem = require('react-bootstrap').NavItem;

module.exports = React.createClass({
  render: function() {
    return (
      <div>
        <Navbar>
          <Navbar.Header>
            <Navbar.Brand>
              <a href="#">React-Bootstrap</a>
            </Navbar.Brand>
          </Navbar.Header>
          <Nav>
            <NavItem href="/link1">Link1</NavItem>
            <NavItem href="/link2">Link2</NavItem>
          </Nav>
        </Navbar>
        Hello, World!
        <footer>footer</footer>
      </div>
    );
  }
});

Bootstrapを利用するためにindex.jsでrequireしておきます。

// react-app/src/index.js
require('bootstrap/dist/css/bootstrap.css');
require('bootstrap');

サーバーを再起動して、http://localhost:3000にアクセスします。Bootstrapを使ったナビゲーションバーが実装できました!

f:id:bati11:20151231110319p:plain

ナビゲーションバーの部分は別コンポーネントにするとJSXが読みやすくなります。

module.exports = React.createClass({
  render: function() {
    return (
      <div>
        <TopLevelNavBar />
        Hello, World!
        <footer>footer</footer>
      </div>
    );
  }
});

var TopLevelNavBar = React.createClass({
  render: function() {
    return (
      <Navbar>
        <Navbar.Header>
          <Navbar.Brand>
            <a href="#">React-Bootstrap</a>
          </Navbar.Brand>
        </Navbar.Header>
        <Nav>
          <NavItem href="/link1">Link1</NavItem>
          <NavItem href="/link2">Link2</NavItem>
        </Nav>
      </Navbar>
    );
  }
})

おしまい

ホットリロードの設定をして、React-Bootstrapを使ってナビゲーションバーを実装しました。長くなったので、React-Routerを使ったルーティングとサーバーとの連携は次回にします!