LitElement + actor-helpers でカウンター


[先日](https://qiita.com/howking/items/e07523a9ef0ce2817e35)LitElementからReduxを使ってみたのですが、[Chrome Dev Summit 2018](https://developer.chrome.com/devsummit/)の[こちら](https://www.youtube.com/watch?v=Vg60lf92EkM)のセッションで[Web Worker](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers)を使った状態管理ライブラリ([PolymerLabs/actor-helpers](https://github.com/PolymerLabs/actor-helpers))の動画があったので、ちょっと触ってみました。
![Screenshot from 2018-11-30 23-14-13.png](https://qiita-image-store.s3.amazonaws.com/0/90678/5c02ca7b-2b0c-1fb7-e03d-1fcde9ff690e.png)

## actor-helpersとは

![Screenshot from 2018-11-30 23-16-41.png](https://qiita-image-store.s3.amazonaws.com/0/90678/1e77bab3-930d-821b-22d1-d0f130c837f0.png)

- 歴史ある並列処理を得意とした[Erlang](https://ja.wikipedia.org/wiki/Erlang)のようなプロセス間通信モデル([actor model:アクターモデル](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%AF%E3%82%BF%E3%83%BC%E3%83%A2%E3%83%87%E3%83%AB))であり、なにか新しい概念で作られたライブラリではない
- 別プロセスを`new Worker('worker.js')`でつくり、`Actor`継承クラスを`hookup()`で立ち上げ、`lookup()`で見つけ、`send(Message)`で通信し、`onMessage(Message)`で受けとる
- とっても軽量だけど、現状は正式なGoogleのプロジェクトではなく、npmレジストリにもない。ドキュメントもない(公式サンプルは[こちら](https://github.com/polymerlabs/actor-boilerplate))
 
昔必死にErlangを勉強したことがあるので、ちょっと興味深いです。:bulb::bulb::bulb:

## actor-modelでカウンター

[Preact](https://preactjs.com/)で書かれた[サンプル](https://github.com/PolymerLabs/actor-boilerplate/tree/example/todo)を参考にばっくり[LitElement](https://lit-element.polymer-project.org/)+Typescriptで書いてみます。リポジトリは[こちら](https://github.com/howking/actor-counter)。

```js:/actor-counter
|-- README.md
|-- index.html
|-- package-lock.json
|-- package.json
|-- rollup.config.js
|-- tsconfig.json
|-- src // ソースフォルダ
|   |-- actions.ts
|   |-- bootstrap.ts
|   |-- my-button.ts
|   |-- my-view.ts
|   |-- state-actor.ts
|   |-- states.ts
|   `-- worker.ts
`-- dist // 公開フォルダ
    |-- bootstrap.js
    |-- index.html  
    `-- worker.js   
```

動作してるサイトは[こちら](https://howking.github.io/actor-counter/dist/)。

![Screenshot from 2018-12-01 00-24-00.png](https://qiita-image-store.s3.amazonaws.com/0/90678/7d14d23f-e0ba-aea7-ad99-65b1dc4c5d00.png)

### 各ファイルと流れの説明

```html:index.html
<!doctype html>
<my-view></my-view>
<my-button></my-button>
<script src="bootstrap.js"></script>
```

index.htmlはmy-viewとmy-buttonのWebコンポーネントを表示し、bootstrap.jsを読み込むだけです。

```ts:src/bootstrap.ts
import { initializeQueues } from 'actor-helpers/src/actor/Actor.js'
import './my-view.js'
import './my-button.js'

(async () => {
  await initializeQueues()
  new Worker('worker.js')
})()
```

bootstrap.jsでは initializeQueues()で初期化処理をしてから早速 worker.js でプロセスを分けます。

```ts:src/worker.ts
import { hookup } from 'actor-helpers/src/actor/Actor.js'
import StateActor from './state-actor.js'

hookup('state', new StateActor())
```

worker.jsでは状態管理用のアクター(StateActor)を`hookup()`で立ち上げるだけ。

```ts:src/state-actor.ts
import { lookup, Actor } from 'actor-helpers/src/actor/Actor.js'
import { Action } from './actions.js'
import { State, initialState } from './states.js'

export default class StateActor extends Actor<Action> {

  private ui = lookup('ui')

  private state:State = initialState

  async init(){
    this.ui.send(this.state)
  }
  
  onMessage(msg: Action) {
    switch (msg) {
      case Action.INCREMENT:
        this.state.counter += 1
        break
      case Action.DECREMENT:
        this.state.counter -= 1
        break
    }
    this.state.clicks += 1
    this.ui.send(this.state)
  }
}
```

StateActorは`lookup()`でUIのアクターを見つけつつ、stateの初期化処理や、Actionに応じて値を更新してメッセージを投げます。

```ts:src/my-view.ts
import { LitElement, html, property, customElement } from '@polymer/lit-element'
import { hookup, actorMixin } from 'actor-helpers/src/actor/Actor.js'
import { State } from './states.js'

@customElement('my-view' as any)
export class MyView extends actorMixin(LitElement) {

  @property()
  counter? :number

  @property()
  clicks? :number

  async init() {
    hookup('ui', this)
  }

  onMessage({counter, clicks}:State) {
    this.counter = counter
    this.clicks = clicks
  }

  render(){
    return html`<h2>Counter: ${this.counter}</h2><h4>clicks: ${this.clicks}</h4>`
  }
  
}
```

HTMLElementをActorにするのは**無理がある**( :construction: 実際に別ウィンドウを出した時の挙動はおかしい )かもですが、いちおうMixinを使って起動時に自分自身が`ui`だとして`hookup()`して動かします。
StateActorからメッセージが飛んできたら`onMessage()`が呼ばれるので、プロパティを更新して描画します。

```ts:src/my-button.ts
import { LitElement, html, property, customElement } from '@polymer/lit-element'
import { lookup } from 'actor-helpers/src/actor/Actor.js'
import { Action } from './actions.js'

@customElement('my-button' as any)
export class MyButton extends LitElement {

  @property()
  private state = lookup('state')

  render(){
    return html`
      <button @click=${()=>this.state.send(Action.INCREMENT)}>Increment</button>
      <button @click=${()=>this.state.send(Action.DECREMENT)}>Decrement</button>
    `
  }
}
```

ボタンを表示するエレメントはStateActorを`lookup()`してからActionを投げるだけです。

```ts:src/actions.ts
declare global {
  interface ActorMessageType {
    state: Action
  }
}

export enum Action {
  INCREMENT,
  DECREMENT
}
```

```ts:src/states.ts
declare global {
  interface ActorMessageType {
    ui: State
  }
}

export interface State {
  counter: number
  clicks: number
}

export const initialState = { counter: 0, clicks: 0 }
```

ActionやStateの定義。Actorがやりとりするメッセージの対応関係は`ActorMessageType`にグローバル宣言しておく必要があります。

Reduxのように使えそうなので、是非開発が進んでほしいところです。:pray: :pray: :pray:

[:christmas_tree: FORK Advent Calendar 2018](/advent-calendar/2018/fork)
:arrow_left: 01日目 [CakePHPでpugる](https://qiita.com/kinoleaf/items/ff020e23a09a678473f0) @kinoleaf 
:arrow_right: 03日目 [vsixを使ったvscodeのアップデートに負けにくいCSSや拡張機能のカスタマイズ方法](https://qiita.com/BigFly/items/495994f0443d85eadb7d) @BigFly