LoginSignup
2
0

More than 5 years have passed since last update.

LitElement + actor-helpers でカウンター

Last updated at Posted at 2018-12-02

先日LitElementからReduxを使ってみたのですが、Chrome Dev Summit 2018こちらのセッションでWeb Workerを使った状態管理ライブラリ(PolymerLabs/actor-helpers)の動画があったので、ちょっと触ってみました。
Screenshot from 2018-11-30 23-14-13.png

actor-helpersとは

Screenshot from 2018-11-30 23-16-41.png

  • 歴史ある並列処理を得意としたErlangのようなプロセス間通信モデル(actor model:アクターモデル)であり、なにか新しい概念で作られたライブラリではない
  • 別プロセスをnew Worker('worker.js')でつくり、Actor継承クラスをhookup()で立ち上げ、lookup()で見つけ、send(Message)で通信し、onMessage(Message)で受けとる
  • とっても軽量だけど、現状は正式なGoogleのプロジェクトではなく、npmレジストリにもない。ドキュメントもない(公式サンプルはこちら)

昔必死にErlangを勉強したことがあるので、ちょっと興味深いです。:bulb::bulb::bulb:

actor-modelでカウンター

Preactで書かれたサンプルを参考にばっくりLitElement+Typescriptで書いてみます。リポジトリはこちら

/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   

動作してるサイトはこちら

Screenshot from 2018-12-01 00-24-00.png

各ファイルと流れの説明

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を読み込むだけです。

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 でプロセスを分けます。

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()で立ち上げるだけ。

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に応じて値を更新してメッセージを投げます。

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()が呼ばれるので、プロパティを更新して描画します。

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を投げるだけです。

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

export enum Action {
  INCREMENT,
  DECREMENT
}
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
:arrow_left: 01日目 CakePHPでpugる @kinoleaf
:arrow_right: 03日目 vsixを使ったvscodeのアップデートに負けにくいCSSや拡張機能のカスタマイズ方法 @BigFly

2
0
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
2
0