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が実行されます。
おわりに
モジュール化に関してはよくわかっていません。![]()
