#はじめに
この記事はReact#2 Advent Calendar 2019 10日目の記事です。
先日某勉強会で以下のようなLTをさせていただきました。
当記事ではこのLTを元に
・Universal JavaScriptについて
・LT内でも言及されているReact Routerを用いたSSRについて
深ぼっていこうと思います。
#なぜUniversal JavaScriptか
まずはじめに、少しReactAdventCalendarという趣旨から少しはずれるかもしれませんが、Universal JavaScriptという言葉について触れておこようと思います。
Universal JavaScriptとは、
・サーバーサイドやクライアントのコードをJavaScriptのコードで共通化させていこうという設計論
・Isomorphic JavaScriptやSSRと同義語として扱われることも多い。
SSR(サーバーサイドレンダリング)という言葉の方が皆さんにとっても馴染みが深いと思います(ていうか私もです。Universal JacaScriptという用語は最近知りました。)
このUniversal JavaScriptという用語は2015年、こちらのUniversal JavaScriptという記事で言及させたのがはじまりだと言われています。
当記事でも
Because good names are important. A good name teaches about purpose and responsibility, so you have to spend some time thinking about it.
と言われている通り、物事を伝えるためには言葉選びというのは大切です。
私も教育に携わっている身なので、そのことはよくわかります。
私は最近プログラミングをはじめて、SPA+SSRというような感じでサーバーサイドレンダリングはSEOのため、ページ表示を早くするためプラスアルファでやるものという認識でした。(私の勉強不足というのもあるかもしれませんが)
ただこの用語に出会ってからはSSRは実装方法によっては、JavaScriptのクライアント・サーバー(Node.js)どちらも実装することが可能であるという利点を生かした設計ができるのではという考えをもつことができました。
(これはSSRへの理解にも繋がると考えています。)
なので、あえてタイトルは現在ではあまり目にすることはないUniversal JavaScriptというタイトルを使わせていただきました(決して興味を持たせようという釣りではありません。)
#実装例
冒頭のLTの資料でも紹介させていただきましたが、こちらのUniversal JavaScriptの考えを理解するのに最適なのがNode.jsデザインパターンです.
こちらの例を少し紹介したいと思います。
環境
フロントエンド:React
サーバーサイド:Node(Express)
app.get('*', (req, res) => {
Router.match(
{routes: routesConfig, location: req.url},
(error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
let markup = ReactDom.renderToString(<Router.RouterContext {...renderProps} />);
res.render('index', {markup});
} else {
res.status(404).send('Not found')
}
}
);
Expressでのルーティングのコードです。
Router.matchとはフロントエンドのReact.Routerのルーティングとマッチさせます。つまりはExpressのルーティングにReactRouterのルーティングを合わせ、ReactDOM.renderToStringによって受け取ったReactDOMをHTMLにして、返すことによってSSRを実現しています。
このようにフロント/サーバーのコードを同じ考えのもと共通かしていくのがUniversal JavaScriptの考えです。
#現在のSSR
とはいえ、先ほどの例のコードは現在のReactRouterのバージョンに対応していないので、どのようなコードを書いて、実装していったら良いか考えてみましょう。
先ほどの例では、
{routes: routesConfig, location: req.url},
というようにフロントエンドのルーティングを一つのroutesConfigというモジュールにまとめることによって、ルーティングの共通化を実装しやすくしています。
これを実装するにはreact-routerの
react-router-config
が便利です。
こちらは、Routeを以下のようにまとめ、
const routes = [
{
component: Root,
routes: [
{
path: "/",
exact: true,
component: Home
},
{
path: "/child/:id",
component: Child,
routes: [
{
path: "/child/:id/grand-child",
component: GrandChild
}
]
}
]
}
];
このようにすることでルーティングを実装することができます。
import { BrowserRouter } from "react-routerdom";
import { renderRoutes } from "react-router-config";
import routes from "./client/routes";
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>;
以下のようにしてルーティングを実装します。サーバーサイドのルーティングにはStaticRouterを使います。
import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config"
import routes from "./client/routes"
http
.createServer((req, res) => {
const context = {};
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
{renderRoutes(routes)}
</StaticRouter>
);
if (context.url) {
res.writeHead(301, {
Location: context.url
});
res.end();
} else {
res.write(`
<!doctype html>
<div id="app">${html}</div>
`);
res.end();
}
})
.listen(3000);
※サンプルはReact Routerのドキュメントから引用
#まとめ
いかがでしょうか?
SSRという用語だとなかなか気が進まない作業でもUniversal JavaScriptだとなんかかっこいい感じがします(笑)
というのは冗談で、UniversalJavaScriptという考えを知った上でSSRに取り組むと理解が進むのではないでしょうか?という初心者目線の投稿でした。
読んでくださり、ありがとうございました。
なんかReact要素少なかったような。。。すいません。
#参考
ReactRouter
react-router-config
以下はIsomorphicJavaScriptの記事ですが、歴史がしれてとても興味深かったです。
(https://speakerdeck.com/koichik/isomorphic-survival-guide)