Hyperapp V2がリリースされました。
2019/03/27 追記:alpha.10に合わせて全面的に書き換えました。(おそらく次のアップデートでイベント名が小文字になる予定です。)
2019/09/14 追記:v2.0.1に合わせて書き換え。
2020/07/15 追記:v2.0.5がリリースされました。ESモジュール化、JSXの直接的なサポート・Payload Creatorが削除、Lazy
がmemo
にリネームされています。(本記事に未反映)
Hyperappとは
VDOMを搭載した超絶小さい(ソースが読める)JavaScriptフレームワークです。(2KBぐらい)
Hyperapp V2
リファレンス
- V2のREADME https://github.com/jorgebucaran/hyperapp/blob/master/README.md
- V2マイルストーン https://github.com/jorgebucaran/hyperapp/milestone/7
- V2のプルリク https://github.com/jorgebucaran/hyperapp/pull/726
- Action API https://github.com/jorgebucaran/hyperapp/issues/749
- Effects API https://github.com/jorgebucaran/hyperapp/issues/750
- Subscriptions API https://github.com/jorgebucaran/hyperapp/issues/752
- class/className attribute API https://github.com/jorgebucaran/hyperapp/issues/754
- Lazy Views https://github.com/jorgebucaran/hyperapp/pull/777
- Middleware API https://github.com/jorgebucaran/hyperapp/issues/753
V2概略図
- Actions
- Stateの更新
- Effectの作成
- Effects
- 非同期処理 - HTTP要求とか
- 副作用 - URLの変更(history API)とか
- Subscriptions
- グローバルイベントの管理 - Windowイベントとか
- カスタムイベントストリームの管理 - Timer(setInterval)とか
V1からの変更点
- State - Action - View でセットだったV1からActionが分離されました(Actionは純粋な関数に変更)
- ActionからActionを呼び出せなくなりました
- Actionが分離されたためNested Actionが消滅しました
- パラメーター付きのActionを呼び出すときはタプルを使います
- 非同期Actionの代わりにEffectが追加されました
- イベントの購読とその解除のためのSubscriptionが追加されました
- ライフサイクルイベントが削除されました(対応策)
- SVGのxlink属性のサポートが削除されました(詳細)
- DOM更新アルゴリズムが変更され高速化しました https://github.com/jorgebucaran/hyperapp/issues/499#issuecomment-403257843
-
renderを実行するタイミングが変更され、待ち時間(setTimeout)なしで実行することで高速化しました- 問題があったためrequestAnimationFrameを使用するように変更されました
動くサンプルアプリ
数値をカウントするだけのアプリです。JSXで書かれています。
インストール
npm i hyperapp
JSXを使用する場合はbabelのtransform-react-jsxでpragmaにhを指定します。
公開している関数
Hyperappは3つの関数を公開(export)しています。
-
h
関数- VDOMのノード(VNode)を作成する関数
- JSXを使用して間接的に利用します(直接使ってもOK)
-
app
関数- Hyperappアプリのエントリポイント
- 詳細は後述
-
Lazy
関数- 後述のLazy Viewで使用するVNodeを作成する関数
Actions
Actionは新しいStateとEffectの作成を行う関数です。次のように書きます。
// Stateのvalueに1を設定するAction
const Foo = state => ({ ...state, value: 1 });
// Payload付きAction
const Foo = (state, payload) => ({ ...state, value: payload.value });
注意:引数のStateを変更してはいけません。Stateを更新する場合は、常に新しいStateを作成する必要があります。
PayloadはどこからActionが呼び出されるのかによって中身が決まります(後述のdispatch
関数を参照)。VDOMのイベントから呼び出される場合はEventが設定されます。
Stateを更新したくない場合は引数のStateをそのまま返せばよいでしょう。
const Foo = state => state;
Actionの戻り値は上記のようにStateを返すほか、後述のEffectを返すパターンもあります。
const Foo = state => [ nextState, nextEffect ];
Effectは続けて複数指定できます。
const Foo = state => [ nextState, nextEffect1, nextEffect2, ... ];
Effects
Effectは非同期処理や副作用を扱います。簡単に言うと、Stateを変更しない動作を行います。
// 指定した時間後に指定したActionを呼び出すEffect Runner
const delayRunner = (dispatch, { action, interval }) => {
setTimeout(() => dispatch(action, 'delay!'), interval);
};
// delayRunnerを実行するEffectを作成するEffect Constructor
const delay = (action, { interval }) => [
delayRunner,
{ action, interval }
];
// delayから呼び出されるAction (最終的にtextへ'delay!'が設定される)
const Delayed = (state, payload) => ({ ...state, text: payload });
// Effectのdelayを呼び出すAction (1000ms後にDelaydを呼び出す)
const DelayWithAction = state => [
state,
delay(Delayed, { interval: 1000 })
];
delay
がEffectを作成する関数(Effect Constructorと呼ばれる)です。
Effectは関数とパラメーターのタプル(配列)であり、関数はEffect Runnerと呼ばれています。
const effect = [effectRunner, parameters]
Effect Runnerでは、第1引数にdelay
で作成したパラメーターがそのまま入り、第2引数にActionを実行するdispatch
関数が入ります。
dispatch
関数
**dispatch
**関数は、Actionを実行し、作成されたStateの更新とEffectの実行を行う関数です。おもに第1引数はAction、第2引数はActionパラメーターを渡します。
// ActionとPayload
dispatch(action, payload);
// タプルなPayload付きAction
dispatch([action, payload]);
// Payload Creator付きAction(Effect/Subscription内部やVDOMのイベントで使用)
dispatch([action, payloadCreator], payloadCreatorParameter);
// StateとEffect(Actionの戻り値と同様)
dispatch([newState, newEffect]);
// State(Actionの戻り値と同様)
dispatch(newState);
Actionを
dispatch
する理由は、ロギングなどで関数名を取得するためです。Actionを自分で呼び出してしまうとdispatch
内部においてfunction.name
で関数名(Actionの名前)を取得できなくなります。将来、Hyperappのデバッグツールが作られるときに必要になります。
app
関数
app
関数でHyperappのアプリを開始します。
app({
init: { value: 0 },
view: state => <div>{state.value}</div>,
subscriptions: state => ...,
node: document.body,
middleware: ...,
});
init
プロパティ
**init
**プロパティには初期化に使うオブジェクトを指定します。
**init
プロパティに指定されたオブジェクトはそのままdispatch
関数に渡されます。そのため、前述のdispatch
**関数で使用できるActionやEffectも使用できます。
init: { foo: 'bar' },
init: InitAction,
init: [InitAction, { foo: 'bar' }],
init: [{ foo: 'bar' }, delay(...)],
node
プロパティ
**node
**プロパティにはレンダリング先の要素(Element)を指定します。
view
プロパティ
**view
**プロパティには、VNodeを作成する関数を指定します。引数にStateが渡されます。ボタンのクリックでActionを発生させる場合は次のように書きます。
app({
view: state => <button onClick={action}>{state.value}</button>
});
JSXを使わずに書くと次のようになります。
app({
view: state => h("button", { onClick: action }, state.value)
});
小文字のonから始まる属性は、dispatch
関数の呼び出しに変換され、addEventListener
で登録されます。(内部で小文字に正規化されます)
また、dispatch
されるときデータにEventオブジェクトが設定されます。onClick
はClickイベントにevent => dispatch(action, event)
が登録されます。
イベント属性に指定した値はdispatch
に渡されるため、次のような書き方も可能です。
onClick={[action, payload]}
onClick={[action, payloadCreator]} // 後述
onClick={[newState, newEffect]}
onClick={newState}
Payload Creator
ActionのPayloadを作成する関数をPayload Creatorと呼びます。VDOMイベントに次のように指定すると、第1引数にEventが渡されます。
onClick={[action, ev => ({ value: ev.currentTarget.value })]}
Payload Creatorの戻り値が、指定したActionのPayloadになります。上記の場合、Payloadは{ value }
です。
preventDefault()
をここで呼び出すこともできます。
onSubmit={[action, ev => ev.preventDefault()]}
Payloadはundefined
(preventDefault()
の戻り値)になるので、Payloadを指定したい場合は、次のように書きます。
onSubmit={[action, ev => { ev.preventDafault(); return { value: 1 } }]}
// もしくはヘルパー関数を作って...
const preventDefault = (action, payload) => [action, ev => { ev.preventDefault(); return payload }]
onSubmit={preventDefault(action, { value: 1 })}
key
VNodeに**key
**プロパティを指定すると、更新時に同じDOMノードが使いまわされます。
h('div', { key: 'foo' });
<div key="foo" />
class
class
プロパティでclasscatライクにclassNameを記述することができます。
// <div class="foo" />
<div class={{ foo: true, bar: false }} />
<div class={['foo', { bar: false }]} />
<div class={'foo'} />
style
**style
**プロパティでインラインスタイルを記述することができます。
// <div style="border: solid 1px red" />
<div style={{ border: 'solid 1px red' }} />
subscriptions
プロパティ
**subscriptions
**プロパティには、Subscriptionオブジェクトの配列を返す関数を指定します。第一引数にStateが渡されます。省略可能。
この関数はStateが変更されるたびに呼び出され、Effectに似たSubscriptionオブジェクトを返します。Effectとの唯一の違いは、Effect Runnerで「購読を解除する関数」を返すところです。
// 指定した時間間隔で指定したActionを定期的に呼び出すEffect Runner
const tickRunner = (dispatch, { action, interval }) => {
const id = setInterval(() => dispatch(action), interval);
return () => clearInterval(id); // 購読解除用関数
};
// tickRunnerを実行するSubscriptionを作成するSubscription Constructor
const tick = (action, { interval }) => [
tickRunner,
{ action, interval }
];
// tickで呼び出されるAction
const action = state => ({ ...state, value: state.value + 1 });
app({
// valueが1のときにtickを購読
subscriptions: state => [
state.enabled && tick(action, { interval: 1000 })
],
});
上記の例ではStateのenabled
がtrue
のときtick
を購読します。enabled
がfalse
になるとtick
が購読解除されます。
注意:Subscriptionを定義する場合、effect
プロパティに指定するEffect Runnerは別変数に格納しておきましょう。匿名関数を使用すると正しく購読が継続されません。effect
の値が別オブジェクトと認識され、Stateが変化するたびに再講読されてしまいます。
Subscriptionsは複数のSubscriptionオブジェクトを返せます。
app({
subscriptions: state => [
tick(...),
foo(...),
],
});
EffectとSubscriptionのライブラリ
基本的なEffectとSubscriptionはhyperapp-fxで定義済みです。
hyperapp-fx
https://github.com/okwolf/hyperapp-fx/tree/HAv2
middleware
プロパティ
**middleware
**プロパティには、次のような関数を指定することで、dispatch
されるときに処理を挟むことができます。省略可能。
app({
middleware: dispatch => (action, payload) => {
console.log('dispatching', action, payload);
dispatch(action, payload);
console.log('dispatched', action, payload);
}
})
Lazy View
Lazy Viewは、Stateが変化しないときにVDOMを使いまわすパフォーマンス向上機能です。
VDOMを使いまわすことでDOMに反映させる処理がスキップされますが、Lazy VNodeの浅い比較処理があるため、小さいVDOMだと逆効果。
また、Lazy ViewはV1のLazy Componentsとはまったく関係ありません。
import { h, Lazy, app } from "hyperapp";
// autoが変化したときのみにレンダリングされる部分
const lazyView = props => (
<p>
auto:{props.auto ? "enable" : "disable"}
<br />
update: {new Date().toISOString()}
</p>
);
app({
view: state => (
// ...
{Lazy({ view: lazyView, key: "lazy-view", auto: state.auto })}
// ...
)
})
JSXで書くことも可能です。
<Lazy view={lazyView} key="lazy-view" auto={state.auto} />
**Lazy
関数でLazy View用のVNodeを作ります。Lazy
**関数では、render
プロパティやkey
プロパティなどを持つオブジェクトを渡します。
render
プロパティには、VNodeを作成する関数を指定します。この関数の引数には**Lazy
**に指定したオブジェクトが渡されます。
key
プロパティには、キーとなる文字列を指定します。これはLazy VNodeを効率よく使いまわすための識別子となります(通常のVNodeのkey
と同じ)。必須ではないですが、付けることが推奨されています。
render
やkey
以外のプロパティを指定することで、指定した値が変化したときにrender
が実行されます。
おわりに
モジュール化に関してはよくわかっていません。