LoginSignup
13
13

More than 5 years have passed since last update.

ES6で書くIsomorphicアプリ入門 - Part3: React Isomorphic Demo

Last updated at Posted at 2015-07-12

ES6で書くIsomorphicアプリのBoilerplateを調べました。いくつか手を動かしながら勉強していこうと思います。最初はなるべくシンプルなIsomorphicな動作を選びたいのですが、Reactアプリは複雑になりがちで周辺ツールも多くどの構成を選んだら良いか悩みます。Tutorial: Setting Up a Simple Isomorphic React appのポストがとてもわかりやすいので写経していきます。

Isomorphicな特徴

IsomorphicなReactアプリはクライアントのコードは1つのファイルにバンドルして、HTMLからロードします。最初にHTMLを開いたときはサーバーサイドでレンダリングされたコンポーネントを表示しますが、バンドルされたReactアプリがロードされたら上書きします。

プロジェクト

Tutorial: Setting Up a Simple Isomorphic React appのリポジトリはreact-isomorphic-boilerplateです。

今回作成したディレクトリ構造です。作業リポジトリはこちらです。

$ cd ~/node_apps/react-isomorphic-boilerplate-app
$ tree
.
├── Dockerfile
├── README.md
├── css
├── docker-compose.yml
├── node_modules -> /dist/node_modules
├── package.json
├── src
│  ├── client
│  │   └── entry.js
│  ├── server
│  │   ├── server.js
│  │   └── webpack.js
│  └── shared
│      ├── components
│      │  └── AppHandler.js
│      └── routes.js
├── views
│  └── index.jade
└── webpack.config.dev.js

Dockerfile

io.jsのベースイメージを使いDockerfileを用意します。

~/node_apps/react-isomorphic-boilerplate-app/Dockerfile
FROM iojs:2.3
MAINTAINER Masato Shimizu <ma6ato@gmail.com>

RUN mkdir -p /app
WORKDIR /app

RUN adduser --disabled-password --gecos '' --uid 1000 docker && \
    mkdir -p /dist/node_modules && \
    ln -s /dist/node_modules /app/node_modules && \
    chown -R docker:docker /app /dist/node_modules

USER docker
COPY package.json /app/
RUN  npm install

COPY . /app
CMD ["npm","start"]

docker-compose.yml

docker-compose.ymlには環境変数としてpublic ip addressを指定します。HTMLからWebpack dev serverに接続するためのIPアドレスです。サーバーサイドはクラウド上で動作しているため、ブラウザからはリモートでアクセスする必要があります。PUBLIC_IPにインターネットから接続できるIPアドレスを指定します。

~/node_apps/react-isomorphic-boilerplate-app/docker-compose.yml
npm:
  build: .
  volumes:
    - .:/app
    - /etc/localtime:/etc/localtime:ro
  environment:
    - PUBLIC_IP=xxx.xxx.xxx.xxx
    - EXPRESS_PORT=3030
    - WEBPACK_PORT=8090
  ports:
    - 3030:3030
    - 8090:8090

docker-composeコマンドを短縮するためにエイリアスを用意します。

~/.bashrc
alias iojs-run='docker-compose run --rm npm'
alias iojs-up='docker-compose up npm'

package.jsonはデフォルトで最小限を書いておきます。

~/node_apps/react-isomorphic-boilerplate-app/package.json
{
    "name": "react-isomorphic",
    "description": "react-isomorphic",
    "version": "0.0.1",
    "private": true
}

docker-composeのエイリアスを使って必要なモジュールをnpmでインストールします。

$ iojs-run npm install --save-dev react webpack react-router react-hot-loader webpack-dev-server babel-loader

用意しておいたpackage.jsonにdevDependenciesのセクションが追加されました。

~/node_apps/react-isomorphic-boilerplate-app/package.json
{
    "name": "react-isomorphic",
    "description": "react-isomorphic",
    "version": "0.0.1",
    "private": true,
    "devDependencies": {
        "babel": "^5.6.14",
        "babel-core": "^5.6.17",
        "babel-loader": "^5.3.1",
        "express": "^4.13.1",
        "jade": "^1.11.0",
        "node-libs-browser": "^0.5.2",
        "nodemon": "^1.3.7",
        "react": "^0.13.3",
        "react-hot-loader": "^1.2.8",
        "react-router": "^0.13.3",
        "webpack": "^1.10.1",
        "webpack-dev-server": "^1.10.1"
    },
    "scripts": {
        "clean": "rm -rf lib",
        "watch-js": "babel src -d lib --experimental -w",
        "dev-server": "node lib/server/webpack.js",
        "server": "nodemon lib/server/server.js",
        "start": "npm run watch-js & npm run dev-server & npm run server",
        "build": "npm run clean && babel src -d lib --experimental"
    }
}

scriptsセクションに開発に必要なコマンドを用意します。ごちゃごちゃしているのでGulpでまとめた方が良さそうです。ホットロードもしますが、ES6で書いたコードはbuildでコンパイルしておきます。ブラウザからのリクエストを処理するExpressのサーバーと、バンドルされたReactアプリを返すWebpack dev serverの2つを起動します。

package.jsonが出来上がったのでイメージにビルドします。

$ docker-compose build

サーバーサイド

プロジェクトにプログラムを配置するディレクトリを作成します。

$ mkdir -p src/{server,shared,client} views
  • server: ExpressとWebpack dev server
  • client: Reace bundleのエントリポイント
  • shared: componentsとroutes

views/index.jade

ビューはJadeのテンプレートエンジンを使います。#app!= contentの記述で<div id="app">要素を作成します。

~/node_apps/react-isomorphic-boilerplate-app/views/index.jade
html
  head
    title="React Isomorphic App"
    meta(charset='utf-8')
    meta(http-equiv='X-UA-Compatible', content='IE=edge')
    meta(name='description', content='')
    meta(name='viewport', content='width=device-width, initial-scale=1')

  body
    #app!= content
    script(src='http://'+public_ip+':'+webpack_port+'/js/app.js', defer)

src/server/server.js

ExpressのコードもES6で書きます。routeは/*で全部拾ってreact-routerに渡します。このプロジェクトにはfavicon.icoを用意していませんが、staticなコンテンツもreact-routerにroutingの役割が回ってしまいます。

Warning: No route matches path "/favicon.ico". Make sure you have <Route path="/favicon.ico"> somewhere in your routes

react-routerはサーバーとクライアントでshared/routesを共有しています。デフォルトのサンプルだと同じコンポーネントを使っているので、Expressが<div id="app">にrenderしたcontentと、React bundleがロードされた後に、document.getElementById('app')でrenderする内容の区別がつきません。Isomorphicではなくなりますが、処理をわかりやすくするためにcontents変数にはコンポーネントではなく、デフォルトの文字列を入れるようにしました。

~/node_apps/react-isomorphic-boilerplate-app/src/server/server.js
import express from 'express';
import React from 'react';
import Router from 'react-router';
const app = express();

// set up Jade
app.set('views', './views');
app.set('view engine', 'jade');

import routes from '../shared/routes';

app.get('/*', function(req, res) {
    Router.run(routes, req.url, Handler => {
        //let content = React.renderToString(<Handler />);
        let content = 'empty!';
        res.render('index', { public_ip_port: process.env.PUBLIC_IP_PORT,
                              content: content });
    });
});

var server = app.listen(process.env.EXPRESS_PORT, function() {
    var host = server.address().address;
    var port = server.address().port;
    console.log('Example app listening at http://%s:%s', host, port);
});

src/server/webpack.js

Reactアプリをレンダーする開発用サーバーです。Node.jsのサーバーとは別に動作します。WebpackDevServerインスタンスは、webpack.config.dev.jsに書かれた設定を読み込んで使います。今回はサーバーサイドがクラウド上で動作しているため、Webpack dev serverはリモートから接続します。localhostでなく0.0.0.0でLISTENするように変更しました。

~/node_apps/react-isomorphic-boilerplate-app/src/server/webpack.js

import WebpackDevServer from "webpack-dev-server";
import webpack from "webpack";
import config from "../../webpack.config.dev";

var server = new WebpackDevServer(webpack(config), {
    // webpack-dev-server options
    publicPath: config.output.publicPath,
    hot: true,
    stats: {colors: true},
});

server.listen(process.env.WEBPACK_PORT, "0.0.0.0", function() {});

webpack.config.dev.js

entryにapp.jsにbundleするエントリポイントを複数指定します。

  • webpack-dev-serverのホストとポート
  • ホットロード
  • アプリのクライアント
~/node_apps/react-isomorphic-boilerplate-app/webpack.config.dev.js
var webpack = require('webpack');

var public_url = 'http://'+process.env.PUBLIC_IP+':'+process.env.WEBPACK_PORT;

module.exports = {
    devtool: 'inline-source-map',
    entry: [
        'webpack-dev-server/client?'+public_url,
        'webpack/hot/only-dev-server',
        './src/client/entry',
    ],
    output: {
        path: __dirname + '/public/js/',
        filename: 'app.js',
        publicPath: public_url+'/js/',
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin(),
    ],
    resolve: {
        extensions: ['', '.js']
    },
    module: {
        loaders: [
            { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader?experimental'], exclude: /node_modules/ }
        ]
    }
}

共有

src/shared/components/AppHandler.js

AppHandler.jsはサーバーとクライアントで共有しているコンポーネントです。今回のテストではExpressからIsomorphicにコンポーネントを共有して、サーバーサイドレンダリングできることを確認した後、静的な文字列に変更しています。

~/node_apps/react-isomorphic-boilerplate-app/src/shared/components/AppHandler.js
import React from 'react';

export default class AppHandler extends React.Component {
    render() {
        return <div>Hello App Handler</div>;
    }
}

src/shared/routes.js

react-routerのroutesを定義します。Routeはpathの/にAppHandlerコンポーネントを表示します。

~/node_apps/react-isomorphic-boilerplate-app/src/shared/routes.js
import { Route } from 'react-router';
import React from 'react';

import AppHandler from './components/AppHandler';

export default (
    <Route handler={ AppHandler } path="/" />
);

クライアント

src/client/entry.js

React bundleのエントリポイントです。サーバーサイドではReact.renderToString(<Handler />);でJadeでレンダリングするcontents変数を作成しましたが、クライアントサイドではReact.render(<Handler />, document.getElementById('app'));を使って、直接divのidを指定してコンポーネントをマウントします。

~/node_apps/react-isomorphic-boilerplate-app/src/client/entry.js
import React from 'react';
import Router from 'react-router';
import routes from '../shared/routes';

Router.run(routes, Router.HistroyLocation, (Handler, state) => {
    React.render(<Handler />, document.getElementById('app'));
});

起動とテスト

一応クリーンビルドしておきます。

$ iojs-run npm run clean
$ iojs-run npm run build
...
npm info postclean react-isomorphic@0.0.1
npm info ok
src/client/entry.js -> lib/client/entry.js
src/server/server.js -> lib/server/server.js
src/server/webpack.js -> lib/server/webpack.js
src/shared/components/AppHandler.js -> lib/shared/components/AppHandler.js
src/shared/routes.js -> lib/shared/routes.js
npm info postbuild react-isomorphic@0.0.1
npm info ok

docker-compose up npmのエイリアスである、iojs-upを実行します。

$ iojs-up
...
npm_1 | webpack: bundle is now VALID.

ブラウザからDockerホストのパブリックIPアドレスを実行します。接続先はExpressサーバーの3030ポートです。

本来はIsomorphicに共有しているコンポーネントをサーバーサイドでrenderします。React bundleが上書きする動作を確認するため、'empty!'の静的な文字列が最初にrenderします。index.htmlがロードされたあと、React bundleがWebpack dev serverからロードされます。react-router/pathに従いAppHandler.jsがcomponentとして表示されます。

課題

シンプルなサンプルなのでとてもわかりやすいですが、Expressのrouteを/*としてすべてreact-routerに渡しています。その反面favicon.icoなど静的ファイルもreact-routerでハンドリングが必要になったり、Isomorphicで共有いしているコンポーネントの動作が見えづらいところがあります。

13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13