LoginSignup
4
2

More than 5 years have passed since last update.

Hyperappでhead内のタグを書き換えるライブラリ、Hyperapp Helmetをリリースしました!

Last updated at Posted at 2018-12-10

はじめに

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で助言を頂いたりして大変お世話になりました。ここにお礼を申し述べます。

よし、これで次はアプリに取りかかれるぞ〜

4
2
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
4
2