おことわり
- この発表はかなりお気持ちです
- 他の発表とは異なり(?)なんら学術的裏付けがあるものではありません
- なんと(?)まったく数式が出てきません
自己紹介
- 亀岡 亮太
- 株式会社HERP リードエンジニア
- Twitter: @ryotakameoka
- GitHub: @ryota-ka
Web フロントエンドアプリケーションの話をします
はじめに
はじめに
この発表の内容
- Web フロントエンドフレームワークはどのような性質を満たすべきか?
- 異なる副作用を分離したい
- 非同期がほしい
- Cycle.js というフレームワークの紹介
- effect を足していく
- effect を handle する
この発表のゴール
- Cycle.js の存在を覚えて帰ってもらう
- 「Cycle.js おもしろそう,書いてみようかな」と思ってもらう
Web フロントのフレームワークはどのような性質を満たしていてほしいか?
Web フロントのフレームワークの望ましい性質
- 種々の副作用を互いに分離できると嬉しい
- 非同期が織り込み済みだと嬉しい
Web フロントのフレームワークの望ましい性質
- 種々の副作用を互いに分離できると嬉しい
- 非同期が織り込み済みだと嬉しい
種々の副作用を互いに分離できると嬉しい
アプリケーションは様々な副作用を持つ
- DOM
- ユーザの入力イベントを受け取る
- 画面を描画する
- HTTP
- APIを叩いて結果を受け取る
- ステート管理
- WebSocket
- Location API
- History API
- Web Storage API
- タイトル変えたり……
- favicon 変えたり……
- visibility API とか……
メッチャあるやんけ😰
種々の副作用を互いに分離できると嬉しい (cont'd)
- 異なる副作用は別々に処理したい
- あらゆる副作用をゴッチャにしたくない
- 関心の分離をしたい
- テスタビリティ
- 後から副作用を追加できるようにしておきたい
- 拡張性
種々の副作用を互いに分離できると嬉しい (cont'd)
React + Redux (+ Redux middleware) は
DOM + ステート管理 (+ その他 IO すべて) に見える
(少なくとも私には)
Web フロントのフレームワークの望ましい性質 (cont'd)
- 種々の副作用を互いに分離できると嬉しい
- 非同期が織り込み済みだと嬉しい
非同期が織り込み済みだと嬉しい
- どうあがいても絶対必要なので
- API のレスポンスはいつ返ってくるかわからない
- ユーザの入力に対する高価な処理 (e.g. 検索 API を叩く) の debouncing
- 「どこに何を書けばいいかわからない」は利用者がつらい
- お前のことやぞ Redux (middleware)
望ましい性質を満たすには?
望ましい性質を満たすには?
Web フロントのフレームワークの望ましい性質 (再掲)
- 種々の副作用を互いに分離できると嬉しい
- → ???
- 非同期が織り込み済みだと嬉しい
- → ???
望ましい性質を満たすには?
Web フロントのフレームワークの望ましい性質 (再掲)
- 種々の副作用を互いに分離できると嬉しい
- → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
- → Observable / Stream
望ましい性質を満たすには?
Web フロントのフレームワークの望ましい性質 (再掲)
- 種々の副作用を互いに分離できると嬉しい
- → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
- → Observable / Stream
それぞれの副作用を effect 的に扱う
- 副作用の処理を client-server モデルとして捉える
- client (アプリケーション) は server (effect handler) に副作用 (effect) の処理を依頼し,結果を受け取る
それぞれの副作用を effect 的に扱う (cont'd)
- 異なる handler は異なる effect をハンドルする
- 関心事の分離
- アプリケーションが新たな effect を要求するようになれば handler も増やせばよい
- 拡張性の確保
望ましい性質を満たすには? (cont'd)
- 種々の副作用を互いに分離できると嬉しい
- → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
- → Observable / Stream
Observable / Stream
- ReactiveX が代表的
- Cycle.js では xstream が主要な選択肢
- 非同期に値が流れてくるやつ
- Observable が値を流し,Observer がその値を使って何をするかを決める
- 一応形式的に考察されていたりするっぽい
- 以下 Stream と呼びます
Stream (cont'd)
例 : ダブルクリックの検出
出典 : The introduction to Reactive Programming you've been missing
望ましい性質を満たすには? (cont'd)
- 種々の副作用を互いに分離できると嬉しい
- → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
- → Observable / Stream
effect handler と Stream で非同期にやりとりすればいいのでは?
ところで Cycle.js というフレームワークがありまして……
Cycle.js
- "A functional and reactive JavaScript framework for predictable code"
-
GitHub では約 9,300 stars
- 「名前だけは知ってる」という方も多いかも?
Cycle.js (cont'd)
Cycle’s core abstraction is your application as a pure function main()
where inputs are read effects (sources
) from the external world and outputs (sinks
) are write effects to affect the external world. These I/O effects in the external world are managed by drivers
: plugins that handle DOM effects, HTTP effects, etc.
(cycle.js.org より引用)
これやんけ
図
用語
-
source
- 外界からのメッセージが流れてくる stream
- component が driver から受け取る
-
sink
- 外界に干渉するためのメッセージを流す stream
- component が driver に渡す
-
driver
- sink を受け取って (典型的には) 副作用を起こし,source を返す関数
- e.g. DOM driver, HTTP driver
- effect を handle できる
-
component
- sources を受け取って sinks を返す関数
- React や Vue の component とは意味が異なるので注意
-
higher-order component
- component を取って component を返す関数
- effect を handle できる
effect 的な Cycle.js の世界観
- component は effectful な計算である
- component が持つ effect は driver や higher-order component によって handle される
- すべての effect を handle することができればアプリケーションが実行できる
component を書く
effect を足していきます
-
-
DOM
effect
-
-
-
state
effect
-
-
-
WebSocket
effect
-
-
-
toast
effect
-
effect を足していきます
-
1.
DOM
effect 🔥 -
-
state
effect
-
-
-
WebSocket
effect
-
-
-
toast
effect
-
1. DOM
effect
type SoDOM = { DOM: DOMSource }; // DOM driver から受け取る
type SiDOM = { DOM: Stream<JSX.Element> }; // DOM driver に渡す
function main({ DOM }: SoDOM): SiDOM {
const submitEvent$: Stream<Event> = DOM.select('.submit').events('click');
const bodyInputEvent$: Stream<Event> = DOM.select('.body').events('input');
const body$: MemoryStream<string> = Stream.merge(
bodyInputEvent$.map((e: any) => e.target.value),
submitEvent$.mapTo(''), // 送信されたらクリア
).startWith('');
const view$ = body$.map(body =>
<div>
<input type="text" className="body" value={body} />
<button type="button" className="submit">
送信
</button>
<ul>
{messages.map(msg => (
<li key={msg}>{msg}</li>
))}
</ul>
</div>
);
return {
DOM: view$,
};
}
1. DOM
effect (cont'd)
想像図
なぜ「想像」なのか?
→ DOM
effect がどのように handle されるかで挙動が変わりうるから
effect を足していきます (cont'd)
-
-
DOM
effect
-
-
2.
state
effect 🔥 -
-
WebSocket
effect
-
-
-
toast
effect
-
2. state
effect
type State = {
body: string;
messages: string[];
}
function State(): State {
return {
body: '',
messages: [],
};
}
export type Endo<T> = (x: T) => T;
export type SoState = { state: StateSource<State> };
export type SiState = { state: Stream<Endo<State>> };
export function main({ DOM, state }: SoDOM & SoState): SiDOM & SiState<State> {
const state$ = state.stream; // 現在の state が得られる
// ...
const view$ = state$.map(View); // view は切り出しました
const reducer$ = Stream.merge<Endo<State>>(
bodyInputEvent$.map((e: any) => state => ({ ...state, body: e.target.value })),
submitEvent$.mapTo(state => ({ ...state, body: '' })),
).startWith(_ => State());
return {
DOM: view$,
state: reducer$, // Endo<State> を流す
};
}
2. state
effect (cont'd)
想像図 (さっきと一緒)
effect を足していきます (cont'd)
-
-
DOM
effect
-
-
-
state
effect
-
-
3.
WebSocket
effect 🔥 -
-
toast
effect
-
3. WebSocket
effect
type SoWebSocket = { WebSocket: Stream<IncomingMessage> };
type SiWebSocket = { WebSocket: Stream<OutgoingMessage> };
function main({
// ...
WebSocket,
}: SoDOM & SoState & SoWebSocket): SiDOM & SiState & SiWebSocket {
// ...
const outgoingMessage$ = submitEvent$
.compose(sampleCombine(state$)) // submit 時の state を切り取る
.map<OutgoingMessage>(([_, { body }]) => ({ type: 'message', body }));
const reducer$: Stream<Endo<State>> = Stream.merge<Endo<State>>(
// ...
WebSocket.filter(IncomingMessage.isMessage).map(({ message }) => state => ({
...state,
messages: [message, ...state.messages], // 受け取ったメッセージを追加する
})),
).startWith(_ => State());
return {
// ...
WebSocket: outgoingMessage$, // client -> server
};
}
3. WebSocket
effect (cont'd)
想像図
effect を足していきます (cont'd)
-
-
DOM
effect
-
-
-
state
effect
-
-
-
WebSocket
effect
-
-
4.
toast
effect 🔥
4. toast
effect
type SoToast = {};
type SiToast = { toast: Stream<string | null> };
function main({
// ...
}: SoDOM & SoState & SoToast & SoWebSocket): SiDOM & SiState & SiToast & SiWebSocket {
// ...
const toast$ = WebSocket
.filter(IncomingMessage.isRoomJoin)
.map(({ name }) => `${name} さんが入室しました`);
return {
// ...
toast: toast$,
};
}
4. toast
effect (cont'd)
想像図
effect のハンドリング
effect のハンドリング
- component の effect をすべて取り除くことで,アプリケーションを実行することができる
- 取り除いていくぞ
toast
effect
DOM
effect に押し付ける
function handleToast<So extends SoDOM, Si extends Partial<SiDOM & SiToast>>(
component: Component<So, Si>,
): Component<So, Omit<Si, 'toast'>> {
return function(sources) {
const sinks = component(sources);
const SiDOM: Stream<JSX.Element> = sinks.DOM || Stream.empty();
const SiToast: Stream<string | null> = sinks.toast || Stream.empty();
const view$ = Stream.combine(SiDOM, SiToast.startWith(null))
.map(いい感じにガッチャンコする);
return {
...sinks,
DOM: view$,
};
};
}
state
effect
自己解決 (中で循環参照してる)
function withState<
So extends { state: StateSource<State> },
Si extends { state: Stream<Endo<State>> }
>(
main: Component<So, Si>
): Component<Omit<So, 'state'>, Omit<Si, 'state'>>;
DOM
effect & WebSocket
effect
残りは driver で
const drivers = {
DOM: makeDOMDriver('#app'),
WebSocket: makeWebSocketDriver(),
};
run(handleToast(withState(main)), drivers);
テスト
テスト
- component は sources → sinks なただの関数
const sinks = component({
DOM: mockDOMSource(...), // compatible な sources を与える
// ...
});
// ここで sinks.DOM や sinks.WebSocket に対して assertion を書くだけ
テスト (cont'd)
it('emits 42', done => {
// ...
foo$.take(1).addListener({
next(x) {
expect(x).toBe(42);
},
complete() {
done();
},
});
});
テスト (cont'd)
好きな組み合わせをテストすればよい
- 前提条件
- ユーザがボタンをクリックしたときに……
- API のレスポンスが得られたときに……
- WebSocket でメッセージを受け取ったときに……
- アサーション
- API リクエストが飛ぶか?
- state は更新されるか?
- WebSocket でメッセージが送られるか?
- toast は表示されるか?
終わりに
終わりに
- Cycle.js はこんなフレームワーク
- 副作用を effect 的に扱える
- effect handler (driver) とは Stream でやり取りする
- higher-order component でも effect を handle できる
- Web フロント以外にも使えて汎用的
- もし興味があれば書いてみてください!
- その他気になった方はよければ懇親会で話しかけてください