Edited at

Microを使ってReactのSSRをする

More than 1 year has passed since last update.

先日、『Node.js でちょっとしたサーバーサイドやるなら、 Micro が良いかも』を読みReactのSSRも簡単にできそうだなと感じたので試してみました。

以下の記事の完成したコードはshisama/micro-react-ssr-sampleに置いています。


Agenda


Micro

zeit/micro

Next.jsを作っているzeitが作ったNode.jsのフレームワークです。

Node.js でちょっとしたサーバーサイドやるなら、 Micro が良いかも』やzeitのブログ記事『Micro 8: Better for Production, Easier for Development』に詳しく載っています。


Setup

microとReactをインストールします。

microはproduction用、micro-devはdevelopment用です。

npm install --save micro

npm install --save-dev micro-dev

microとmicro-devを起動できるようにpackage.jsonに記載します。


package.json

"scripts": {

"start": "micro",
"dev": "micro-dev"
}

reactとreact-domもインストールします。babelなどは不要です。

npm install --save react

npm install --save react-dom


Hello, World!

Hello, Worldを画面に出すまでやってみます。

まず、Reactを使わずmicroのみでやってみます。


index.js

module.exports = async (req, res) => {

return 'Hello, World!'
}

npm startを実行し、 http://localhost:3000 にアクセスするとHello, World!が画面に表示されます。

また、npm run devでmicro-devを実行するとHot Reloadingしてくれます。

次にReactを使って表示してみます。

const React = require('react')

const { renderToString } = require('react-dom/server')

module.exports = async (req, res) => {
return renderToString(React.createElement('div', null, 'Hello, World!'));
}

たったこれだけです。簡単ですね。

次はもう少し実践的なことをしてみましょう。


実践編


Babel Setup

ここからはJSXを使っていくためbabelを使います。

npm install --save-dev babel-cli babel-preset-env babel-preset-react

.babelrcを作成します。

各々の設定の内容の意味はこちらをご参考にしてください。


.babelrc

{

"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
],
"react"
]
}


非同期で取得したデータを表示する

次に非同期で取得したデータを表示させてみます。

今回はQiitaのAPIを使用します。

Qiita API v2 documentation - Qiita:Developer

データをリスト表示するだけのコンポーネントを作成します。


List.jsx

import React from 'react';

import Item from './Item';

export default (props) => {
const list = props.items.map((item, index) => {
return <Item {...item} index={index} />
});
return (
<React.Fragment>
<ul>
{list}
</ul>
</React.Fragment>
);
};



Item.jsx

import React from 'react';

export default (props) => {
return (
<li key={props.index}>
{props.user.id}: <a href={props.url}>{props.title}</a>
</li>
);
}


次にサーバサイドでレンダリングするHTMLコンポーネントを作成します。


Template.jsx

import React from "React"

export default (props) => {
return (
<html>
<head>
<meta charset="UTF-8" />
<title>{props.title}</title>
</head>
<body>
<div id="app">{props.children}</div>
<script id="qiita-data" type="text/plain" data-json={props.data}></script>
<script src="./bundle.js" />
</body>
</html>
);
};


#qiita-data<script>タグはあとから出てくる取得したデータを格納しておくための要素です。

次にサーバサイドのエントリポイントを用意します。

axiosを使ってデータを取得します。

取得したデータは先程のHTML用のコンポーネントにJSON文字列として渡しています。

HTML用のコンポーネントは受けとったJSON文字列を#qiita-datadata-jsonというカスタム属性に格納しています。


server.js

import React from 'react';

import { renderToString } from 'react-dom/server';
import axios from 'axios';
import Template from './Template';
import List from './List';

export default async (req, res) => {
const apiRes = await axios.get('https://qiita.com/api/v2/items');
const data = apiRes.data;
return renderToString(
<Template title="Qiita API List" data={JSON.stringify(data)}>
<List items={data} />
</Template>
)
}


クライアントサイド用にコードを用意します。

サーバ側で取得して#qiita-dataに格納しておいたデータを取得して表示するようにしています。


client.js

import React from 'react';

import { hydrade } from 'react-dom';
import Template from './Template';
import List from './List';

const items = JSON.parse(document.querySelector('#app').getAttribute('data-json'));

hydrade(
<Template title="Qiita API List">
<List items={items} />
</Template>,
document.querySelector('#app')
);



Babelでサーバサイド用のコードをトランスパイル

これまでに作成したファイルをsrcディレクトリに格納します。

サーバサイド用にBabelでトランスパイルします。

package.jsonにいかを記述し、実行します。


package.json

"scripts": {

"build:server": "babel src -d dist"
}

npm run build:server


webpackでクライアント用のコードをバンドル

ブラウザで実行するためのクライアント用のコードをwebpackでbundleします


webpack.config.js

const path = require('path');

module.exports = {
mode: 'production',
entry: {
bundle: path.resolve(__dirname, 'src', 'client.js')
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
resolve: {
extensions: ['.js', '.jsx']
}
}



package.json

"scripts": {

"build:client": "webpack"
}

npm run build:client


Microでサーバ起動

最後にMicroでサーバを起動します。

本番実行時はmicroで実行

micro ${filepath}

開発時はmicro-devを実行するようにすると良いでしょう。

micro-dev ${filepath}


package.json



"scripts": {
"start": "micro dist/server.js",
"dev": "micro-dev dist/server.js",
"build:server": "babel src -d dist",
"build:client": "webpack"
}


production

npm start



develop

npm run dev


ここまできたら http://localhost:3000 を開くとQiita APIで取得したデータが一覧で表示されると思います。


Expressと比較

Expressで書いたサーバサイドのコードと比較してみます。

以下はExpressで実行するコードですが、Microの方がわずかにコード量は少なくシンプルではないでしょうか。

import React from 'react';

import { renderToString } from 'react-dom/server';
import axios from 'axios';
import Template from './Template';
import List from './List';
import express from 'express';

const app = express();

app.get('/', async(req, res) => {
const apiRes = await axios.get('https://qiita.com/api/v2/items');
const data = apiRes.data;
renderToString(
<Template title={title} data={JSON.stringify(data)}>
<List items={data} />
</Template>
).pipe(res);
});

app.listen(3000);


所感

Microはまだ出てきたばかりで知見がない分、現時点ではExpressを選ぶ方がいいかもしれません。

しかし、micro-devはHot Reloadingをしてくれるし、ログ表示もわかりやすいです。

Next.jsやNowを作ったZEIT製なので期待はできます。

知見がインターネットに転がり始めたら使ってもいいのではないでしょうか。

そのためにもMicroを触ってみて得た知見をアウトプットしていきましょう。


参考

最後までお読み頂きありがとうございました。

不備や質問があれば、コメント欄かTwitter(@shisama_)までお願い致します。