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

コメントを残す