普段の仕事はサーバーサイドばっかりだったので、年末年始でフロントエンドの知識をアップデートしたいなぁ、ReactかVue.jsやろうかなぁと思ってました。それで、こちらの記事を読んでると...
サーバサイドJavaをずっとやってきて、モダンなJSの知識や経験があまりないけど、最近Reactってのが話題になっているのがさすがに気になるので挑戦したい人
これは!Javaだけってわけじゃないけど、ずっとサーバーサイドってまさに自分のことじゃないか!ということでコードを読んでみることに。
リポジトリをcloneしてreact-appディレクトリを一旦削除して、react-appを自分でつくり直しながら学ぶことにしました。コード読んでみて分からないことが色々あったので、一歩一歩分からないところを潰しながらやっていきました。その時の備忘録です。サーバーサイドはGrailsで実装されてるのをそのまま使います。
長くなったので2回に分けて。今回はホットリロードの設定をして、Bootstrapを使ってナビゲーションバーを実装するとこまでです。React Routerやサーバー通信は次回。
使用した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します。
ディレクトリ構成は、バックエンドとフロントエンドで分かれてます。バックエンドは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 の意味を確認しながら作ってみます。
- https://github.com/uehaj/grails-react-boilerplate/blob/master/react-app/webpack.config.js
- tutorials/getting-started
とりあえずこれで最小限。
// 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.jsonのscripts
に、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っていうのを使うって書いてあります。
読んでみると、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:3000
をentry
に含んでいるのと、react-hot
がloader
に含まれていることです。
// 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のentry
にwebpack/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から使えます。
これを使用するために、まずは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を使ったナビゲーションバーが実装できました!
ナビゲーションバーの部分は別コンポーネントにすると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を使ったルーティングとサーバーとの連携は次回にします!