1. はじめに
Hyperappでアプリを開発中、課題解決の為に、
loteooさんにzacenoさんが書かれた素晴らしいページを教えてもらいました。
いつでも読み返せるように翻訳します。
本記事では原文サイトの中の、
Initialization [訳: 初期化] について翻訳したものを記述します。
完璧な翻訳ではないので、齟齬が無いよう原文とパラレルで記述します。
以下原文ページのリンク
- hypercraft - index
- cross-namespace-action-calling
- initialization
- memoization
- modular-apps
- static-vnodes
- utility-actions
- decorator-components
- stateful-components
- keying-components
- about
2. 翻訳
Initialization
初期化
A useful convention and pattern for initialization-time side-efffects, such as listening to browser events or subscribing to websockets.
ブラウザのイベントのリスニングや、Webソケットの使用と言った、初期化時の副作用の便利な慣習とパターン。
It is common for an app to need to do "things" at start-up. Things, such as subscribing to websocket-connections, listening to browser-events, make an async fetch-request.
一般的に、アプリケーションは起動時には何らかの処理を行う必要があります。例えば、Webソケットの接続、ブラウザのイベントのリスニング、非同期のfetchリクエストです。
The callbacks of these operations need to call actions of your app in order to interact with it. Thankfully, app(...) returns your wired actions, so you can hook them up to your callbacks.
それらオペレーションのコールバックはあなたのアプリケーションのアクションをインタラクティブに呼ぶ必要がありますよね。
ありがたいことに、app(...)
は、全てのアクションを束ねたactionsをreturnしてくれますので、何らかのコールバック処理にそのactionsの処理を繋げる事ができます。
const wiredActions = app(...)
fetch('http://example.com/first_data')
.then(data => data.json())
.then(data => wiredActions.loaded(data))
This works for main-level initialization. But if you've broken up your state/actions in several namespaces, imported from different files (such as I talk about in modularizing apps) - then how do you give each of your modules their own wired actions?
これはメインレベルの初期化で有効です。
でももし、複数の名前空間でstate/actionsを分割し、異なるファイル(モジュール化アプリケーションについて話しています)からインポートする場合には、どうやってそれぞれのモジュールにそれ(全てのアクションを束ねたactions)を渡しますか?
Hyperapp's router has the following approach to this:
Hyperappのルータには、次のようなアプローチがあります。
import { h, app } from "hyperapp"
import { location as router} from "@hyperapp/router"
const state = {
router: router.state,
...
}
const actions = {
router: router.actions,
...
}
const view = ...
const wiredActions = app(state, actions, view, document.body)
router.subscribe(wiredActions.router)
Notice how the router has a subscribe function, to which you pass the router-namespace's actions. This allows the router module to subscribe them to route-change events.
routerがsubscribeファンクションをどのように使っているか注目してください。
そのsubscribeファンクションに、router
と言う名前空間のアクションを渡します。
これで、routerモジュールは、ルートの変更イベントをactionsで定義されているrouterと言う名前空間のアクションにサブスクライブします。
This works fine for the router, but it doesn't quite scale if you have multiple modules. Especially if they're nested several levels deep.
これはルータとしていい感じに動きますが、仮にもし複数モジュールがある場合は拡張ができません。特にそれが深い階層になっている場合は。
In my apps, I have often adopted the convention of an init action. As an action, it has access to all the other actions, so I can set them up as callbacks just the same. All that needs to be done after the app(...) call is to call the init() action.
私のアプリでは、initアクションの規約を採用することがよくありました。それはあるアクションが、他のすべてのアクションにアクセスできると言うものです。
つまりそれらアクションを全く同じようにcallbacksとして設定できると言う事。それに必要なのは、**app(...)**呼び出しの後に、**init()**アクションを呼び出すことだけです。
const {init} = app(
//STATE
{
router: router.state,
...
},
///ACTIONS
{
router: router.actions,
init: _ => (_, actions) => {
router.subscribe(actions.router)
fetch('https://example.com/first_data')
.then(data => data.json())
.then(actions.loadData)
},
...
},
//VIEW
...,
//CONTAINER
document.body
)
init()
At this point, it doesn't look much better than if we'd just done all the initialization after app(...). But imagine if we had several modules, and each wanted to define their own initialization steps, and bind to their actions (after they'd been wired). Rather than following the example of the router, we could simply initialize the submodules from our top-level init:
この時点では、**app(...)**の後にすべての初期化を実行する書き方よりも良さそうには見えないですね。
しかし、いくつかのモジュールがあり、それぞれが独自の初期化の処理を定義し、それらの(全てのアクションを束ねた後の)actionにバインドしたかったとすると、actionsnのトップレベルのinitからサブモジュールを初期化する方がrouterの例よりもシンプルです:
import foo from './foo'
import bar from './bar'
const {init} = app(
//STATE
{
foo: foo.state,
bar: bar.state,
...
},
//ACTIONS
{
foo: foo.actions,
bar: bar.actions,
init: _ => (_, actions) => {
actions.foo.init()
actions.bar.init()
//top level initializations
},
...
},
//VIEW
...,
//CONTAINER
document.body
)
init()
As long as we stick to the convention of every module having an init action which calls the init() of submodules - then all submodules' initialization will be called at startup. And there's no need to know how to call them (such as router.subscribe), or what actions they need.
モジュールがその他サブモジュールのinit()を呼ぶinit アクションをもつ規約にこだわるなら、
サブモジュールの初期化の処理はアプリケーションの起動時に呼ばれることになります。
そうすると、router.subscribeのようなサブモジュールの初期化処理とそれらが必要とするアクションを知る必要性は無くなりますね。
One thing you perhaps are wondering: when, in the lifecycle of an app, is init() called?
一つ気になる事があります: アプリのライフサイクルでinit()が呼ばれるのはいつですか?
By the time app(...) returns, it will have wired all the actions (or else how could it return them), but the app will not yet have rendered for the first time. If you want to hold something off until after the first render, wrap it in a setTimeout(..., 0)
**app(...)**がreturnをするまでには、すべてのアクション(またはそれらを返す方法)を束ねますが、その時点ではまだアプリケーションは初期レンダリングしません。最初のレンダリングの後まで何らかの処理実行を待たせる場合は、**setTimeout(...、0)**にラップしてください。