JavaScript
Node.js
reactjs
micro
ssr

Microを使ってReactのSSRをする

先日、『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_)までお願い致します。