Help us understand the problem. What is going on with this article?

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

shisama
Node.js Core Collaborator. 関西Node学園Organizer.
https://shisama.dev
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away