SSR を利用した SPA の実装 ~ クライアントサイドルーティング の実装 ~

Overview

SPA におけるページ遷移は, (API のやりとりは別として) サーバーを介さずに, クライアントサイドのみで処理をします. このとき, URL が変更されないと, SEO 対策に不利であったり, ユーザーがそのページをブックマークできないなどの不都合が生じます.

したがって, 遷移したページに応じて URL も変更する必要があります. SPA では, この処理をクライアントサイド側で実装します.

これが, クライアントサイドルーティングです. クライアントサイドルーティングの実装には, 大きく 2 つのアプローチがあります.

  • History API を利用して自前でルーティング
  • react-router を利用する

この記事では一般的な方法である react-router (4.x) を利用した, クライアントサイドルーティングの実装方法について紹介したいと思います.

1. Install react-router

$ npm install --save react-router-dom

2. Implement Component for Routing

まずは, ルーティングを実装する前に, 前回の記事で実装した コンポーネント以外に, と コンポーネントを実装しておきます. いずれも確認のためのコンポーネントなので実装は単純です.

components/Home/index.js

import React from 'react';

const Home = () => (
<h2>Home</h2>
);

export default Home;

components/About/index.js

import React from 'react';

const About = () => (
<h2>About</h2>
);

export default About;

さて, ここから, クライアントサイドルーティングのコアとなる コンポーネントを実装します

component/Router/index.js

import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';
import Home from '../../components/Home';
import About from '../../components/About';
import Counter from '../../components/Counter';

export default class Router extends Component {
    render() {
        return (
            <BrowserRouter>
                <nav>
                    <ul>
                        <li><Link to="/>>Home</Link></li>
                        <li><Link to="/about">About</Link></li>
                        <li><Link to="/counter">Counter from 0</Link></li>
                        <li><Link to="/counter/100">Counter from 100</Link></li>
                    </ul>

                    <Route exact path="/" component={Home} />
                    <Route path="/about" component={About} />
                    <Route exact path="/counter" component={Counter} />
                    <Route path="/counter/:count" component={Counter} />
                </nav>
            </BrowserRouter>
        );
    }
}

まず, <nav> の部分ですが, <Link /> コンポーネントを使ってナビゲーションを実装する必要があります. <Link /> コンポーネントは <a /> にレンダリングされますが, 直接 <a /> タグでナビゲーションを実装しないように注意してください. <a /> タグでナビゲーションを実装してしまうと通常のページ遷移 (サーバーにリクエストを送信してレスポンスを受信してページを描画する) となってしまいます (<Link /> コンポーネントのソース). <Link /> コンポーネントの to props に記述した文字列がパスとなります.

そして, クライアントサイドルーティングを実際に担っているのが <Route /> コンポーネントです. path には, URL のパスを, component には, path で指定した URL にアクセスした場合にレンダリングするコンポーネントを指定します. また, path には, 変数を渡すことも可能で, その場合は : をつけて, :count のように指定します. このようにすることで, 対応するコンポーネントの props にその値が渡されます.

そこで, <Counter /> コンポーネントを少し改変します.

components/Counter/index.js

import React, { Component } from 'react';

export default class Counter extends Component {
    static CLASS_NAME = 'Counter';

    constructor(props) {
        super(props);

        const count = parseInt(props.match.params.count);

        this.state = {
            count : isNaN(count) ? 0 : count
        };
    }

    // ...

こうすることで, 例えば, /counter/100 にアクセスした場合, props.match.params.count に 100 が渡されるので, state の初期値は 0 ではなく, 100 になります.

exact を指定することで, / にアクセスした場合のみ <Home /> コンポーネントを描画するようにします.

exact を指定しない場合, /about や /counter にアクセスしても <Home /> コンポーネントが描画されてしまいます (もっとも, 意図的にそうするのであれば, exact を指定しないようにします).

<Route exact path="/" component={Home} />

同様の理由で, /counter にも exact を指定しています.

<Route exact path="/counter" component={Counter} />

3. Render Router Component

コンポーネントが実装できればあとはそれをレンダリングするだけです.

import React from 'react';
import ReactDOM from 'react-dom';
import Router from './components/Router';

ReactDOM.render(<Router />, document.getElementById('app'));

最後に, サーバーサードにもルーティングを追加します. そうしないと, 例えば, 直接 /about にアクセスしたときに, 404 となってしまうからです.

app-server.js

app.get('/', (req, res, next) => {
    render(req, res);
});

app.get('/about', (req, res, next) => {
    render(req, res);
});

app.get('/counter', (req, res, next) => {
    render(req, res);
});

app.get('/counter/:count', (req, res, next) => {
    render(req, res);
});

SSR を利用した SPA の実装 ~ クライアントサイドレンダリング の実装 ~

Overview

前回までの記事で, 簡単な SSR (Server Side Rendering) を実装しました. しかしながら, SSR を利用した SPA (Single Page Application) であっても, ほとんどのケースでクライアントサイドレンダリングが必要になるはずです.

この記事では, 前回までの内容の続きとしてクライアントサイドレンダリングを実装します.

1. Install packages

$ npm install --save-dev babel-loader babel-plugin-transform-class-properties css-loader extract-text-webpack-plugin postcss-easy-import postcss-loader webpack

2. Implement React Component

今回は, 単純なカウンター機能をもつ, コンポーネントを実装します. まあ, 特にトリッキーなことはない, 単純なコンポーネントです. ちなみに, CSS の class 名は BEM の命名規則に沿っています.

components/Counter/index.js

import React, { Component } from 'react';

export default class Counter extends Component {
    static CLASS_NAME = 'Counter';

    constructor(props) {
        super(props);
        this.state = {
            count : 0
        };
    }

    onClickUpButton() {
        this.setState({ count : this.state.count + 1 });
    }

    onClickDownButton() {
        this.setState({ count : this.state.count - 1 });
    }

    render() {
        const { count } = this.state;

        return (
          <div className={Counter.CLASS_NAME}>
              <p>
                  <button type="button" className={`${Counter.CLASS_NAME}__up Button`}   onClick={this.onClickUpButton.bind(this)}>+</button>
                  <button type="button" className={`${Counter.CLASS_NAME}__down Button`} onClick={this.onClickDownButton.bind(this)}>-</button>
              </p>
              <p>
                  <span className={`${Counter.CLASS_NAME}__count`}>{count}</span>
              </p>
          </div>
        );
    }
}

3. Implement Client Side Rendering

実装した コンポーネントをレンダリングする処理を実装します.

client.js

import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './components/Counter';

ReactDOM.render(<Counter />, document.getElementById('app'));

4. Implement CSS

まずは, 各 CSS ファイルのインポートのみする CSS ファイルを実装します.

main.css

@charset "UTF-8";

@import "./styles/base.css";
@import "./components/**/*.css";

./styles/base.css には, リセットスタイルや共通のスタイルを記述します. ./components/**/*.css は, コンポーネント特有のスタイルを記述します.

styles/base.css

:root {
    --font-size-normal: 16px;
}

:root {
    --base-color  : #ff1493;
    --assort-color: #ffbfe1;
    --white       : #ffffff;
    --black       : #000000;
}

* {
    margin: 0;
    padding: 0;
}

button {
    border: none;
    outline: none;
    background-color: none;
}

body {
    line-height: 1.5;
    font-family: Helvetica, Arial, sans-serif;
    color: var(--base-color);
}

.Button {
    cursor: pointer;
    color: var(--white);
    text-align: center;
    background-color: var(--base-color);
    transition: background-color ease 0.6s;
}

.Button:hover {
    background-color: var(--assort-color);
}

.Button:active {
    box-shadow: 0px -1px 3px rgba(0, 0, 0, 0.2);
}

components/Counter/index.css

.Counter {
    margin-top: 24px;
    text-align: center;
}

.Counter button:not(:first-child) {
    margin-left: 12px;
}

.Counter__up,
.Counter__down {
    width: 128px;
    height: 128px;
    line-height: 128px;
    font-size: calc(var(--font-size-normal) * 2);
    border-radius: 12px;
}

.Counter__count {
    font-size: calc(var(--font-size-normal) * 4);
}

5. Edit webpack.config.js

webpack.config.js

const webpack           = require('webpack');
const ExtrackTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: ['./client.js', './main.css'],
  output: {
    path: `${__dirname}/public`,
    filename: 'app.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: ExtrackTextPlugin.extract({
          use: [
            'css-loader',
            'postcss-loader'
          ]
        })
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        postcss: [
          require('postcss-easy-import')({ glob: true })
        ]
      }
    }),
    new ExtrackTextPlugin('app.css')
  ],
  devtool: 'source-map'
};

6. Fix render.js

クライアントサイドレンダリングを担う app.js と app.css の読み込みを追加します.

render.js

const React          = require('react');
const ReactDOMServer = require('react-dom/server');
const App            = require('./App').default;

function render(req, res) {
    const content = ReactDOMServer.renderToString(React.createElement(App));
    const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
    <link rel="stylesheet" href="/app.css" type="text/css" media="all" />
</head>
<body>
    <header>
        ${content}
    </header>
    <div id="app"></div>
    <script type="text/javascript" src="/app.js"></script>
</body>
</html>`;

    res.status(200).send(html);
};

module.exports = render;

7. Add npm scripts

最後に, npm scripts に, クライアントサイドのビルド処理を追加して完成です.

package.json

  // ...
  "scripts": {
    "build": "babel components/App.js --out-file App.js && webpack",
    "start": "node app-server.js"
  },
  // ...
$ npm run build

変更したファイルをコミットして,

$ git push heroku master

Node.js + React で SSR ~ SSR の実装 ~

Overview

前回の記事で SSR の環境を構築できたので, さっそく, React を利用した簡単な SSR の実装をしてみましょう.

1. Install packages

$ npm install --save react react-dom babel-cli babel-core babel-preset-es2015 babel-preset-react express

前回の記事でも述べましたが, 本来は –save-dev でインストールすべきパッケージも –save でインストールしていることに注意してください (Heroku のデプロイで dependencies のパッケージがインストールの対象とならないので).

2. Implement React Component

SSR で利用する, React コンポーネントを実装します. といっても, とりあえず <h1 /> で Hello SSR と表示する単純なコンポーネントです.

components/App.js

import React, { Component } from 'react';

export default class App extends Component {
    render() {
        return <h1>Hello SSR</h1>:
    }
}

また, ビルド後の App.js は, ルート直下の App.js にデプロイすることにします.

3. Implement render function

前回の記事では, express のルーターに直接レンダリング処理を記述していましたが, それだとコードの見通しがよくないので, SSR をする専用の関数を実装します.

render.js

const React          = require('react');
const ReactDOMServer = require('react-dom/server');
const App            = require('./App').default;

function render(req, res) {
    const content = ReactDOMServer.renderToString(React.createElement(App));
    const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
</head>
<body>
    <section id="app">
        ${content}
    </section>
</body>
</html>`;

    res.status(200).send(html);
};

module.exports = render;

ポイントは, ReactDOMServer.renderToString でコンポーネントを HTML 文字列に変換している処理です. あとは, その文字列を HTML に埋め込むだけです.

render 関数が実装できたので, app-server.js を以下のように書き換えます.

app-server.js

'use strict';

const express = require('express');
const app     = express();
const path    = require('path');
const render  = require('./render');

const port = process.env.PORT || 8080;

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res, next) => {
    render(req, res);
});

app.listen(port, () => {
    console.log(`Listening on port ${port} ...`);
});

4. npm scripts

package.json に以下のスクリプトを追加します.

  // ...
  "scripts": {
    "build": "babel components/App.js --out-file App.js",
    "start": "node app.js"
  },
  // ...

5. Edit Procfile

Procfile を以下のように変更します.

web: npm run build && npm start

 

以上で, SSR の実装ができました. 変更したファイルをすべてコミットして,

$ git push heroku master

を実行し, https://nodejs-ssr-sample.herokuapp.com/ にアクセスして, 「Hello SSR」が表示されていれば OK です.

ちなみに, SSR された HTML のソースは以下のようになっています.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
</head>
<body>
    <section id="app">
        <h1 data-reactroot="" data-reactid="1" data-react-checksum="-601091822">Hello SSR</h1>
    </section>
</body>
</html>

Node.js + React で SSR ~ Heroku の利用 ~

Overview

前回までの記事で, SPA (Single Page Application) の開発環境構築に関して紹介しましたが, SPA では, 以下のような理由から, SSR (Server Side Rendering) を併用することも多いです.

  • 初期表示速度の改善
  • レガシークローラーやページ解析への対応

したがって, SSR の最低限の実装を紹介していきたいと思います.

しかしながら, SSR という名前からもわかるように, レンダリングをしてくれるサーバーが必要となります. 今回は, サーバーサイドのスクリプトには Node.js を使って, Isomorphic な実装をしたいと思います. したがって, Node.js が使えるサーバーが必要になります.

Node.js が使えて, 無料で利用できる PaaS (Platform as a Service) として Heroku があります (Node.js だけでなく, PHP, Ruby, Python, Java, Scala, Go なども使えます) . 今回の SSR の実装には, Heroku を使うことにします.

この記事では, Heroku の使い方を紹介します (すでに, Node.js を使える環境をもっているのであれば, スルーしてください).

1. Create Heroku Account

まずは, アカウントを作成します.

Heroku
Heroku
Heroku Sign up
Heroku Sign up

2. Install Heroku CLI

Heroku をターミナルから操作できるように, Heroku CLI をインストールします.

3. Log in

ターミナルで以下のコマンドを実行します (Email アドレスとパスワードを入力する必要があるので, アカウント作成したときのものをそれぞれ入力してください).

$ heroku login
Enter your Heroku credentials:
Email: rilakkuma.san.xjapan@gmail.com
Password: **************
Logged in as rilakkuma.san.xjapan@gmail.com

Heroku に ssh 公開鍵をアップロードします.

$ ssh-keygen
$ heroku keys:add ~/.ssh/id_rsa.pub

4. git init

Heroku にアプリケーションをデプロイするにも, Git でファイルを管理する必要があります

$ git init

5. Create Procfile

Procfile というファイルを作成し, アプリケーションを起動するコマンドを記述します. 今回は以下のような記述で OK です

web: node app-server.js

6. Create Application

SSR をするための, アプリケーションを作成していきます.

$ npm init -y
$ npm install --save express

1つ注意点があり, 本来は, devDependencies に入るパッケージ (–save-dev でインストールするパッケージ) は, Heroku だとデプロイの対象とならないようなので, すべて, –save オプションでパッケージをインストールします.

app.js

'use strict';                                                                                                                             

const express = require('express');
const app       = express();
const path      = require('path');

const port = process.env.PORT || 8080;

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res, next) => {
    const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
</head>
<body>
    <section id="app">
        <h1>Hello SSR</h1>
    </section>
</body>
</html>`;

    res.status(200).send(html);
});

app.listen(port, () => {↲
    console.log(`Listening on port ${port} ...`);
});

ここで注意が必要なのは,

const port = process.env.PORT || 8080;

の部分です. ローカルで動作させる場合には, 8080 のような任意のポート番号で OK なのですが, Heroku でデプロイする場合には, 任意のポート番号を利用すると, 以下のようなエラーが発生するので, Heroku の環境変数に定義されているポート番号を利用するように process.env.PORT を指定します.

Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

ここまでで, 作成した

  • package.json
  • Procfile
  • app-server.js

をコミットしておきます.

7. heroku create

以下のコマンドを実行します

$ heroku create app-ssr
Creating ⬢ app-ssr... done
https://app-ssr.herokuapp.com/ | https://git.heroku.com/app-ssr.git

app-ssr はアプリケーション名で, 省略することも可能です (その場合, 任意のアプリケーション名がつきます).

また, 以下のコマンドであとから変更することも可能です.

$ heroku rename [アプリケーション名」

8. Deployment

最後の手順です. アプリケーションを Heroku でデプロイします. といっても, 難しいことはなく, GitHub に push するのと同じ要領で,

$ git push heroku master

push すれば, 自動的にデプロイが開始されます.

デプロイが完了したら,

https://app-ssr.herokuapp.com/

にアクセスします.

SSR by Heroku
SSR by Heroku

このように表示されていれば OK です. また, ディベロッパーツールの Network をみても document としてレスポンスが返ってきているのがわかります.

もし, 何らかのエラーが原因で表示されない場合には,

$ heroku logs

でログを調べて, デバッグしてみてくだだい.

また, heroku コマンドの一覧は,

$ heroku help

で調べることが可能です.

webpack 2 で postcss を使う

前回の記事で webpack 2 を利用した SPA の開発環境の構築を記載しましたが, webpack 2 で postcss を導入してみたいと思います.

1. Install postcss

npm install --save-dev css-loader postcss-loader postcss-easy-import extract-text-webpack-plugin

2. Create webpack.config.js

const webpack           = require('webpack');
const ExtrackTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: ['./src/main.js', './src/main.css'],
  output: { path: `${__dirname}/public`, filename: 'app.js' },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: ExtrackTextPlugin.extract({
          use: [
            'css-loader',
            'postcss-loader'
          ]
        })
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        postcss: [
          require('postcss-easy-import')({ glob: true })
        ]
      }
    }),
    new ExtrackTextPlugin('app.css')
  ],
  devtool: 'source-map',
  devServer: {
    contentBase: `${__dirname}/public`,
    port: 8080,
    inline: true,
    historyApiFallback: true
  }
};

赤字で表示している記述が, 1.x からの大きな変更点です. 1.x では, 以下のように記述していました (していたそうです …)

// ...
entry : {
  js: './src/main.js',
  css: './src/main.css'
},
//

webpack 2 で 1.x の記述をしてしまうと, 以下のようなエラーが発生してしまいます.

ERROR in chunk css [entry]
app.js
Conflict: Multiple assets emit to the same filename app.js

3. src/main.css

@charset "UTF-8";

@import "./styles/base.css";
@import "./components/**/*.css";

npm でインストールした postcss-easy-import プラグインで Sass のような, CSS のインポートが可能になります.

4. src/styles/base.css

:root {
    --base-color: #ff1493;
}

body {
    margin: 0;
    padding: 0;
    line-height: 1.5;
    font-family: Helvetica, Arial, sans-serif;
    color: var(--base-color);
}

こちらも同様に, Sass のような CSS 変数の利用が可能になります.

以上で, webpack 2 を利用したCSS のビルド環境も構築できました.
あとは,

$ npm run build

を実行すると, public/app.css が生成されます. また, JavaScript ファイルと同様, 変更して保存すると, ブラウザが自動でリロードされます.

参考

webpack 2 で SPA の開発環境構築

Build development environment for SPA using webpack 2

React を利用した SPA (Single Page Application) のための開発環境を, webpack 2 で構築していきます.

1. Install Node.js / npm

nodebrew を使うなどしてインストールします.

2. Create package.json

$ npm init -y

3. Install React, Babel and webpack

$ npm install --save react  react-dom
$ npm install --save-dev babel-core babel-loader babel-plugin-transform-class-properties babel-preset-es2015 babel-preset-react webpack webpack-dev-server

4. Create .babelrc

{
  "presets": ["es2015", "react"],
  "plugins": ["transform-class-properties"]
}

5. Create webpack.config.js

module.exports = {
  entry: { js: './src/main.js' },
  output: { path: `${__dirname}/public`, filename: 'app.js' },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      }
    ]
  },
  devtool: 'source-map',
  devServer: {
    contentBase: `${__dirname}/public`,
    port: 8080,
    inline: true,
    historyApiFallback: true
  }
};

赤字で表示しているキーが, webpack 2 で変更されました (1.x では, loaders と loader だったそうです …)

1 つ注意点として, output の path と devServer の contentBase のパスは絶対パスで記述する必要があります. 相対パスで記述してしまうと, 警告やエラー (Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
– configuration.output.path: The provided value “public” is not an absolute path!) が発生してしまいます.

それぞれの設定の詳細は他のドキュメントに任すこととして, とりあえずこれで開発環境構築のための webpack の記述ができました.

6. src/main.js

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>Hello SPA</h1>, document.getElementById('app'));

7. public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
     <title>Hello SPA</title>
     <link rel="stylesheet" href="/app.css" type="text/css" media="all" />
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript" src="/app.js"></script>
</body>
</html>

8. npm scripts

ビルドと開発サーバーの起動を簡単にできるように, package.json に npm scripts を定義します.

  //...
  "scripts": {
    "build": "webpack",
    "start:dev": "webpack-dev-server"
  },
  // ...

以上で, 最低限の開発環境の構築ができました.

$ npm run build

を実行すると, public/app.js が生成されます.

$ npm run start:dev

を実行すると, 開発サーバーが起動し, http://localhost:8080/ にアクセスするとページが表示されます.
また, src/main.js を変更して保存すると, ブラウザが自動でリロードされて, コンテンツが書き換わります (gulp の livereload) .