この記事はWanoグループ Advent Calendar 2017 の10日目のメンズの代打になります。
そもそもこういうのでSSRするかって意見など諸説あるわけですが、特にネタもないので最近やったのを書きます。
やっていきます。
サーバー(lambda)側ファイル構成
.
├── config
│ └── webpack.config.js
├── dest-server
│ └── server.js
├── extra
│ └── ServerApp.js
├── package.json
├── serverless.yml
├── src
│ └── server.tsx
├── static
│ └── js
│ └── ClientApp.js
├── tsconfig.json
├── views
│ └── index.mustache
└── yarn.lock
serverless frameworkは devDependenciesなnode_moduleをデプロイ時に避けてくれますが、それでも無駄にlambdaの同梱ファイルが大きくなるのも良くないので、Reactアプリ本体は別リポジトリで切りつつ、ClientAppとServerAppとしてserverlessデプロイ用のリポジトリに吐いています。
serverless定義
apiとかlambdaの定義です。
あんましコードがでかくならなければ serverless-httpでエンドポイント1つにまとめちゃっていいかなーと思います。
こちらの記事に感謝。
そもそもSSRするってのは割とCDNのキャッシュ前提みたいなとこが多分にあると思っているわけなので、初回レンダリングはあんまりナーバスになりすぎない方向で考えます。
service: serverless-express-sample
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
plugins:
- serverless-offline
functions:
app:
handler: dest-server/server.main
memorySize: 256
description: "レンダリングはここ"
events:
- http:
method: ANY
path: '/'
- http:
method: ANY
path: '{proxy+}'
server.tsx
サーバー側の実装、server.tsxです。
webpack上はこいつをcommonjsモジュールとして吐きます。
テンプレートは好きなの使います。
styled-componentsのSSR対応もここでやっています。
2年近くcss-modulesを使ってましたが、ゆるふわ個人プロジェクトだと後からコンポーネントを切り出したりなんだりが割とダルいのと、styled-componentsの方が開発中に随時スタイルを細かい単位のコンポーネントで切る癖がちゃんとつくので、割とお気に入りです。
Prefetchが必要なステート管理機構の初期データ取得もここでやってしまいます。
const appState = await serverApp.getInitialAppState(req.url);
の部分です。
Reduxでもmobxでもこの辺は似た構成になるのかな、と思っています。
ネイティブなんかでも似たような初期化処理の分離はしますよね。
'use strict';
import * as React from "react";
const serverless = require('serverless-http');
const express = require('express');
import { ServerStyleSheet } from 'styled-components';
import { renderToString } from "react-dom/server";
const app = express();
const mustacheExpress = require('mustache-express');
app.engine('mustache', mustacheExpress());
app.set('view engine', 'mustache');
app.set('views', './views');
app.get('/', async (req, res , next) => {
return await renderApp(req , res , next);
});
app.get('/page/*', async (req, res , next) => {
return await renderApp(req , res , next);
});
if (process.env.NODE_ENV == "development"){
app.use('/static', express.static('./static'));
}
const renderApp = async (req, res , next)=>{
const serverApp = require("../extra/ServerApp");
const sheet = new ServerStyleSheet();
const appState = await serverApp.getInitialAppState(req.url);
const app = sheet.collectStyles(serverApp.renderServerApp(req.url , appState));
const appString = renderToString(app);
const styleTags = sheet.getStyleTags();
res.render('index' , {
appState : JSON.stringify(appState),
isProduction : process.env.NODE_ENV == "production",
styled : styleTags,
appString : appString,
} , (err , html )=>{
if (err){
res.error('なんかエラー返す')
}
res.send(html)
});
}
export const main = serverless(app);
index.mustache
テンプレートはこれ1枚な感じです。
ClientAppでは、 #initial-data
からデータを復元し、ステート管理機構に反映させます。
reduxならpureなjsonをそのまま当てればいいですが、mobxなどであればclass-transformerが便利かもしれません。
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>テストサイト</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.development.js"></script>
{{{ styled }}}
</head>
<body>
<script id="initial-data" data-value='{{{ appState }}}' />
<div id="app-root">{{{ appString }}}</div>
<div id="modal-root"></div>
<script src="/static/js/ClientApp.js" async></script>
</body>
</html>
cloudfront
最後に、レンダリングされた結果のキャッシュ機構を考えますが、AWSを使っているので、普通にこのままcloudfrontを使います。
API GatewayにもAPIキャッシュ機構はあるわけですが、キャッシュの取り回しが悪いのと、なぜか妙に料金が高いので使いません。
普通にドメイン当てたcloudfrontのパスの全てを、上記でデプロイしたエンドポイントに向けた方が何かと都合がいいかと思います。
(もちろんこちらであれば、特定のAPIはキャッシュしない、などの設定もできます。)
S3の静的webサイトホスティングもそうですが、単純にcloudfrontでなんとかなる場面の方が多かったりします。
社内用メンバー制サイトであればSSRは要りませんが、ログイン処理とか隠したいコンテンツの配信とか色々楽なのでAPI Gatewayでhtml返すのは割とアリかなーと思っています。ttl0にすればOK。
これで公開までの雛形は一通り揃うかな、と思います。