Almin 0.13.xのStable版をリリースしました。 今後に向けて下地となる基盤部分をかなり書き換えたのと、Strict modeや試験的にTransactionを追加しました。

Diff 12.x…13

まとめ

0.12xからの0.13.10の変更をまとめると次の通りです。

  • 🔥 Breaking Changes
    • IE9/IE10のサポート終了
      • CIから落としただけなのでまだ動くかもしれないですが
    • 一部Deprecatedはありますが基本的にコードは変更しなくても動くはずです
  • ⚠️ Deprecated
    • リネーム context.onWillExecuteEachUseCase -> context.events.onWillExecuteEachUseCase
    • almin/migration-toolsを使うことで自動的にマイグレーションできます。
  • 🆕 Features
  • :house: Internal
    • Unit of Work(作業単位)が内部的な実装として追加され、データフローなどがコードとしてきちんと管理されるように(Transactionなどもこれを利用)

詳しくはRelease Almin 0.13.10 · almin/alminをみてください。


Breaking Change 🔥

Almin 0.13は幾つか破壊的な変更がありますが、殆どの場合コードは変更しなくても動くと思います。

Drop IE9 and IE10

IE 9/10のサポートは終了しました。

Store#onDispatch receive only dispatched the Payload by UseCase#dispatch #224 #254 #255

Store#onDispatchは今までUseCase#dispatch以外の内部的なpayloadを受け取っていました。 今回からはStore#onDispatchUseCase#dispatchのみ、つまりユーザー自身がdispatchしたpayloadのみを受け取ります。

他のAlminのライフサイクルイベントを受け取り対場合はcontext.events(LifeCycleEventHub)を参照してください。

Recommened: 基本的にはStoreの更新はStore#receivePayloadで行うことを推奨しています。(Strict modeはこれをチェックできます)

Store#receivePayloadでのStore更新のパターン

class YourStore extends Store {
   constructor(){
      super();
      // Initialize state
      this.state = {
         foo : "bar"
      };
   }

   // Update code here
   receivePayload(payload){
     this.setState(this.state.reduce(payload));
   }

   getState(){
     return this.state;
   }
}

Automatically catch throwing Error from UseCase#execute() #193 #194

0.12まではUseCase#executeで同期的なエラーが発生すると突き抜けて、window.onerrorまで到達していました。

0.13からは、同期的なエラーであってもcatchできるようになっています。

class ThrowUseCase extends UseCase {
  execute() {
     throw new Error("error");
  }
}

context.useCase(new ThrowUseCase).catch(error => {
    console.log("It can catch")
});

new Store() throw Error

Storeはabstract classなので継承してください。 new Store()は単純にgetState時に例外を投げます。

Notable Changes

Renaming: context.on* to context.events.on* #239

context.on*などのライフサイクルイベントを検知するイベントをcontext.events.on*に移動しました。

ただし、Context#onChangeはライフサイクルイベントという扱いではなく、UI更新のためのハンドラを登録するAPIなのでそのままContextに残っています。 他のAPIは非推奨となっています。

今はまだ@deprecatedタグがついただけですが、もうちょっとしたらコンソールにも警告が出るようになります。そして次のメジャーアップデートで削除します。

これを移行するマイグレーションツールも用意してあるのでご利用ください。

Migration

almin/migration-toolsはつぎのようにnpmでインストールできます。 後は移行したファイルを指定して対話に従って選択すれば完了です。

Notes 先にバージョン管理システムにコミットしてバックアップしてから実行してください。

npm install -g @almin/migration-tools
almin-migration-tools [<file|glob> ...]

Before:

context.onWillExecuteEachUseCase((payload, meta) => {});
context.onDispatch((payload, meta) => {});
context.onDidExecuteEachUseCase((payload, meta) => {});
context.onCompleteEachUseCase((payload, meta) => {});
context.onErrorDispatch((payload, meta) => {});

After:

context.events.onWillExecuteEachUseCase((payload, meta) => {});
context.events.onDispatch((payload, meta) => {});
context.events.onDidExecuteEachUseCase((payload, meta) => {});
context.events.onCompleteEachUseCase((payload, meta) => {});
context.events.onErrorDispatch((payload, meta) => {});

UndocumentなAPIであるUseCaseExecutor#on*が削除 #243

Remove

UseCaseExecutor#onWillExecuteEachUseCase
UseCaseExecutor#onDidExecuteEachUseCase
UseCaseExecutor#onCompleteExecuteEachUseCase

Feature 🆕

Strict mode

詳しくはドキュメントを見てください。

ドキュメント: https://almin.js.org/docs/tips/strict-mode.html

Strict modeはStore#receivePayload以外のタイミングで、Storeを更新シていると警告を出すようにするモードです。

// enable strict mode
const context = new Context({ 
   dispatcher, 
   store, 
   options: {
     strict: true
   }
});

関連:

Context#transaction #226

  • Stability: Experimental
  • この機能はstrict modeではないと警告がでます

Context#transactionはUseCaseをトランザクション的に実行するAPIです。 Context#useCaseは一つのUseCaseを一個ずつ実行して、そのUseCaseの実行終了毎にViewの更新が行われます。(具体的にはStoreが一つでも変更されているならContext#onChangeが呼び出されます)

一方、Context#transactionTransactionContextというトランザクション用のコンテキストを作り、transactionContext.useCaseでUseCaseを実行してもすぐにはViewの更新は行われません。

幾つかのUseCaseをtransactionContext.useCaseで実行した後に、確定したいタイミングで、transactionContext.commit()を実行するとそれまでに実行していたUseCaseからのdispatchやライフサイクルなどはまとめてStoreに伝わります。

Context#transactionの中でUseCaseを何度実行しても結果として起きるViewの更新はcommit()したタイミングの一回だけになります。

UseCase vs. Transaction

サンプルコード:

const context = new Context({
    dispatcher: new Dispatcher(),
    store: storeGroup,
    options: {
        strict: true
    }
});
// then - called change handler a one-time
let onChangeCount = 0;
context.onChange(stores => {
    onChangeCount++;
});
// when
context
    .transaction("transaction name", transactionContext => {
        return transactionContext
            .useCase(new ChangeAUseCase())
            .execute(1)
            .then(() => transactionContext.useCase(new ChangeAUseCase()).execute(2))
            .then(() => transactionContext.useCase(new ChangeAUseCase()).execute(3))
            .then(() => transactionContext.useCase(new ChangeAUseCase()).execute(4))
            .then(() => transactionContext.useCase(new ChangeAUseCase()).execute(5))
            .then(() => {
                // commit the result to StoreGroup
                transactionContext.commit();
            });
    })
    .then(() => {
        // finish the transation
        console.log(onChangeCount); // => 1
    });

名前の通り大量のUseCaseを連続して実行する必要がある際に利用できます。 初期画面に必要なUseCaseが複数があるが、その途中で何度もViewを更新する必要がないといった際に、一つのトランザクションとしてまとめることができます。

例えばReactにもReactDOM.unstable_batchedUpdatesという隠しAPIみたいなものがありますが、そういうのをState管理側でやるための仕組みです。

Reduxにも似たようなBatch updatingの仕組みを持ったmiddlewareがあります。

AlminのContext#transactionはstrict modeじゃないと正しく動きません。 簡単にいうと、Alminの範囲外でStoreが更新されているとそのトランザクションが正しくても結果が正しくないことがおこるのでそういうケースを防止するためです。

現在の実装は1つのトランザクションで1回のcommit()またはexit()のみができます。exit()した場合はそのトランザクションで実行したUseCaseのイベントを破棄します。

また、今はあるUseCaseの中からcommit()したい!やサブトランザクションのような概念はありません。Stability: Experimentalなのでその辺に意見ある人は意見ください。

Add Fluent style executor #193

  • Stability: Experimental
  • This feature is subject to change. It may change or be removed in future versions.
  • See #193

TypeScriptでUseCase#executeをする際に型チェックがちゃんとできるバージョンです。

context.useCase(new MyUseCase())
    .executor(useCase => useCase.execute("test"))
    .then(value => {
        console.log(value);
    });

context.useCase(useCase).execute()context.useCase(useCase).executor(useCase => useCase.execute())の糖衣構文です。

もっとこうした方がよさそうという意見がある場合は次のIssueに意見をください。

Documentation

APIドキュメントが更新されているので https://almin.js.org/ をみてください。

Almin-logger

Internals

JavaScriptのライブラリを徐々にTypeScriptに移行する | Web Scratchで書いたようにAlminのテストをTypeScriptに少しづつ移行しています。

まだ全部は移行できてないので、Pull Request待ってます!

Notes 📝

Unit of Work

Almin 0.13からUnitOfWorkという内部的なクラスが追加されました。 簡単にいうと通常はUseCaseの実行をそのままStoreに流してくれますが、トランザクション時はUseCaseの実行を止めたり進めたりできるものです。

Context#transactionはこれの上に作られています。

Unit of Work

サンプルコード

サンプルコードにTypeScript + AlminでのTodoMVCの実装を追加しました。

JavaScript版FlowType版などもあります。

個人的にはshopping-cartの方がらしく書けているかなと思います。

触ってみて何かおかしなサンプルがある可能性はあるので、そのときはIssueを立ててください。

もっと実際のアプリケーション的なコードを見たい場合はFaaoなどを見てみてください。 これはプロダクションのレベル感で書いているので、そこそこ複雑です。

おわり

Alminは大抵の人が読んで分かるコードを書けるようにデザインしています。 ありふれたクラスベースにしているのもそうですが、 react-reduxconnect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])みたいにいきなり厳しい感じになったしないように気をつけています。

Reduxはドキュメントも充実してるし、やっぱり基盤的な部分はよくできるてるのでReduxもちゃんと理解した方がいいと思います。ミドルウェアや拡張部分が複雑になったりしてるだけでコアはしっかりしてます。

AlminはReduxやFluxのようなこともできますが、ドメイン層をクライアントサイドどう扱うかという部分に焦点を置いています。 逆をいえば、Alminが扱わない領域(ドメイン/インフラ)と扱う領域(アプリケーションレイヤー/Viewの連携)をはっきり区別するということにつながります。

たとえば、Fluxだとドメイン(ロジック)をどこに置くかを考えると、Storeの中を構造化していくことになることが多いです。

Flux

Storeの中だとドメイン側がライブラリの都合に引っ張られてしまいがちです。(強い意志が必要) なので、Alminは扱う領域をアプリケーションレイヤーのみにして、その他のドメインレイヤーなどはユーザー側で扱う領域としています。

Almin

この構造によってドメインのロジックはPureなJavaScriptとして書けることを期待しています。 この辺については次のスライドで書いています。

実際にある程度の規模感のものもちゃんと書けるようです。

今後の予定としては、トランザクション周りをもう少し考えたいのと、結局ライブラリの中として上手くできていてもプロジェクトの中で上手く動くかは別なのでその辺を上手くナビゲーションできる仕組みを考えたい気がします。

例えば、AlminのUseCase実装からユースケース図を自動生成とかは現実的にできます。このようなプロジェクトが目指すべき構造からずれていないかを可視化できたりチェックできるような仕組みを提供できると、ユーザーは書くことに集中できてモチベーションが維持しやすいのではないかなと思ったりします。

そういう視点のContributeも待ってますし、それとは別にPRが送りやすそうなIssueはGood for beginnersラベルが付けてあります。