Worker(➚MDN) をつかって状態管理するライブラリ Businessman(➚GitHub) を作りました。
JavaScript はシングルスレッドが基本ですが、Businessman では Worker によって、UI を操作するメインスレッドとは別のスレッドで状態管理をします。
マルチスレッドによって、高負荷な処理があっても UI の応答性を確保することができます。
また、ステートをアプリケーションが直接操作する危険性も排除できます。
TL;DR
Businessman を利用する手順は以下のようになります。
-
npm install --save businessman
でインストールする - Worker にインストールするためのファイルをビルドする
-
install( '/path/to/worker.js' )
で状態管理を開始する
このあとは dispatch
や subscribe
などのよくある API をつかって状態を更新したり取得することができます。
Worker を作成する
ほかの状態管理ライブラリと最も異なるのが、状態管理用のファイルをアプリケーションとは別のファイルに持つ必要があるということです。
なぜなら、Worker()
コンストラクタでは以下のように外部スクリプトを引数に取り Worker インスタンスを作成するからです。
var myWorker = new Worker( 'worker.js' )
Overview
Worker 用のファイルを作成するための最小の記述は、例として以下のようになります。
import { worker } from 'businessman'
worker.registerStore( {
type: 'counter',
state: 0,
mutations: {
increment: ( state, num ) => {
return state += num
}
},
actions: {
increment: ( commit, num = 1 ) => {
commit( 'increment', num )
}
},
getters: {
absolute: ( state ) => {
return Math.abs( state )
}
}
} )
worker.start()
詳しく見ていきます。
worker.registerStore
ストアを登録します。
引数には、type
, state
, mutations
, actions
を含んだオブジェクトをとります。
type
type
はストアのアイデンティティになります。Businessman では ひとつのストアにひとつの状態 を持つことを推奨1 しているので、type
は状態の名前とも言えます。
type: 'counter'
type
は必ずユニークな文字列である必要があります。
ほかのストアと同じ値は使用できません。
state
state
はストアの状態を保持します。registerStore
の際には初期値を指定しておきます。値はどのような型でも使えるはずです。
state: 0
mutations
mutations
は現在の状態とアクションからのデータを受け取って、新しい state
を返します。
その後、自動的に状態が変更されてメインスレッドに通知されます。
increment: ( state, num ) => {
return state += num
}
ミューテーションには非同期処理を置くことができません。
actions
actions
は mutations
のミューテーションを呼び出すための関数です。
第 1 引数の commit
を実行することでミューテーションを呼び出します。
アクションでは REST API を呼び出して新しいデータを取得するなど、非同期処理を置くこともできます。
increment: ( commit, num = 1 ) => {
commit( 'increment', num )
}
コミットするとメインスレッドに状態が通知されますが、第 3 引数を false
にすると状態を null
として通知することができます。
単純に状態が変わったという事実だけが知りたい場合には、より高速に処理するために有用です。
increment: ( commit, num = 1 ) => {
commit( 'increment', num, false )
}
Getters
後述する getState()
で状態を取得するときに、任意の算出方法によって状態を取得できます。
absolute: ( state ) => {
return Math.abs( state )
}
worker.registerManager
アクションとミューテーションはひとつのストアに所属するため、複数のストアを横断した処理を書くことができません。そのときは manager
をつかうことで実現できます。
manager
は worker.registerManager
を使って登録します。
worker.registerManager( {
type: 'countUpMessage',
handler: ( stores, num = 1 ) => {
stores.counter.dispatch( 'increment', num )
stores.message.dispatch( 'update', `${num} has been added to the counter` )
}
} )
詳しく見ていきます。
type
type
はマネージャーのアイデンティティです。ストア同様、ユニークな文字列である必要があります。他のマネージャーと同じ値は使用できません。
type: 'countUpMessage'
handler
handler
はマネージャーで実行したい処理を書いた関数です。
第 1 引数にはすべてのストアインスタンスが入ってくるので、処理したい内容に応じて dispatch
( 後述 )を呼び出します。
handler: ( stores, num = 1 ) => {
stores.counter.dispatch( 'increment', num )
stores.message.dispatch( 'update', `${num} has been added to the counter` )
}
worker.start
ストア( と任意でマネージャー )を登録したら、その Worker がインストールされたときに状態管理を開始する必要があります。
worker.start()
とくにオプションはありません。これだけです。
ビルド
最後に、ここまで新しい ES の仕様に合わせて記述してきましたが、このままでは ブラウザでは動きません 。
Rollup や Babel などを使ってブラウザ向けに変換しておいてください。
個人的には Rollup + Buble(➚npm) の組み合わせをよく使います。
Worker をインストールする
Worker ができたら、今度はインストールします。ここからは UI スレッドの領域になります。
import { install } from 'businessman'
install( '/path/to/worker.js' )
インストールは非同期ですが、インストールが終わる前から、後述する dispatch
や subscribe
を実行できます。
状態の操作
ストアの操作や購読をします。
まずは標準の方法を書きますが、より馴染み深いスタイルでの使い方も後述しています。
dispatch
ストアのアクションを実行します。
第 1 引数にストアタイプ、第 2 引数にアクションタイプ、第 3 引数にアクションに渡したい値を指定します。
import { dispatch } from 'businessman'
dispatch( 'counter', 'increment', 1 )
subscribe
ストアの更新を受け取ります。
第 1 引数にストアタイプ、第 2 引数には更新を受け取ったときのコールバックを指定します。
コールバックには更新後の新しい状態と、適用されたミューテーションタイプが入ってきます。
import { subscribe } from 'businessman'
subscribe( 'counter', ( state, mutationType ) => {
console.log( state, mutationType )
} )
unsubscribe
サブスクライブを削除します。
第 1 引数にストアタイプを指定して、第 2 引数を指定すればひとつのコールバックだけを削除できます。デフォルトではすべてのコールバックを削除します。
import { unsubscribe } from 'businessman'
unsubscribe( 'counter' ) // すべてのコールバックを削除
unsubscribe( 'counter', listener ) // ひとつのコールバックだけを削除
無名関数がコールバックに指定されているときは、ひとつのコールバックだけを削除することはできません。
operate
複数ストアを横断した処理をおこなうためのマネージャーを実行します。
マネージャータイプとペイロードを指定することで呼び出します。
import { operate } from 'businessman'
operate( 'countUpMessage', 1 )
getState
ストアの状態を取得します。
ストアは Worker にあるため、状態の取得も非同期になります。
import { getState } from 'businessman'
getState( 'counter' )
.then( ( state ) => {
console.log( state )
} )
Getters のタイプを指定して、計算後の状態を取得することもできます。
import { getState } from 'businessman'
getState( 'counter', 'absolute' )
.then( ( state ) => {
console.log( state )
} )
getAllState
すべてのストアの状態を取得します。
もちろん非同期になります。
import { getAllState } from 'businessman'
getAllState()
.then( ( state ) => {
console.log( state )
} )
ストアスタイルによる操作
この API は v2.0.0 で廃止されました。利用する場合は v1.5.1 以下になります。
store.dispatch()
のような、よくあるスタイルで状態の操作をすることもできます。
ただし、ストアは Worker スレッドにあるため直接操作をすることはできません。
Businessman にビルトインされている CREATE_CLIENT_STORE
アクションをサブスクライブすることによって、ストアスタイルによる操作をするためにストアのメソッドを一部コピーしたオブジェクトを受け取ることができます。
let counter
subscribe( 'CREATE_CLIENT_STORE', ( stores ) => {
console.log( stores ) // { counter: { dispatch: function () {...}, subscribe: function () {...}, unsubscribe: function () {...}, getState: function () {...} } }
counter = stores.counter
} )
CREATE_CLIENT_STORE
から取得されるストアでは次のように API を呼び出します。
/* Dispatch */
counter.dispatch( 'increment', 1 )
/* Subscribe */
counter.subscribe( () => {
...
} )
/* Unsubscribe */
counter.unsubscribe()
// or
counter.unsubscribe( listener )
/* getState */
counter.getState()
.then( ( state ) => {
...
} )
ほかにも、CREATE_CLIENT_MANAGER
をサブスクライブするとマネージャーの一覧を取得できます。
これから
アップデートしていく予定です。状態のスナップショットと復元、ドキュメントの整備などなど。
-
スレッド間のデータサイズがパフォーマンスに影響するためです。 ↩