カテゴリー別アーカイブ: React

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>

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) .