複雑なツールを使いこなして優れたWebアプリケーションを構築するのは簡単ではありません。
HyperAppはシンプルさと機能の両立を目的に生まれたJavaScriptライブラリーです。ほかのフレームワークと同水準の機能を維持しながら、理解が必要な概念の数を極限まで削減しました。ReactやPreact、Mithrilと比べると、APIがコンパクトで、ステート管理が標準機能にあり、バンドルサイズが圧倒的に小さいところが特徴です。
HyperAppの概要、サンプルを使ったコードを解説します。ほかのフレームワークの使用経験は不要ですが、HTMLとJavaScriptの基礎知識があることが前提です。
HyperAppとは?
HyperAppはインタラクティブなWebアプリケーションを構築するツールで、一方向のデータフローやJSX、バーチャルDOMなど、Reactで有名になった多くの概念を踏襲しています。Elmアーキテクチャーに基づいており、アプリケーション設計はElmやReact、Reduxと似ています。
import { h, app } from "hyperapp"
app({
state: "Hi.",
view: state => <h1>{state}</h1>
})
コードはJSXを使っていますが、HyperAppにJSXは必須ではありません。
HyperxとES6のテンプレートリテラルで書き直したコードです。
import { h, app } from "hyperapp"
import hyperx from "hyperx"
const html = hyperx(h)
app({
state: "Hi.",
view: state => html`<h1>${state}</h1>`
})
外部ライブラリーを使わずに書いたコードです。
import { h, app } from "hyperapp"
app({
state: "Hi.",
view: state => h("h1", null, state)
})
コンセプト
バーチャルノード
HyperAppは、バーチャルノードを作成するためにh関数を使います。h関数はHyperScriptのシグネチャに従って記述します。
h("div", { id: "app" }, [
h("h1", null, "Hi.")
])
バーチャルノードはHTMLやDOMツリーを記述するJavaScriptのオブジェクトです。先ほどのサンプルでは、次のオブジェクトが生成されます。
{
tag: "div",
data: {
id: "app"
},
children: [{
tag: "h1",
data: null,
children: ["Hi."]
}]
}
JSXは動的なHTMLを表現するJavaScriptの言語拡張です。ES6のテンプレートリテラルに準拠したHyperxの記法も使えます。どちらも同じh関数が呼び出されます。
ステート、ビュー、アクション
「Hello World」ではおもしろくないので、HyperAppアプリケーションに共通して含まれる要素を盛り込んだインタラクティブなサンプルを作りました。
app({
state: 0,
view: (state, actions) => (
<main>
<h1>{state}</h1>
<button onclick={actions.add}>+</button>
<button onclick={actions.sub}>-</button>
</main>
),
actions: {
add: state => state + 1,
sub: state => state - 1
}
})
stateはアプリケーションにおけるデータモデル全体を表現します。初期値は数字の0です。
state: 0
app関数を実行すると、stateがviewに渡され、<h1>タグに値を表示します。
<h1>{state}</h1>
viewの中にonclickハンドラーを持つボタンが2つあります。ハンドラーは、2つ目の引数としてviewに渡されるactionsです。
<button onclick={actions.add}>+</button>
<button onclick={actions.sub}>-</button>
actions.addやactions.subは直接ステートを更新せずに、新たなステートを返します。
add: state => state + 1,
sub: state => state - 1
actionsを呼び出してstateを更新すると、view関数が呼び出されてアプリケーションがレンダリングされます。
アーキテクチャー
ユーザーはアプリケーションを操作してactionsを呼び出します。actionsがstateを更新する唯一の手段です。裏側ではHyperAppがコードにactionsを挿入して、アプリケーションをレンダリングするタイミングを指示します。
副作用のないactionsはreducerとも呼ばれます。Reduxの経験者なら分かると思いますが、reducerは純粋関数で、アプリケーションの現在のステートとなんらかのデータを受け取り、新しいステートを返します。
それぞれのリンクがステート構成を表すチェーンを視覚化します。チェーン内の最新のリンクが現在のステートです。view関数は現在のstateとactionsを受け取り、DOMツリーを構築します。actionsが呼び出されると、チェーンの中に新しいリンクが作られます。
副作用
アクションの中から複数のactionsを呼び出します。fetchのような非同期の関数に渡されたコールバックでもアクションを呼び出します。
actionsがnullかundefinedを返すと、HyperAppはstateを更新する必要がないと判断し、レンダリングのパイプラインを飛ばします。actionsがPromiseを返してもHyperAppはstateを更新しません。コールバックに従いほかのactionsを呼び出すなどの処理をします。
サンプル
GIF検索ボックス
Giphy APIでGIF検索ボックスを作成します。ステートを非同期で更新する方法をサンプルで説明します。
stateにはGIF URLの文字列とboolean型のフラグが格納されています。このフラグは、ブラウザーが新しいGIFを取得中か管理します。
state: {
url: "",
isFetching: false
}
isFetchingフラグで、ブラウザー動作中にGIFを非表示にします。設定しないと、ダウンロードを完了する前にダウンロード済みのGIFから順に表示されます。
style={{
display: state.isFetching ? "none" : "block"
}}
viewにはテキストボックスとGIFを表示するimg要素があります。
onkeyupイベントでユーザーからの入力を処理していますが、onkeydownやoninputも使用できます。
キーストロークごとactions.searchを呼び出して、新しいGIFをリクエストします。fetch中でなく、テキストボックスが空でない場合のみ有効です。
if (state.isFetching || text === "") {
return { url: "" }
}
searchでは、fetch APIを使ってGiphyからGIF URLをリクエストします。
fetchが終わると、promiseでGIF情報を含む結果を受け取ります。
fetch(
`//api.giphy.com/v1/gifs/search?q=${text}&api_key=${GIPHY_API_KEY}`
)
.then(data => data.json())
.then(({ data }) => {
actions.toggleFetching()
data[0] && actions.setUrl(data[0].images.original.url)
})
データを受け取ると、actions.toggleFetchingが呼ばれて(追加のfetchリクエストを出す)、fetchしたGIF URLをactions.setUrlに渡し、ステートを更新します。
TweetBoxクローン
最後のサンプルはシンプルなTweetboxクローンです。
stateには、メッセージのテキストと、MAX_LENGTHに初期化した残り文字数countを格納します。
state: {
text: "",
count: MAX_LENGTH
}
viewにはTweetBoxコンポーネントを配置します。propsと呼ばれる属性を使って、ウィジェットにデータを渡します。
<TweetBox
text={state.text}
count={state.count}
update={e => actions.update(e.target.value)}
/>
ユーザーが入力すると、actions.update()を呼び出して、テキストを更新し、残り文字数を計算します。
update: (state, actions, text) => ({
text,
count: state.count + state.text.length - text.length
})
前回の文字数から現在の文字数を引いて、残り文字数の変化量を取得します。新しい残り文字数は直前の値に変化量を足したものです。
入力が空白なら、この操作の結果は(MAX_LENGTH - text.length)と同じです。
state.countが0未満なら、state.textがMAX_LENGTHより長いので、Tweetボタンを無効化して、OverflowWidgetコンポーネントを表示します。
<button
onclick={() => alert(text)}
disabled={count >= MAX_LENGTH || count < 0}
>
Tweet
</button>
文字が入力されていない、state.count === MAX_LENGTHのときも、Tweetボタンを無効化します。
OverflowWidgetタグには、あふれたメッセージに説明を加えて表示します。state.textから追加で削除する文字数を定数のOFFSETで規定します。
<OverflowWidget
text={text.slice(count - OFFSET)}
offset={OFFSET}
count={count}
/>
OFFSETをOverflowWidgetに渡してtextを削除し、overflow-textクラスをあふれ出た部分に適用します。
<span class="overflow-text">
{text.slice(count)}
</span>
新しいプロジェクトを開始する
CDN経由でHyperAppを使う方法が簡単です。
<script src="https://unpkg.com/hyperapp"></script>
現実的なアプローチは、ビルドパイプラインをnpm・Yarnで設定し、アプリケーションにHyperAppをバンドルする方法です。ビルドパイプラインは次のものを含みます。
- npmやYarnなどのパッケージマネージャー。サードパーティのパッケージを共有、再利用しやすくなる
- BabelやBubléなどのコンパイラー。新しいバージョンのJavaScriptを古いブラウザーでも動作するコードに変換する
- WebpackやRollup、Browserifyなどのバンドラー。モジュールと依存オブジェクトをブラウザーへ転送できる単一のバンドルにする
バンドラーを使ってパイプラインを設定する方法は、オフィシャルドキュメントに書かれています。やり方が分かっていれば、次のボイラープレートを参照してください。
HyperApp対React
概念的なレベルで、HyperAppとReactには多くの共通点があります。どちらのライブラリーもバーチャルDOMやライフサイクルイベント、キーベースのリコンシリエーションを使っています。
Reactはステートの関数としてビューを広めました。HyperAppはアイデアを進化して、Elmアーキテクチャーと同じ考え方に基づくカスタムステート管理手法を導入しました。
Reactにはステートフルなコンポーネントがあり、HyperAppにはステートレスなコンポーネントがあります。
ステートレスなコンポーネントは、コーポネント自身に関する情報以外、アプリケーションに関する情報を持っていないため、副作用がなく、再利用しやすい上に、テストやデバッグがシンプルです。
多くの点でReactはHyperAppより低水準です。ルートやXHR、ステート管理などをサードバーティーのライブラリーに依存しています。
最後に
HyperAppはElmアーキテクチャーにならったステート管理手法を標準で備えており、ReactやReduxに類似してますが、ボイラープレートはありません。
HyperAppのサイズは1KBです。転送やパースが速いという特徴があります。HyperAppはバーチャルDOMと実際のDOMの差分を取得して、実際のイベントハンドラ―を使い、サードパーティーライブラリーと簡単に統合できます。
HyperAppについて詳しくはオフィシャルドキュメントを読んでください。またアップデートやニュースをTwitterで発信しています。
(原文:HyperApp: The 1 KB JavaScript Library for Building Front-End Apps)
[翻訳:内藤 夏樹/編集:Livit]