概要
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 for Macで動作確認しています。
学べること
- Reactサーバーサイドレンダリングのメリット
- Docker Composeの使い方
構成
Docker Composeで、nginx、node.js、WordPress、MySQLの4つのコンテナを起動します。
フロント用JSファイルは、Webpackでビルドしたものをnginxから静的配信するため、www
ボリュームを2コンテナ間で共有しています。
- 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が正しく動きません。
そして、次に「WP REST API」を有効化します。
ここまででWordPressの初期設定は完了です。
動作イメージ
WordPress側の設定が完了したら、localhost:8080
にアクセスしましょう。トップページが表示されるはずです。
このサンプルは、トップ画面と記事画面の2ページ構成です。ちなみに、デザインやCSSに関する突っ込みはNGとします。
トップ画面
カードをクリックすると記事ページに遷移します。
WordPressから、新しい記事を投稿すると、一覧にも追加されていることが確認できます。
記事画面
記事ページには、「お気に入りボタン」を付けました。nodeプロセスがデータを持っているだけなので、コンテナを落とすと消えてしまいますが、サーバーサイドレンダリングの確認には十分でしょう。
解説
コードは基本的にリポジトリにあるので、ここでは要点だけ解説します。
Dockerfile/docker-compose.yml
基本的には、Dockerでnginx+node.jsのSPA構成を試すの記事で書いたものを踏襲しています。mysql、wordpressのコンテナが追加されていますが、Docker Hubの説明を見ながら必要な環境変数を設定するだけです。
WordPressはプラグインを管理画面からインストールすることもできますが、今回はDockerを使ったインフラのコード化がテーマの一つなので、wordpress/plugins
ディレクトリをプラグインのディレクトリにマウントする形をとっています。
volumes:
- ./wordpress/plugins:/var/www/html/wp-content/plugins:ro
そして、もう一つのポイントはnode.jsコンテナに設定したrestart: always
です。nodeプロセスの永続化は、foreverなどのモジュールを使用することが多いですが、Dockerコンテナはフォアグラウンドで一つプロセスを立ち上げないといけないという制約があります。なので、forever start
でプロセスをデーモン化することが不可能です。
foreverを使いつつ実現する方法もあるようですが、それよりもDockerの機能として用意されているrestart
を使ったほうが正しそうな気がします。
front-app:
restart: always
nginx設定
nginxの設定は特に難しいものはありません。静的ファイルは/www/app
にwww
ボリュームがマウントされているので、そこにパスを通し、それ以外はnodeコンテナにフォワードしています。
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を使った簡易サーバーです。
流れとしては
- リクエストを受けたらWP REST APIを実行し、記事データを取得
- そのデータを元にReactのサーバーサイドレンダリング(
renderToString
())を行い、HTML Stringを生成 - そのHTML Stringを
handlebar.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プロセスのメモリ上で簡易的に管理します(なので、コンテナを落とすとお気に入りのデータは飛びます)。
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のサーバーサイドレンダリングが真価を発揮するところです。
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アプリケーションではよく起こります。
通常のスタックの場合
- サーバーサイドのテンプレートエンジンで初期状態を判定し、Viewを構築
- ボタンのアクションに応じてフロントJSでDOMを書き換える
という風に、**お気に入りボタンのON/OFFという一つのViewを表現するために、テンプレートエンジン・フロントと2箇所に似たような実装を書かなければなりません。**テンプレートエンジンのコードは書きかえたけど、jQueryで書いたフロントのコードは変更を忘れていた、ということが容易に起こりえます。
React.jsなら、作成したコンポーネントをテンプレートエンジンとしても使えるので、このViewの2重管理問題から解放されます。
フロントのエントリポイント
フロントエンド用のエントリポイントでは、各ページのコンポーネントを読み込み、URLを判別して表示するコンポーネントを切り替えています。
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をサーバーとフロント側で安全に共有する
const initialData = JSON.parse(
document.getElementById('initial-data').getAttribute('data-json')
) || {};
Webpack設定
フロント用のコードのビルドにはWebpack
を使用しています。と入っても特殊なことはしておらず、基本的な構成になっています。
module.exports = {
entry: {
js: './scripts/react/app.js',
},
output: {
path: `${__dirname}/out`,
filename: 'bundle.js',
},
一つポイントがあるとすると、Docker Compose用に構成を工夫しているという点です。ビルドはnodeコンテナで行い、その静的ファイルを配信するのはnginxコンテナです。
そのため、www
というnamedボリュームを作成し、Webpackのout
ディレクトリと、nginxの静的ファイル格納ディレクトリを共有しています。
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をユーザーから見えないところに配置できるので、セキュリティ的にもよろしい感じになるんじゃないかと。
詰まっているところ
node_modulesが更新されない
ホストPCで新しいモジュールをnpm install
して、docker-compose build
を再度走らせてdocker-compose up
しても、「モジュールがありません」と怒られる。
ビルドしたのはいいけど、古いイメージが残っている?まだDockerがよく理解できていないようなので、だれか詳しい人教えてください。
まとめ
Reactの話なのか、WordPressの話なのか、Dockerの話なのかわからなくなってしまいましたが。まあ、それらの技術がどう使えるのか、全体のアーキテクチャから見てみるのも、良いと思います。
このような(ちょっと)複雑なインフラ構成も、簡単に共有できて試せるようになったのは、Dockerの大きな功績ですね。