reactjsでwebアプリを作成するにあたって、とても参考になりそうなreact-webpack-nodeというBoilerplateを見つけたので、中身を見てみる。
構成は、
- React + Redux
- Express
- MongoDB + mongoose
となっており、フルスタック。
jsのmodule bundlerはwebpackで、hot reloadにもserver side renderingにも対応。
ログインのサンプル実装まである上に、herokuへのdeploy手順まで記載されているので、
reactjsで何か作りたい時はこれを土台に単に機能を乗せていくだけで良い感が...
まずは勉強がてらcommitされているファイルを軽めにみていく。
package.jsonをみてみる
scriptsは、
"scripts": {
"clean": "rimraf public",
"start": "NODE_ENV=production node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
"build:webpack": "NODE_ENV=production webpack --progress --colors --config ./webpack/webpack.config.prod.js",
"build": "npm run clean && npm run build:webpack",
"postinstall": "npm run build"
},
が用意されている。
dependenciesとしては、
- babel/webpackのprefixがついたもの
- react/reduxのprefixがついたもの
- express/mongoのprefixがついたもの
が大半を占める。
dependenciesに入っているmoduleの中で気になったものは以下。
- helmet :
- http headerをセットすることでexpressアプリをよりsecureに。
- mongoose :
- mongodbのobjectのmodel化を可能とするmodule。
- react-helmet :
- helmetとは関係なく、htmlのheadセクション(title, meta, link, script, base tags)のreact component化を可能とするmodule。
- deep-equal :
- オブジェクトの階層を潜って比較してくれるassert.deepEqualのアルゴリズム改善版実装(5倍速とのこと)。
- passport :
- 認証モジュール。expressのmiddlewareとして使う。
- redbox-react :
- reactのエラーを赤いボックスでpretty format表示。
webpackのconfigファイルを見てみる
続いて、webpack用のconfigファイル。
- [root]/webpack/webpack.config.dev.js
- [root]/webpack/webpack.config.prod.js
の2種類が配置されている。
これらのファイルがどう使われているかを見るために、再度package.jsonのscriptsを確認すると、
"scripts": {
...
"start": "NODE_ENV=production node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
...
},
となっており、ここでは単にNODE_ENVを切り替えてserver/index.jsを起動しているだけとなっている。
server/index.jsを見ると
var webpack = require('webpack');
var config = require('../webpack/webpack.config.dev.js');
var compiler = webpack(config);
と記載があり、webpack.config.dev.jsを元にしたwebpackのcompilerオブジェクトが生成されている。
ただ、下の方でNODE_ENVがdevelopmentだった場合のみwebpack系middlewareを登録しているので
var isDev = process.env.NODE_ENV === 'development';
if (isDev) {
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
}
productionの時にcompilerが使われることはない(appはexpressオブジェクト)。
ではwebpack.config.prod.jsはどこで使われているかというと
"scripts": {
...
"build:webpack": "NODE_ENV=production webpack --progress --colors --config ./webpack/webpack.config.prod.js",
"build": "npm run clean && npm run build:webpack",
"postinstall": "npm run build"
},
で指定されている。
ということでソースを修正した場合、npm startをする前にnpm run buildを実行する必要がある。
webpack.config.dev.jsをみてみる
続いて、webpack.config.dev.jsの中身を見ていく。
ファイルの頭の方で
var hotMiddlewareScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true';
と、webpack-hot-middlewareのconfigが定義されている。
webpack-hot-middlewareはwebpack-dev-serverを使わずにhot reloadingを実現するmodule。
module.exports = {
...
entry: {
app: ['./client', hotMiddlewareScript]
},
}
というようにentryに追加することで、jsが再構築された通知を受けてクライアントを更新することが可能となる。
ちなみに、'./client'は[root]/app/client.jsxのことで、reactのstoreの作成とreact-reduxのProviderのrenderが実行される。
※webpack-hot-middlewareを使うには、entryにconfigを指定する以外にも設定が必要。
entryで指定されたソースをcompileした結果の出力設定は、
module.exports = {
...
output: {
// The output directory as absolute path
path: assetsPath,
// The filename of the entry chunk as relative path inside the output.path directory
filename: '[name].js',
// The output path from the view of the Javascript
publicPath: '/assets/'
},
}
となっており、ここで[name].jsの[name]にはentryのkeyのappが入るため、結果app.jsが出力される。
他には、
module.exports = {
...
module: {
loaders: commonLoaders.concat([
{ test: /\.scss$/,
loader: 'style!css?module&localIdentName=[local]__[hash:base64:5]' +
'&sourceMap!autoprefixer-loader!sass?sourceMap&outputStyle=expanded' +
'&includePaths[]=' + encodeURIComponent(path.resolve(__dirname, '..', 'app', 'scss'))
}
])
},
}
という設定もある。module.loadersに指定しているcommonLoadersの定義は
var commonLoaders = [
{
/*
* TC39 categorises proposals for babel in 4 stages
* Read more http://babeljs.io/docs/usage/experimental/
*/
test: /\.js$|\.jsx$/,
loaders: ['babel'],
include: path.join(__dirname, '..', 'app')
},
{ test: /\.png$/, loader: 'url-loader' },
{ test: /\.jpg$/, loader: 'file-loader' },
{ test: /\.html$/, loader: 'html-loader' }
];
となっていて、js/jsxやasset系のloaderを設定している。
このloaderは単にファイル読み込みをするのではなく、ファイル読み込み時に前処理を施すもの。
例えばjsに関してはbabelを指定しているので、読み込み対象のファイルがes6形式で記載されていても
babelがよろしく変換してくれる(ということのはず)。
webpack.config.prod.jsをみてみる
次にwebpack.config.prod.jsを見る。
module.exportsにserver side rendering(ssr)用のconfigが追加されている点がdevと異なる。
ssr用のentryとしては"./server"が指定されており、このserver.jsxでは
export default function render(req, res) {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
fetchTopics(apiResult => {
const authenticated = req.isAuthenticated();
const store = configureStore({
// reducer: {initialState}
topic: {
topics: apiResult
},
user: {
authenticated: authenticated,
isWaiting: false
}
});
const initialState = store.getState();
const renderedContent = renderToString(
<Provider store={store}>
<RoutingContext {...renderProps} />
</Provider>);
const renderedPage = renderFullPage(renderedContent, initialState, {
title: headconfig.title,
meta: headconfig.meta,
link: headconfig.link
});
res.status(200).send(renderedPage);
});
} else {
res.status(404).send('Not Found');
}
});
};
というようにrender関数がexportsされており、dispatchされたら初期描画に必要なデータを取得した上でstoreオブジェクトに詰めて、
renderToStringでhtmlを生成する。
ここで若干気になったのは、ssr対応のエンドポイントごとにこの処理を実装しなければならない点。
fluxの部分も含めてサーバーサイドで処理しちゃって初期表示できちゃうような勝手なイメージがあった。
ただ考えてみたらこれはしょうがない部分である気もする。
reduxのreducerなどでfetchした結果を加工する箇所を共通化しておけば、
エンドポイントを増やしていくのもそんなに辛くならないかも?
長くなったので、ここまでにしてアプリケーションソースは別記事に。