LoginSignup
6
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

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側が減らされている)

fragments
export const fragments = curry(createRenderer)(frags => frags)
default
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で実装します。

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

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

動作確認とまとめ

a_2019_12_18_221131(slt)(raw).gif

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

その代わりサーバーサイドレンダリングのため、curlでももちろん結果は取れます。(検索エンジンBOTに強い)
b_2019_12_18_222309(slt)(raw).gif

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3