Help us understand the problem. What is going on with this article?

React,Redux,Express,Mongoのフルスタックなboilerplate

More than 3 years have passed since last update.

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した結果を加工する箇所を共通化しておけば、
エンドポイントを増やしていくのもそんなに辛くならないかも?

長くなったので、ここまでにしてアプリケーションソースは別記事に。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away