LoginSignup
55

More than 5 years have passed since last update.

React SSR+WordPress REST APIをDocker Composeで試す

Posted at

概要

WordPress REST API Pluginを使ってWordPressをバックエンドのAPIサーバーとして使い、フロントに置いたnode.jsサーバーでReactのServer Side Rendering(SSR)を試すサンプルです。

リポジトリはここ
KeitaMoromizato/wordpress-react-ssr

WordPressはプラグインで拡張ができるように作られていますが、バージョンアップが面倒だったり、セキュリティがあれだったり、PHPが嫌いだったりと心配事が尽きません。

ならいっそ、WordPressを記事の入稿機能だけをもったバックエンドサーバーとして使って、Viewのレンダリングは別のサーバーを立てましょうというのが割と正しそうな気がします。ちょうどReactのサーバーサイドレンダリングを説明するサンプルも欲しかったことだし。

そんな構成も、Docker Composeを使えば簡単に試せます。もうちょっと簡単なDocker Composeのサンプルが欲しい方は、コチラの記事もどうぞ。

Dockerでnginx+node.jsのSPA構成を試す

ちなみにこのサンプルはDocker for Macで動作確認しています。

学べること

  • Reactサーバーサイドレンダリングのメリット
  • Docker Composeの使い方

構成

Docker Composeで、nginx、node.js、WordPress、MySQLの4つのコンテナを起動します。

フロント用JSファイルは、Webpackでビルドしたものをnginxから静的配信するため、wwwボリュームを2コンテナ間で共有しています。

docker.png

  • Docker 1.12.1
  • Docker Compose 1.8.0
  • node.js 6.9.1
  • React.js 15.3.2
  • nginx 1.11.5
  • WordPress 4.6
  • WP REST API 2.0

技術要素

WordPress

まあこの記事読んでる人にWordPressの説明なんて要らないですよね。

デファクトスタンダードなCMSながら、その知名度から攻撃されることもよくあります(adminのパスとかよくアクセスあるよね)。そんなWordPressですが、実はWP REST APIという、REST APIを提供するプラグインが用意されています。

このプラグインを導入して、WordPressはユーザーから見えない所に置きましょうというのがこの記事のコンセプトです。

React(Server Side Rendering)

フロントエンド界隈で注目を集めるReact.jsですが、今回はどちらかというとサーバーサイドで使います。いわゆるサーバーサイドレンダリングというやつです。

Reactコンポーネントは、renderToString()という関数でHTML文字列に変換できます。それをnode.jsサーバーから実行して、テンプレートエンジン代わりにReactを使いましょうということです。

後に解説しますが、Viewの管理が一元化されるのが非常に便利です。

Docker(Docker Compose)

この「Reactをレンダリングするnode.jsサーバー」+「APIを提供するWordPress」という構成を、Dockerで管理しています。

正確に言うと、複数のDockerコンテナをまとめて管理できるDocker Composeという機能を使っています。実際は、node.jsとWordPressのコンテナ以外にも、WordPressのデータを保存するMySQLコンテナ、静的ファイルを配信するnginxコンテナを使っているので、同時に4つのコンテナを管理している構成です。

動かす

コードは全てGitHubに公開しているので、簡単に手元で確認できます。
https://github.com/KeitaMoromizato/wordpress-react-ssr

リポジトリをCloneした後、ビルドしてdocker-compose upでコンテナ郡を起動します。

$ git clone https://github.com/KeitaMoromizato/wordpress-react-ssr
$ cd wordpress-react-ssr
$ sudo docker-compose build
$ sudo docker-compose up

まだ完璧なサンプルでないので、一度目のupではWordPressが起動しません。一度落として、もう一度sudo docker-compose upしましょう。

すると各コンテナが立ち上がるので、localhost:8081にアクセスしてWordPressの初期設定を行います。

言語やログイン情報は適当に打ち込んで、管理画面に行きましょう。そして[パーマリンク設定]から、パーマリンクを[投稿名]に変更します。この設定を変更しないと、REST APIが正しく動きません。

パーマリンク設定_‹_test_—_WordPress.png

そして、次に「WP REST API」を有効化します。

プラグイン_‹_test_—_WordPress.png

ここまででWordPressの初期設定は完了です。

動作イメージ

WordPress側の設定が完了したら、localhost:8080にアクセスしましょう。トップページが表示されるはずです。

このサンプルは、トップ画面と記事画面の2ページ構成です。ちなみに、デザインやCSSに関する突っ込みはNGとします。

トップ画面

カードをクリックすると記事ページに遷移します。

top.png

WordPressから、新しい記事を投稿すると、一覧にも追加されていることが確認できます。

Wordpress_React_SSR.png

記事画面

記事ページには、「お気に入りボタン」を付けました。nodeプロセスがデータを持っているだけなので、コンテナを落とすと消えてしまいますが、サーバーサイドレンダリングの確認には十分でしょう。

article.png

解説

コードは基本的にリポジトリにあるので、ここでは要点だけ解説します。

Dockerfile/docker-compose.yml

基本的には、Dockerでnginx+node.jsのSPA構成を試すの記事で書いたものを踏襲しています。mysql、wordpressのコンテナが追加されていますが、Docker Hubの説明を見ながら必要な環境変数を設定するだけです。

WordPressはプラグインを管理画面からインストールすることもできますが、今回はDockerを使ったインフラのコード化がテーマの一つなので、wordpress/pluginsディレクトリをプラグインのディレクトリにマウントする形をとっています。

docker-compose.yml
    volumes:
      - ./wordpress/plugins:/var/www/html/wp-content/plugins:ro

そして、もう一つのポイントはnode.jsコンテナに設定したrestart: alwaysです。nodeプロセスの永続化は、foreverなどのモジュールを使用することが多いですが、Dockerコンテナはフォアグラウンドで一つプロセスを立ち上げないといけないという制約があります。なので、forever startでプロセスをデーモン化することが不可能です。

foreverを使いつつ実現する方法もあるようですが、それよりもDockerの機能として用意されているrestartを使ったほうが正しそうな気がします。

docker-compose.yml
  front-app:
    restart: always

nginx設定

nginxの設定は特に難しいものはありません。静的ファイルは/www/appwwwボリュームがマウントされているので、そこにパスを通し、それ以外はnodeコンテナにフォワードしています。

default.conf
server {
  listen 8080;
  server_name localhost;

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://front-app:3000/;
  }

  location ~ /r/(.+\.(js|css|jpg|png))$ {
    alias /www/app/$1;
  }
}

node.jsアプリケーション

expressを使った簡易サーバーです。

流れとしては

  1. リクエストを受けたらWP REST APIを実行し、記事データを取得
  2. そのデータを元にReactのサーバーサイドレンダリング(renderToString())を行い、HTML Stringを生成
  3. そのHTML Stringをhandlebar.jsのテンプレートに埋め込み、レスポンスを返す

という感じです。

app.js
const template = handlebars.compile(fs.readFileSync(`${__dirname}/template.html`, 'utf-8'));

const WP_BASE_URL = `${process.env.WP_URL}/wp-json/wp/v2`;

/* Pages */
app.get('/', (req, res) => {
  request.get(`${WP_BASE_URL}/posts`).end((error, response) => {
    if (error) {
      return res.sendStatus(500);
    }

    const data = {
      articles: response.body,
    };

    res.send(template({
      body: renderToString(React.createElement(TopPage, data)),
      initialData: JSON.stringify(data),
    }));
  });
});

また、今回は記事のお気に入り機能を実装しているため、お気に入りの登録(POST)と削除(DELETE)のAPIを公開します。本来であればMySQLにデータを永続化したいところですが、面倒なのでnodeプロセスのメモリ上で簡易的に管理します(なので、コンテナを落とすとお気に入りのデータは飛びます)。

app.js
let favorites = [];

app.post('/api/[0-9]+/favorite', (req, res) => {
  const id = +req.url.match(/\/([0-9]+)/)[1];

  favorites.push(id);
  res.send(200);
});

app.delete('/api/[0-9]+/favorite', (req, res) => {
  const id = +req.url.match(/\/([0-9]+)/)[1];

  favorites = favorites.filter(f => f !== id);
  res.send(200);
});

Reactコンポーネント(サーバー/フロント共通)

そして、記事ページでつかうコンポーネントがこちらです。Viewを構成するJSXの他に、お気に入りボタンが押されたときのアクション(APIコール)を実装しています。

ここがReactのサーバーサイドレンダリングが真価を発揮するところです。

react/article.js
import React, { Component } from 'react';
import moment from 'moment';
import request from 'superagent';

export default class ArticlePage extends Component {

  constructor(props) {
    super(props);

    this.state = {
      favorited: props.favorited,
    };
  }

  onClickLike() {
    const method = this.state.favorited ? 'del' : 'post';

    request[method](`/api/${this.props.id}/favorite`).end((error) => {
      if (error) {
        console.error(error);
      }

      this.setState({
        favorited: !this.state.favorited,
      });
    });
  }

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

    return (
      <div id="article">
        <h1>{this.props.title.rendered}</h1>
        <button
          className={`like_btton${favorited ? ' on' : ''}`}
          onClick={() => this.onClickLike()}
        >{ favorited ? 'お気に入り' : 'お気に入りを解除'}</button>

        <p className="article_date">{moment(this.props.date).format('YYYY/MM/DD')}</p>
        <div className="article_body" dangerouslySetInnerHTML={{ __html: this.props.content.rendered }} />
      </div>
    );
  }
}

ここで実装したお気に入りボタンのように、サーバーでレンダリングした状態(DOM)から、フロントのJSで状態(DOM)を上書きするというケースは、Webアプリケーションではよく起こります。

通常のスタックの場合

  1. サーバーサイドのテンプレートエンジンで初期状態を判定し、Viewを構築
  2. ボタンのアクションに応じてフロントJSでDOMを書き換える

という風に、お気に入りボタンのON/OFFという一つのViewを表現するために、テンプレートエンジン・フロントと2箇所に似たような実装を書かなければなりません。テンプレートエンジンのコードは書きかえたけど、jQueryで書いたフロントのコードは変更を忘れていた、ということが容易に起こりえます。

React.jsなら、作成したコンポーネントをテンプレートエンジンとしても使えるので、このViewの2重管理問題から解放されます。

フロントのエントリポイント

フロントエンド用のエントリポイントでは、各ページのコンポーネントを読み込み、URLを判別して表示するコンポーネントを切り替えています。

react/app.js
const routes = [
  {
    path: /\/[0-9]+/,
    component: ArticlePage,
  },
  {
    path: /\//,
    component: TopPage,
  },
];

const path = window.location.pathname;
const Component = routes.reduce((memo, route) =>
  memo || (path.match(route.path) ? route.component : null), null);

render(<Component {...initialData} />, document.getElementById('app'));

以下の記事にもあるように、Reactでサーバーサイドレンダリングするときは、フロントに初期データ(ここでいうinitialData)を渡す必要があります。
JSONをサーバーとフロント側で安全に共有する

react/app.js
const initialData = JSON.parse(
  document.getElementById('initial-data').getAttribute('data-json')
) || {};

Webpack設定

フロント用のコードのビルドにはWebpackを使用しています。と入っても特殊なことはしておらず、基本的な構成になっています。

webpack.config.js
module.exports = {
  entry: {
    js: './scripts/react/app.js',
  },
  output: {
    path: `${__dirname}/out`,
    filename: 'bundle.js',
  },

一つポイントがあるとすると、Docker Compose用に構成を工夫しているという点です。ビルドはnodeコンテナで行い、その静的ファイルを配信するのはnginxコンテナです。

そのため、wwwというnamedボリュームを作成し、Webpackのoutディレクトリと、nginxの静的ファイル格納ディレクトリを共有しています。

docker-compose.yml
services:
  front-app:
    volumes:
      - www:/home/app/nodeapp/out
  nginx-proxy:
    volumes:
      - www:/www/app:ro
volumes:
  www:

プロダクションでは

まだ実環境でこの構成を試してないのですが、実際に本運用に回すなら注意したい点を挙げます。他にあるかな。

ユーザーデータとか

お気に入りボタンを簡易実装しましたが、実際はユーザー登録であったりその他諸々の処理が必要だと思われます。そういったものは、WordPressから分離して、マイクロサービス的に他の場所で管理するのが良さそうです。WordPresは、シンプルな記事入稿機能だけにとどめておくのが正しそう。

MySQLは分離する

今回は簡略化のためにMySQLもDockerコンテナで動かしていますが。もちろんそれではクラウドでスケーリングすることもできません。別途AWS RDSなどを使うと良さそうです。

nginxキャッシュ

どうやらReactのサーバーサイドレンダリングは結構重たいらしいです。適切にnginxでキャッシュを設定しましょう。

プロダクション用Docker Compose

プロダクション用のDocker Compose設定を別途作りたいですね。

  • MySQLコンテナを使わない(RDSにするなど)
  • Webpackのwatchを走らせない
  • Webpackのビルドを最適化

とか、ぱっと思いつく限りでも色々とありそうです。

Security Groupの設定

クラウド(AWS)で動かすとなるとこういう構成かなーと考えている。ユーザー向けにSecurityGroupdで80番ポートを解放して、管理者向けには別ポートを開けて直接WordPressにアクセスさせる。

そのWordPress用ポートには別のEC2インスタンスからのアクセスのみ許可して、そこでBasic認証なり何なりを行う。

こういう構成ならWordPressをユーザーから見えないところに配置できるので、セキュリティ的にもよろしい感じになるんじゃないかと。

docker.002.png

詰まっているところ

node_modulesが更新されない

ホストPCで新しいモジュールをnpm installして、docker-compose buildを再度走らせてdocker-compose upしても、「モジュールがありません」と怒られる。

ビルドしたのはいいけど、古いイメージが残っている?まだDockerがよく理解できていないようなので、だれか詳しい人教えてください。

まとめ

Reactの話なのか、WordPressの話なのか、Dockerの話なのかわからなくなってしまいましたが。まあ、それらの技術がどう使えるのか、全体のアーキテクチャから見てみるのも、良いと思います。

このような(ちょっと)複雑なインフラ構成も、簡単に共有できて試せるようになったのは、Dockerの大きな功績ですね。

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
55