Riot.js v4 サーバーサイドレンダリング(SSR)

[Riot.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/riotjs) の19日目です。

## SSR
スーパースペシャルレアだったり、ダブルスーパーレアではありません。
サーバーサイドレンダリングです。

使うかどうかは賛否両論ありますが、使えるようにはなっておいた方が良いので勉強します。

## @riotjs/ssr
Riot.jsにもSSRの為のモジュールがあります。
[@riotjs/ssr](https://github.com/riot/ssr)
インストールは`npm i -S @riotjs/ssr`

情報量が少ない、というか全く見つからなかったので結構苦戦しました。
[Readme.md](https://github.com/riot/ssr/blob/master/README.md)によると`@riotjs/ssr`に用意されているのは、以下の３種類。

### render

```javascript
import MyComponent from './my-component.riot'
import render from '@riotjs/ssr'

const html = render('my-component', MyComponent, { some: 'initial props' })
```
レンダリングした結果(HTML)を返します。

### fragments

```javascript
import MyComponent from './my-component.riot'
import {fragments} from '@riotjs/ssr'

const {html, css} = fragments('my-component', MyComponent, { some: 'initial props' })
```
レンダリングした結果からHTMLとCSSを分離して取得します。
というか、実際はレンダリング結果をそのまま返している。(HTML側が減らされている)

```javascript:fragments
export const fragments = curry(createRenderer)(frags => frags)
```
```javascript:default
export default curry(createRenderer)(({html}) => html)
```

### register

```javascript
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ファイルを扱えるようにします。
以下のようにすることで別の拡張子も対応可能。

```javascript
register({ exts: ['.riot', '.tag'] })
```

[pirates](https://www.npmjs.com/package/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を作る](https://qiita.com/nekijak/items/52b5bb11651cc5334624)で作ったサンプルと同じ様なことをSSRで実装します。


```javascript:index.js
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>`);
});
```
```html:home.riot
<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>
```
```html:hello.riot
<my-hello>
  <p>Hello World!!</p>
</my-hello>
```
```html:goodbye.riot
<my-goodbye>
  <p>Goodbye World!!</p>
  <style>
    p { color: red; }
  </style>
</my-goodbye>
```
```html:greeting.riot
<my-message>
  <p>{ props.params[0].slice(0, 1).toUpperCase() }{ props.params[0].slice(1) } World!!</p>
</my-message>
```
```html:notfound.riot
<my-notfound>
  <h3>Oops!!</h3>
  <h4>{ props.params.message }</h4>
  <pre>{ props.params.stack }</pre>
</my-notfound>
```
```html:app.riot
<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は使っていません。

```json:package.json
{
  "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"
  }
}
```
```json:.babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}
```

## 動作確認とまとめ
![a_2019_12_18_221131(slt)(raw).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/b611f9f4-939f-d75e-dd96-c14132633e8f.gif)

サーバーサイドでレンダリングしている為、`onMounted`や`onBeforeMount`などマウント時に動くJavaScript以外は効果ありません。(消えます)
マウント時に動いたJavaScriptは実行した結果がHTMLに反映された状態でクライアントに返ります。HTMLに変化のないものは効果ありません。

その代わりサーバーサイドレンダリングのため、`curl`でももちろん結果は取れます。（検索エンジンBOTに強い）
![b_2019_12_18_222309(slt)(raw).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/071b7052-b85a-3233-1964-ca77b47f34f8.gif)

そして表示処理と転送サイズは小さいので、レガシー端末やモバイル端末には良いかもしれません。