フロントエンドエンジニアの岡田です。 昨年末に、弊社のサービス:夜行バス比較なびの一部分をReactで書き換えました。
夜行バス比較なびのJavaScriptは、構築から3年以上たつこともあり、コードの見通しが悪くなってきています。 リグレッションテストなども導入しながら、不具合が起きないように努めてはいますが、テストに時間がかかりすぎるなどの問題がありました。
そこで今回、Reactを導入して、リファクタリングをしました。 いろいろつまずくところもあったので、この記事では、夜行バス比較なびでどうやってReactを使っているかをご紹介します。 SPAサイトの事例はけっこうありますが、運用中のサイトの一部にだけReactを導入、という事例はあまりなさそうなので参考になれば幸いです。
環境
以下の組み合わせで使っています。
Webpack + babel + ESLint(Airbnb)+ imagemin(画像圧縮)+ browser-sync
webpackの環境は、各自の開発PC(Mac)につくります。 もともとRailsのsprocketsを使っていたため、React化(ES2015化)完了までは今まで通りsprockets を使うことにしました。 つまり、Webpackで書き出したファイルを、sprocketsで管理しているディレクトリ以下へコミットしてしまいます。 (良い方法ではないと思いますが、サーバーにnode.jsの環境を作るまでのつなぎです。)
Webpackの設定
webpack.config.js(開発中に使用)
// 画像圧縮 const imagemin = require('imagemin'); const imageminOptipng = require('imagemin-optipng'); const imageminMozjpeg = require('imagemin-mozjpeg'); const imageminGifsicle = require('imagemin-gifsicle'); imagemin(['images/**/*.{gif,jpg,png}'], '../public/images', { plugins: [ imageminGifsicle(), imageminMozjpeg(), imageminOptipng(), ] }).then(files => { console.log(files); //=> [{data: <Buffer 89 50 4e …>, path: 'build/images/foo.jpg'}, …] }); // エントリーポイントの設定 module.exports = { entry: { // PC用エントリーポイント 'es/pc/es2015': './src/scripts/entry/navi/pc.jsx', // SP用エントリーポイント 'es/sp/es2015': './src/scripts/entry/navi/sp.jsx', }, output: { // 出力ファイルのベースとなる階層 // 例:PCは/app/assets/javascripts/es/pc/es2015.js に出力される // 出力された /app/assets/javascripts/es/pc/es2015.js はsprocketsへ任せる path: '../app/assets/javascripts', filename: '[name].js', }, module: { preLoaders: [{ test: /\.(js|jsx)$/, loader: 'eslint-loader', exclude: /node_modules/, },], loaders: [{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: ['/node_modules/'], } ], }, eslint: { configFile: './.eslintrc', }, };
webpack-production.config.js(production buildで使用)
const webpack = require('webpack'); module.exports = { entry: { 'es/pc/es2015': './src/scripts/entry/navi/pc.jsx', 'es/sp/es2015': './src/scripts/entry/navi/sp.jsx', }, output: { path: '../app/assets/javascripts', filename: '[name].js', }, module: { loaders: [{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: ['/node_modules/'], },], }, // 以下の部分でproduction用で書き出し plugins: [ new webpack.DefinePlugin({ // process.env.NODE_ENVを'production'に置き換える 'process.env.NODE_ENV': JSON.stringify('production'), }), // UglifyJsPluginの実行 new webpack.optimize.UglifyJsPlugin({ compress: { // 圧縮する時に警告を除去する warnings: false, }, }), ], };
最初は圧縮&難読化もsprocketsへ任せていましたが、本番環境でJSエラーが出るため、webpackで行うことにしました。
package.json(scripts部分のみ)
{ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack --watch & browser-sync start --config ./bs-config.js", "build": "webpack --config webpack-production.config.js --progress" }, }
ディレクトリ構成
frontend ├── images ├── src │ └── scripts │ ├── entry // エントリーポイントのファイルを設置 │ │ └── navi │ │ ├── pc.jsx │ │ └── sp.jsx │ ├── component // ページをまたいで使うパーツを設置(js, jsx) │ │ ├── common │ │ │ └── xxxxxxxx.js │ │ └── pc │ │ └── { コンポーネント名 } │ │ ├── xxxxxxxx.jsx │ │ └── xxxxxxxx.jsx │ ├── page // ページごとにディレクトリを分けて設置(js, jsx) │ │ └── { ページ名 } │ │ └── sp │ │ ├── xxxxxxxx.jsx │ │ ├── xxxxxxxx.jsx │ │ └── xxxxxxxx.jsx │ ├── model // ビジネスロジック │ │ ├── xxxxxxxx.js │ │ └── xxxxxxxx.js │ └── util // 便利関数 │ ├── xxxxxxxx.js │ └── xxxxxxxx.js ├── bs-config.js ├── package.json ├── webpack-production.config.js └── webpack.config.js
- classは1ファイル1クラス
- 関数は1ファイルに複数可
エントリーポイントの書き方
Reactを導入しているのは一部のページなので、エントリーポイントで必要な場所にrenderしたり、関数を呼び出すよう指定をします。
// 必要なモジュールをインポート import React from 'react'; import ReactDOM from 'react-dom'; import History from '../../component/pc/history/History.jsx'; // ページごとに実行する関数や読み込むコンポーネントを指定 if (document.getElementById('history-result')) { ReactDOM.render( <History />, document.getElementById('history-result')); }
ちなみにスマホサイトについては、jsのファイルの容量が増えることによりパフォーマンスに影響を与える可能性があるため、現時点では対象ページのみReact & ES2015ファイルを読み込んでいます。
今後の課題
まだ手探りで進めていて、今後も順次置き換え・リリース予定です。 今のところ課題は以下のとおりです。
- メインとなる検索結果ページは、Ajaxでできていることもありjsのコード量が特に多いので、どのようにReact化を進めるか(分割して進められるのか?)
- リリースフローの見直し(production buildはサーバーへ任せたい)
これらもまた解決したらご報告したいと思います。