LoginSignup
28
21

More than 3 years have passed since last update.

Hyperapp V2

Last updated at Posted at 2018-10-27

Hyperapp V2がリリースされました。:tada:

2019/03/27 追記:alpha.10に合わせて全面的に書き換えました。(おそらく次のアップデートでイベント名が小文字になる予定です。)
2019/09/14 追記:v2.0.1に合わせて書き換え。
2020/07/15 追記:v2.0.5がリリースされました。ESモジュール化、JSXの直接的なサポート・Payload Creatorが削除、Lazymemoにリネームされています。(本記事に未反映

Hyperappとは

VDOMを搭載した超絶小さい(ソースが読める)JavaScriptフレームワークです。(2KBぐらい)

詳しくはこちら

Hyperapp V2

リファレンス

V2概略図

https://github.com/diontools/hyperapp-learn/raw/master/diag.png

  • 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)なしで実行することで高速化しました

動くサンプルアプリ

数値をカウントするだけのアプリです。JSXで書かれています。

Edit hyperapp v2 alpha

インストール

npm i hyperapp

JSXを使用する場合はbabelのtransform-react-jsxでpragmaにhを指定します。

公開している関数

Hyperappは3つの関数を公開(export)しています。

  • h関数
    • VDOMのノード(VNode)を作成する関数
    • JSXを使用して間接的に利用します(直接使ってもOK:ok_hand:
  • 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 });

:warning:注意:引数の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はundefinedpreventDefault()の戻り値)になるので、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のenabledtrueのときtickを購読します。enabledfalseになるとtickが購読解除されます。

:warning:注意: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と同じ)。必須ではないですが、付けることが推奨されています。

renderkey以外のプロパティを指定することで、指定した値が変化したときにrenderが実行されます。

おわりに

モジュール化に関してはよくわかっていません。:innocent:

28
21
2

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
28
21