Riot.js Advent Calendar 2019 の19日目です。
SSR
スーパースペシャルレアだったり、ダブルスーパーレアではありません。
サーバーサイドレンダリングです。
使うかどうかは賛否両論ありますが、使えるようにはなっておいた方が良いので勉強します。
@riotjs/ssr
Riot.jsにもSSRの為のモジュールがあります。
@riotjs/ssr
インストールはnpm i -S @riotjs/ssr
情報量が少ない、というか全く見つからなかったので結構苦戦しました。
Readme.mdによると@riotjs/ssr
に用意されているのは、以下の3種類。
render
import MyComponent from './my-component.riot'
import render from '@riotjs/ssr'
const html = render('my-component', MyComponent, { some: 'initial props' })
レンダリングした結果(HTML)を返します。
fragments
import MyComponent from './my-component.riot'
import {fragments} from '@riotjs/ssr'
const {html, css} = fragments('my-component', MyComponent, { some: 'initial props' })
レンダリングした結果からHTMLとCSSを分離して取得します。
というか、実際はレンダリング結果をそのまま返している。(HTML側が減らされている)
export const fragments = curry(createRenderer)(frags => frags)
export default curry(createRenderer)(({html}) => html)
register
import register from '@riotjs/ssr/register'
import MyComponent from './my-component.riot' // It will fail
// from now on you can load `.riot` tags in node
const unregister = register()
import MyComponent from './my-component.riot' // it works!
// normally you will not need to call this function but if you want you can unhook the riot loader
unregister()
Node.jsでRiotファイルを扱えるようにします。
以下のようにすることで別の拡張子も対応可能。
register({ exts: ['.riot', '.tag'] })
piratesを使ってrequire
をジャックしています。
@riotjs/compiler
を使ってcompileした結果を@babel/core
のtransformに食わせているだけです。
https://github.com/riot/ssr/blob/master/src/register.js
使ってみる
今回はWeb Serverにはexpress
を使っています。
Riot.js v4 自分でRouterを作るで作ったサンプルと同じ様なことをSSRで実装します。
import { fragments } from '@riotjs/ssr';
import register from '@riotjs/ssr/register';
import express from 'express';
// 拡張子.riotを登録
register();
// Express Server
const app = express();
app.listen(4500);
// Router and SSR
const ssr = (file, obj) => {
try {
// Riotファイルをロード
const tag = require(`./${ file || 'home' }.riot`).default;
// レンダリング実施
return fragments(tag.name, tag, { "params": obj });
} catch (ex) {
// エラーが発生した場合はエラーページへ
return file !== 'notfound' ? ssr('notfound', ex) : {};
}
};
// favicon.icoは無視
app.get('/favicon.ico', (req, res) => res.status(404).send(''));
// 全てのURLにマッチ
app.get('/*', (req, res) => {
// 最初のパスはRiotファイル名とする
let [ file, ...params ] = req.params['0'].split('/');
// レンダリングを実施し、HTMLとCSSを取得
const { html = '', css = ''} = ssr(file, params);
// レンダリング結果を返す
res.send(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${ file || "Riot App" }</title>
<style>${ css }</style>
</head>
<body>
${ html }
</body>
</html>`);
});
<my-home>
<nav>
<ul>
<li><a href="/hello">Hello</a></li>
<li><a href="/goodbye">Goodbye</a></li>
<li><a href="/greeting/hola">Hola</a></li>
<li><a href="/greeting/adios">Adios</a></li>
<li><a href="/hoge">hoge</a></li>
<li><a href="/app">My APP</a></li>
<li><a href="https://qiita.com/advent-calendar/2019/riotjs">通常リンク</a></li>
</ul>
</nav>
</my-home>
<my-hello>
<p>Hello World!!</p>
</my-hello>
<my-goodbye>
<p>Goodbye World!!</p>
<style>
p { color: red; }
</style>
</my-goodbye>
<my-message>
<p>{ props.params[0].slice(0, 1).toUpperCase() }{ props.params[0].slice(1) } World!!</p>
</my-message>
<my-notfound>
<h3>Oops!!</h3>
<h4>{ props.params.message }</h4>
<pre>{ props.params.stack }</pre>
</my-notfound>
<my-app>
<p>{ state.message } World!!</p>
<input type="button" value="Hello" onclick="{ greeting }">
<input type="button" value="Goodbye" onclick="{ greeting }">
<input type="button" value="Hola" onclick="{ greeting }">
<input type="button" value="Adios" onclick="{ greeting }">
<script>
export default {
state : {
message : ""
},
onMounted(props, state) {
state.message = "The";
this.update();
},
greeting(e) {
this.state.message = e.target.value;
this.update();
},
}
</script>
</my-app>
サーバー側なのでWebpackは使っていません。
{
"name": "riotv4-ssr-sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon --ext js,riot --exec babel-node src/scripts/index.js"
},
"keywords": [],
"author": "KAJIKEN <kentaro@kajiken.jp> (http://kajiken.jp)",
"license": "MIT",
"dependencies": {
"@riotjs/compiler": "^4.5.4",
"@riotjs/ssr": "^4.1.1",
"express": "^4.17.1",
"riot": "^4.7.2"
},
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/node": "^7.7.4",
"@babel/preset-env": "^7.7.6",
"nodemon": "^2.0.2"
}
}
{
"presets": [
"@babel/preset-env"
]
}
動作確認とまとめ
サーバーサイドでレンダリングしている為、onMounted
やonBeforeMount
などマウント時に動くJavaScript以外は効果ありません。(消えます)
マウント時に動いたJavaScriptは実行した結果がHTMLに反映された状態でクライアントに返ります。HTMLに変化のないものは効果ありません。
その代わりサーバーサイドレンダリングのため、curl
でももちろん結果は取れます。(検索エンジンBOTに強い)
そして表示処理と転送サイズは小さいので、レガシー端末やモバイル端末には良いかもしれません。