LoginSignup
5
7

More than 5 years have passed since last update.

Worker APIで状態管理するライブラリ Businessman を書いた

Last updated at Posted at 2016-12-30

Worker(➚MDN) をつかって状態管理するライブラリ Businessman(➚GitHub) を作りました。

JavaScript はシングルスレッドが基本ですが、Businessman では Worker によって、UI を操作するメインスレッドとは別のスレッドで状態管理をします。

マルチスレッドによって、高負荷な処理があっても UI の応答性を確保することができます。

また、ステートをアプリケーションが直接操作する危険性も排除できます。

TL;DR

Businessman を利用する手順は以下のようになります。

  • npm install --save businessman でインストールする
  • Worker にインストールするためのファイルをビルドする
  • install( '/path/to/worker.js' ) で状態管理を開始する

このあとは dispatchsubscribe などのよくある API をつかって状態を更新したり取得することができます。

Businessman

Worker を作成する

ほかの状態管理ライブラリと最も異なるのが、状態管理用のファイルをアプリケーションとは別のファイルに持つ必要があるということです。

なぜなら、Worker() コンストラクタでは以下のように外部スクリプトを引数に取り Worker インスタンスを作成するからです。

var myWorker = new Worker( 'worker.js' )

Overview

Worker 用のファイルを作成するための最小の記述は、例として以下のようになります。

worker.js
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 は状態の名前とも言えます。

ex
type: 'counter'

type は必ずユニークな文字列である必要があります。
ほかのストアと同じ値は使用できません。

state

state はストアの状態を保持します。registerStore の際には初期値を指定しておきます。値はどのような型でも使えるはずです。

ex
state: 0

mutations

mutations は現在の状態とアクションからのデータを受け取って、新しい state を返します。

その後、自動的に状態が変更されてメインスレッドに通知されます。

ex
increment: ( state, num ) => {
    return state += num
}

ミューテーションには非同期処理を置くことができません。

actions

actionsmutations のミューテーションを呼び出すための関数です。

第 1 引数の commit を実行することでミューテーションを呼び出します。

アクションでは REST API を呼び出して新しいデータを取得するなど、非同期処理を置くこともできます。

ex
increment: ( commit, num = 1 ) => {
    commit( 'increment', num )
}

コミットするとメインスレッドに状態が通知されますが、第 3 引数を false にすると状態を null として通知することができます。

単純に状態が変わったという事実だけが知りたい場合には、より高速に処理するために有用です。

ex
increment: ( commit, num = 1 ) => {
    commit( 'increment', num, false )
}

Getters

後述する getState() で状態を取得するときに、任意の算出方法によって状態を取得できます。

ex
absolute: ( state ) => {
    return Math.abs( state )
}

worker.registerManager

アクションとミューテーションはひとつのストアに所属するため、複数のストアを横断した処理を書くことができません。そのときは manager をつかうことで実現できます。

managerworker.registerManager を使って登録します。

ex
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 はマネージャーのアイデンティティです。ストア同様、ユニークな文字列である必要があります。他のマネージャーと同じ値は使用できません。

ex
type: 'countUpMessage'

handler

handler はマネージャーで実行したい処理を書いた関数です。

第 1 引数にはすべてのストアインスタンスが入ってくるので、処理したい内容に応じて dispatch( 後述 )を呼び出します。

ex
handler: ( stores, num = 1 ) => {
    stores.counter.dispatch( 'increment', num )
    stores.message.dispatch( 'update', `${num} has been added to the counter` )
}

worker.start

ストア( と任意でマネージャー )を登録したら、その Worker がインストールされたときに状態管理を開始する必要があります。

ex
worker.start()

とくにオプションはありません。これだけです。

ビルド

最後に、ここまで新しい ES の仕様に合わせて記述してきましたが、このままでは ブラウザでは動きません

Rollup や Babel などを使ってブラウザ向けに変換しておいてください。

個人的には Rollup + Buble(➚npm) の組み合わせをよく使います。


Worker をインストールする

Worker ができたら、今度はインストールします。ここからは UI スレッドの領域になります。

ex
import { install } from 'businessman'

install( '/path/to/worker.js' )

インストールは非同期ですが、インストールが終わる前から、後述する dispatchsubscribe を実行できます。

状態の操作

ストアの操作や購読をします。

まずは標準の方法を書きますが、より馴染み深いスタイルでの使い方も後述しています。

dispatch

ストアのアクションを実行します。

第 1 引数にストアタイプ、第 2 引数にアクションタイプ、第 3 引数にアクションに渡したい値を指定します。

ex
import { dispatch } from 'businessman'

dispatch( 'counter', 'increment', 1 )

subscribe

ストアの更新を受け取ります。

第 1 引数にストアタイプ、第 2 引数には更新を受け取ったときのコールバックを指定します。

コールバックには更新後の新しい状態と、適用されたミューテーションタイプが入ってきます。

ex
import { subscribe } from 'businessman'

subscribe( 'counter', ( state, mutationType ) => {
    console.log( state, mutationType )
} )

unsubscribe

サブスクライブを削除します。

第 1 引数にストアタイプを指定して、第 2 引数を指定すればひとつのコールバックだけを削除できます。デフォルトではすべてのコールバックを削除します。

ex
import { unsubscribe } from 'businessman'

unsubscribe( 'counter' ) // すべてのコールバックを削除
unsubscribe( 'counter', listener ) // ひとつのコールバックだけを削除

無名関数がコールバックに指定されているときは、ひとつのコールバックだけを削除することはできません。

operate

複数ストアを横断した処理をおこなうためのマネージャーを実行します。

マネージャータイプとペイロードを指定することで呼び出します。

ex
import { operate } from 'businessman'

operate( 'countUpMessage', 1 )

getState

ストアの状態を取得します。

ストアは Worker にあるため、状態の取得も非同期になります。

ex
import { getState } from 'businessman'

getState( 'counter' )
.then( ( state ) => {
    console.log( state )
} )

Getters のタイプを指定して、計算後の状態を取得することもできます。

ex
import { getState } from 'businessman'

getState( 'counter', 'absolute' )
.then( ( state ) => {
    console.log( state )
} )

getAllState

すべてのストアの状態を取得します。

もちろん非同期になります。

ex
import { getAllState } from 'businessman'

getAllState()
.then( ( state ) => {
    console.log( state )
} )

ストアスタイルによる操作

この API は v2.0.0 で廃止されました。利用する場合は v1.5.1 以下になります。

store.dispatch() のような、よくあるスタイルで状態の操作をすることもできます。

ただし、ストアは Worker スレッドにあるため直接操作をすることはできません。

Businessman にビルトインされている CREATE_CLIENT_STORE アクションをサブスクライブすることによって、ストアスタイルによる操作をするためにストアのメソッドを一部コピーしたオブジェクトを受け取ることができます。

ex
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 を呼び出します。

ex
/* Dispatch */
counter.dispatch( 'increment', 1 )

/* Subscribe */
counter.subscribe( () => {
    ...
} )

/* Unsubscribe */
counter.unsubscribe()
// or
counter.unsubscribe( listener )

/* getState */
counter.getState()
.then( ( state ) => {
    ...
} )

ほかにも、CREATE_CLIENT_MANAGER をサブスクライブするとマネージャーの一覧を取得できます。

これから

アップデートしていく予定です。状態のスナップショットと復元、ドキュメントの整備などなど。


  1. スレッド間のデータサイズがパフォーマンスに影響するためです。 

5
7
0

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
5
7