Hyperappでhead内のタグを書き換えるライブラリ、Hyperapp Helmetをリリースしました!
はじめに
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で助言を頂いたりして大変お世話になりました。ここにお礼を申し述べます。
よし、これで次はアプリに取りかかれるぞ〜