はじめに
Hyperapp Helmetという、Hyperappフレームワーク用のライブラリをリリースしました。
Helmetとは、React Helmetが有名/はしりですが、<head>タグの中を書き換えるコンポーネントのことを言います。
昨今の検索エンジンbotはJavaScriptを解釈しますので、SSRはOGP/Twitter Card対応のためにあると言っても過言ではないと思います。その時に、ページ内容に合わせた<head>内タグの書き換えが必要になりますが、Hyperappにはそれに対応するライブラリがありませんでした。
特長
Hyperapp Helmetは以下の特長を持ちます。
-
@hyperapp/render対応。
renderToString()
だけじゃなく、renderToStream()
でSSRする時も使えます。 - @hyperapp/router対応。JSDOMと併用することにより、サーバサイドでも、ルーティングに合った<head>内タグの書き換えができます。
- ネスト対応。親・子・孫……コンポーネントそれぞれで定義した<Helmet>が全て<head>内タグに反映されます。
- TypeScript対応。
インストール
$ npm install hyperapp hyperapp-helmet
or
$ yarn add hyperapp hyperapp-helmet
使い方
公開メソッドは2つしかないので、README.md読めばわかると思いますが、軽く紹介しておきます。
<Helmet>
任意のコンポーネントで使える。<Helmet>タグで囲まれた部分が<head>タグの中に挿入される。key
アトリビュートが必須。
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
import { App as A } from '../App'
import { Helmet } from 'hyperapp-helmet'
export const Counter = (): any => (state: A.State, actions: A.Actions) => (
<div key="counter">
<Helmet key="counter-helmet">
<title>Counter: {state.count}</title>
<meta name="description" content="Number Counting Page" />
</Helmet>
<h1>{state.count}</h1>
<button onclick={() => actions.down(1)}>-</button>
<button onclick={() => actions.up(1)}>+</button>
<div>
<Link to="/about">About</Link>
</div>
</div>
)
getHelmetNodes(view, state, actions) => VNode[]
アプリケーションのビューを探索し、該当の<head>内タグのVNodeの配列を返す。
import { h, View } from 'hyperapp'
import { App as A } from './App'
import { getHelmetNodes } from 'hyperapp-helmet'
const Fragment = ''
export const Html: View<A.State, A.Actions> = (
state: A.State,
actions: A.Actions
) => {
const helmetNodes = getHelmetNodes(A.view, state, actions)
return (
<Fragment>
<Fragment innerHTML="<!doctype html>" />
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/index.js" defer />
{helmetNodes}
</head>
<body>
<div id="app">{A.view}</div>
</body>
</html>
</Fragment>
)
}
こんだけ! サンプルプロジェクトを公開してあるので、動かして見てください。
開発
せっかくなので、小話をば。
経緯
- 小さくて速いウェブアプリが作りたいな〜
- Reactはエコシステムが大きいから開発が早いけど、サイズがデカいしな〜
- おっしゃ、Preact挑戦してみたろ
- この
preact-compat
っての入れるとReact用のライブラリが使えるが、サイズがデカくなるぞ? う〜ん…… - そういえばHyperappってのがあったな?
-
@hyperapp/router
でルーティングもできるし、@hyperapp/render
ってやつでSSRもできる。全部小さい! ええやんけ! - (素振りリポジトリを作ってみる)
- えっ、HyperappにHelmetないの? マ? 今時SSRはOGP/Twitter Card対応だから意味ないやんけ! う〜ん……
- せっかくだし、作ってみるか!
実装
そんな難しいことはしてないのですが、迷いながら実装したところがあるので、説明させてください。
具体的には2点あり、
1. <Helmet>コンポーネントが<template>タグを返し、それがレンダリングされる。
2. <head>内タグの更新のとき、一旦全削除して、全追加している。
まず、1についてですが、Reactコンポーネントと違って、Hyperappのライフサイクルメソッドは外挿なんですよね。なので、なんらかのノードを返す必要がありました。
最初は<div style={{ display: 'none' }}>{children}</div>
を返していたのですが、<div>の中に<title>とは入ってるのかっこ悪い……となって一度はchildren
を書き出さず、空の<div>だけ返してました。しかし、SSRをするに当たってノードを探索・解釈してみたんですが、実際にはマウントされないため、oncreate
ライフサイクルメソッドが実行されず、タグを格納するグローバル変数に入らないという問題が発生しました。
というわけで、またchildren
を書き出す実装に戻したんですが、上記の通り気味が悪いので、実際には表示されないエレメントは何かないかと探したところ、<template>が見つかりました。これなら<title>等があっても違和感が少ないと考え、これを使うという選択をしました。
現在の実装ではテンプレートとしては使ってないのですが、将来実際にテンプレートとして使って、<head>に流し込むというのもありうるのではないかなと思います。
次に、2についてですが、最初は実際に書き出したエレメントと架空のノードとの差分をとってパッチを当てる実装にしてたんですが、これだと、props/state変数をタグに使用したとき、同定が難しいという問題が発生しました。
ユーザーに逐一key
を設定してもらえば同定できるのですが、それは手間だろうということで、現在のように、<Helmet>毎に全削除&全追加という実装になってます。
おわりに
このライブラリを使うことにより、HyperappでSSRするに当たって、最後の障害がなくなったと思います。ぜひ、使ってやってください。
このライブラリを作るにあたり、@r7kamura 氏のkatatemaを参考にしました。氏にはTwitterで助言を頂いたりして大変お世話になりました。ここにお礼を申し述べます。
よし、これで次はアプリに取りかかれるぞ〜