シンガポールでの生活

ex freeeの@joe_reです。

裏freee developers Advent Calendar 2018の14日目の記事を書きます。

何を書こうか迷ったんですが、freeeをやめた後の近況報告も兼ねて、シンガポールでの生活を中心に書こうと思います。 (退職記事はこちら)

いつから、どういう形態でシンガポールにいるの

今年の10/1から住んでいます。なのでまだ2ヶ月と少しという感じです。

会社としては本社をシンガポールに置いていて、支社が日本にあります。

僕は日本支社から出向している日本人駐在員という形で就労ビザを取っています。

どこに住んでいるの

オーチャードとサマーセットの間に住んでいます。

ショッピングがしたい観光客向けの場所という感じで、日本で言うと銀座みたいな感じです。

これはサマーセット駅の駅ビルの入り口です。中に無数のお店があり、ウィンドウショッピングは無限に無料でできます。

f:id:joe-re:20181214153248p:plain

また、中にドンキホーテ(シンガポールでは商標権の問題でドンドンドンキに名前が変わっている)やTokyu handsがあります。 ドンキホーテは24時間空いていてクッソ便利です。値段も日本とそんなに変わりません。お酒以外は。 僕は行く度にここでカップラーメンを5,6個買って帰るルーチンに陥っています。

近くに高島屋伊勢丹などもあります。銀座ですね。

ブランドにあまり興味がないので特に何か買い物した記憶はありません。

今はクリスマスシーズンなのもあって、クリスマスイルミネーションでライトアップされていたり、夜な夜なライブやってたりして、週末の夜はめっちゃ賑わってます。 f:id:joe-re:20181214154742p:plain f:id:joe-re:20181214154804p:plain

物価

純粋な数値だけを見ると、シンガポールは物価の高い国ですが、住んでみると高くは感じないです。 もちろん日本も物価の高い国なので、他の国と比べるとまた違うとは思いますが。

この記事によると、東京が3位でシンガポールが5位だそうです。

物価の高い都市のベスト10にアジアが5都市、日本は何位? |ビジネス+IT

ただし、高いものと安いものははっきりと日本と違います。

交通機関は安い

シンガポールに来てまず驚いたのは、交通機関の安さです。 電車とバスなら、2.5シンガポールドル(今のレートでだいたい200円ぐらい)でほとんどの場所に行けます。 特にバスはクッソ安い。

タクシーも日本に比べるとめちゃめちゃ安いです。

僕の住んでいるところから空港まではだいたい20キロぐらいなのですが、時間帯にもよりますがだいたい20S$(1,600円)ぐらいで行けます。

シンガポールで今主要なライドシェアリングサービス(Uberみたいなやつ)はGrabです。 使い勝手が良くてポチるだけですぐに迎えに来てくれて、今のところトラブルはありません。

シンガポールに来た時は入れておくと便利です。

嗜好品は高い

反対に高いのは、酒などの嗜好品です。

もちろん税金が高いのと、これらは基本的に輸入品なのがあって、下手すると日本の3倍ぐらいしている場合もあります。 缶ビールでも1.5〜2倍ぐらいなイメージです。

生活に必要な交通機関は安く、反対に嗜好品が高いのはシンガポールの合理的な部分を強く反映している部分だと思います。

酒が高いことに起因する1つのクソみたいなエピソードがあるのですが、思い返してみたら本当にクソみたいな話だったし面白い気もしなかったので割愛します。 酒の肴にでもしますので飲みに行きましょう。

食事はものによる

食事は、贅沢しようと思えば高いし、安く済ませようと思えばいくらでも安く済ませられます。

観光客向けの食事や日本食は高いですが、ローカルフードならだいたい5S$〜8S$ぐらいで満足できます。

東京に住んでいる人からすれば逆に安いのではないでしょうか。

シンガポールには、ローカルの人のメジャーな食事処としてホーカー(hawker)と呼ばれる屋台形式のお店が集まった場所がたくさんあります。 活気のある場所が多く、競争があるのでハズレのお店は少ないと思います。 ローカルフードを安価に楽しみたいならすごくおすすめです。

写真はセラングーンのhawkerで頼んだお粥料理です。 f:id:joe-re:20181214172534p:plain

家賃は高い

家賃に関して具体的な金額までは詳しく知りませんが、基本的には高いと思います。 こちらでは結婚して子供ができるまでは、ルームシェアする人が多いみたいです。 僕は場所の関係もあってとても一人で払える金額ではないので、会社の補助 + 同僚と3人でルームシェアという形でコンドミニアムに住んでいます。

住居

前述の通り、同僚と3人でルームシェアをしています。間取り的には多分3LDKって感じです。

これはリビング f:id:joe-re:20181214173933p:plain

キッチン f:id:joe-re:20181214174334p:plain

部屋 f:id:joe-re:20181214173959p:plain

10Fに住んでいます。眺めはこんな感じです。 f:id:joe-re:20181214174417p:plain

共有部にはBBQスペースやプールもあります。プールには一度も入っていません。入りたいとは思っています。 f:id:joe-re:20181214174705p:plain

f:id:joe-re:20181214174729p:plain

既婚者は僕だけで、奥さんがたまに来るので、ありがたいことに一番広い部屋をもらっています。

1人や2人なら余裕で泊められるので、もしシンガポールで旅費を安く抑えたいなどの気持ちがある方はお声がけください。

食事

辛いものが多いですが、そればっかりってわけではないです。むしろ辛くないものの方が多分多いです。 ただ辛いものが好きな人は、何にでもチリソースをかけます。

配慮はされていて、勝手にチリソースかけられて出されるなんてことはあんまりと思います。 お店で何か頼んだ時にドゥーユーウォントチリ?みたいな感じで聞かれると思うので、辛いものが苦手な人はノーと言える日本人になりましょう。僕は辛いもの苦手なのに結構イェスって言ってます。

僕が最高に好きな朝食メニューにカヤトーストというものがあります。 f:id:joe-re:20181214175706p:plain

これはトーストにカヤジャムという、ココナッツミルクと卵とはちみつなどなどを混ぜ合わせた結構甘いジャムを挟んで、日本で言う所の温泉卵をかけつつ食べるみたいなメニューです。

言葉にすると全く美味しさが伝わらないと思いますが、僕がこれが好きすぎています。 家の近くにはヤクーンカヤトーストというお店とトーストボックスというお店があり、これがカヤトーストを食べられるお店としては(多分)有名です。4.5S$とかだったと思います。

非常に美味しいのでおすすめなのですが、これをローカルの友達に話した時には「俺のよく行く喫茶店なら2.5S$で食べられるし、なんなら自分で作るよ」と言われました。

まぁ多分日本で言う所のスターバックスみたいな感覚なんだと思います。自信はないです。

あまり日本で馴染みのない料理として、エイ(スティングレイ)があります。写真は撮り忘れました。 食べる前に想像していたような臭みはなくて、さらに思ったよりも歯ごたえがあって美味しかったです。

が、一緒に提供されていたチリソースは地獄のような辛さでした。

あとは土鍋(クレイポッド)料理もローカルなメジャー料理です。クッソうまいです。 f:id:joe-re:20181214182337p:plain

あと有名どころであるところのチキンライスは当然美味しいです。僕は蒸してあるパターンを好みます。 f:id:joe-re:20181214182535p:plain

交流

シンガポールにいてというよりは、海外に住んでみてということになると思いますが、趣味の大切さを特に感じました。

最初は日本からきたばっかりなので当然遊びに行く友達もおらず、一日中家にいることがほとんどでしたが、最近は趣味を通じて交流することが増えてきました。

僕の場合は、音楽(一応これでもギターをやってたりします)とボードゲームが主な趣味です。

もちろん現地の人と交流しようと思うと言語の壁はありますが(そして僕にとってそれは決して低くないですが)、共通の趣味や同じように興味を持っていることがあると格段にその敷居は下がるのを感じます。

勉強して言葉を喋れるようになることも大事ですが、それ以上に、自分が何に興味を持ち、何が好きで、何が嫌いで、何をしたいか、みたいな部分をしっかり持つのが大事だと思います。 音楽やってて良かったです。

話は変わって、シンガポールのカラオケに基本的に日本の歌はないですが場所を選べばあります。シンガポリアンで日本に興味がある人は日本のアニメをたくさんみている人が多いです。

f:id:joe-re:20181214185011p:plain

あと日本語バッキバキにできるシンガポール人と出会うと刺激になります。なんでこいつシンガポールにいてこんなに日本語喋れるんや。

仕事

長くなったので割愛します

終わり

年末、12/20〜12/30ぐらいまで日本にいます。飲みにでも行きましょう。お土産あげます。お願いします。

シンガポールに来る予定がある人もぜひお声がけください、案内します。

明日はmacha3162さんです。

kubernetesをローカルの開発に活用する

できる限りプロダクションのクラスタ設定をそのままローカルの開発にも使いたいなー、と思って色々と試行錯誤して、ようやく形になってきたので書いておく。

なぜローカルでkubernetesを動かしたいのか

最近ではInfrastructure as Code、Immutable Infrastructureの考え方と共に、コンテナの上でアプリケーションの環境の構築、運用、開発をすることが増えてきた。

少し前までは、Dockerでローカルの開発環境の構築は楽になったけど、本番にデプロイするのにはハードルがある印象が個人的にはあった。だけど、kubernetesの登場によってそのハードルは大きく下がった。

最近はマイクロサービスアーキテクチャへの注目と共に、様々なコンテナが協調してサービスを形作る構成が増えてきたように思う。kubernetesはこの全てのコンテナを管理する。

kubernetesは最初に学ばなければならないことは多いけれど、一度その概念を掴めばどれだけ楽に構成を管理できるか実感できる。

2年前ぐらい前のものだけど、全体の概念を掴むのはこの動画が良かった。

www.youtube.com

全ての設定はyamlで記述することになるので、構成管理がコード化できるところも良い。(ともすればyaml地獄とも揶揄されるけど)

複数のプロセスを立ち上げて開発をしている場合には、各プロセスを協調させるために、ローカル専用のプロキシサーバを立てるケースもある。

しかし各コンテナのインターナルな通信や、環境変数の設定をkubernetesに任せていると、これがローカルでそのまま動けばプロキシサーバなどいらずにプロダクションと同等の動作をさせることができるのでは、という気持ちになった。

ローカル開発とプロダクションとの差異を解決するためのkustomize

なるべくプロダクションの構成をそのまま使いたいとはいえ、どうしても差異はある。

そもそもプロダクションとローカルではデプロイするイメージは確実に違うし、Webアプリケーション開発においてはクライアントサイドのjsやhtmlなどの静的なファイルをビルドするためのサーバを立てなければいけなかったりもする。

この差異を解決するのには kustomize というツールを使うことにした。

kustomizeにより、環境ごとの差分のみを記述する

kustomizeを用いると、プロダクションの設定とローカルの設定は差分のみを記述するだけで済むようになる。 kustomizeが用意する基本的なレイヤーは、ベースとオーバレイの2つである。

各サービスの環境(プロダクション、ステージング、ローカルなど)ごとの設定は差分のみを記述した上でオーバレイのレイヤーに配置しておき、適用の際にベースの設定にパッチを当てる。

具体的には各環境にkustomization.yamlというファイルを用意し、そこがエントリポイントになる。

kustomizeのリポジトリのファイル配置の例を転記する。

~/someApp
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── development
    │   ├── cpu_count.yaml
    │   ├── kustomization.yaml
    │   └── replica_count.yaml
    └── production
        ├── cpu_count.yaml
        ├── kustomization.yaml
        └── replica_count.yaml

例えば、overlays/development/kustomization.yamlの内容は以下のようになるだろう。

namespace: dev-someApp
commonLabels:
  stage: development
bases:
  - ../base
resources:
  - cpu_count.yaml
  - replica_count.yaml

basesセクションに記述したものはdevelopment, productionの両方から参照され、その差分のあるファイルは各オーバレイのレイヤーにおいておく。 この例ではファイルごと変えているが、ファイルの中で部分的に差分を適用したい場合には、patchesというセクションを使えば可能だ。

patches:
  - patches/deployment.yaml

環境変数の管理

kubernetesを用いて環境変数を管理するには、SecretsとConfigMapを用いるのが一般的だ。 基本的に機密性の高い情報の管理にはSecrets、それ以外ではConfigMapを使う。

kubernetesを使うとリリースごとにリビジョンをつけることができるので、問題がおきた時にロールバックが容易になる。

しかし、リリースごとに環境変数を変えている場合にはこれもロールバックしなければいけないことになり、直接SecretsやConfigMapをいじってしまうと少し厄介になる。

これを解決するために、kustomizeにより作成されたSecret、ConfigMapにはそれぞれリソースに一意な名前となるようにサフィックスが自動で付与され、それを参照することになる。

つまり、デプロイするたびにSecretsやConfigMapは増えていく。

試しに見てみる。

$ kubectl get secrets -n dev-application
NAME                             TYPE                                  DATA      AGE
default-token-5lctz              kubernetes.io/service-account-token   3         1d
secrets-environment-4khckhf9c7   Opaque                                27        5h
secrets-environment-9hg6tc462d   Opaque                                29        3h
secrets-environment-bb4k2t7cd9   Opaque                                26        5h

kustomizeを利用しない場合にこの仕組みを実現する方法は、@yuyatさんの以下の記事が大変参考になった。

Kubernetes の ConfigMap を Immutable に管理する | Born Too Late

また、secretsGenerator、configMapGeneratorという仕組みも用意されていて、ここにはリテラルだけでなく、コマンドの出力結果も適用することができる。

僕は環境変数AWS SystemManagerのParameterStore(ssm)で管理しているので、以下のように記述している。(get_env_from_ssmはaws-cliをラップして、ssmから値を取り出して標準出力するだけの簡単なシェルスクリプトです。)

secretGenerator:
- name: server-environment
  commands:
    BAR_API_KEY: "../../scripts/get_env_from_ssm /dev/BAR_API_KEY"
    FOO_SECRET: "../../scripts/get_env_from_ssm /dev/FOO_SECRET"
  type: Opaque

ローカルで利用するソースコードのチェックアウトとイメージのビルド

ローカル環境のソースコードのパスは開発者ごとに違うのが普通だけど、後述のソースコードのコンテナへのマウントや、イメージの一括のビルドスクリプトを簡易にするため、今回は決められたパスに関連ソースコードをチェックアウトし、イメージをビルドするスクリプトを用意した。Dockerfileは各リポジトリに置いている。

工夫すればソースコードのパスは可変にはできると思う。けど結構苦労すると思う。

具体的には以下のようなスクリプトになった。

  • scripts/checkout_all
#! /bin/bash

mkdir -p ${HOME}/awesomeApps

echo "local setup start..."

if [ -d "${HOME}/awesomeApps/bar" ]; then
  echo "bar already exists."
else
  # git clone ...
fi

if [ -d "${HOME}/awesomeApps/foo" ]; then
  echo "foo already exists."
else
  # git clone
fi

# ...
# ...

echo "finished."
  • scripts/build_all_images
#! /bin/bash

docker build -t foo:dev ${HOME}/awesomeApps/foo
docker build -t bar:dev ${HOME}/awesomeApps/bar
# ...
# ...

開発用のbuildのイメージのタグをバージョン管理せずにdevのみで作っていると、Dockerfileに修正が発生した時に更新が少し面倒になる。

なので開発用のタグもバージョン管理した方が良いかもしれないけど、ローカルのソースコードのマウントのところで後述する通り、kubernetesyamlプログラマブルにするのは少し面倒なので、複雑性とのトレードオフになる。

人数が多くて変更が発生した際に周知が大変な場合には、バージョン管理する価値はあると思う。

ローカルのソースコードkubernetes上のコンテナにマウントする

kubernetesにはvolumesにhostPathを指定することができる。

Volumes - Kubernetes

これを用いればホストのソースコードをマウントすることは可能だが、ここは絶対パスで指定する必要があることに注意が必要だ。

さらにkubernetesはDeclarativeであることを崩さないため、templateの機能は提供されておらず、ここでhostの環境変数で設定を置き換えることはできない。

Feature: Support using environment variables inside deployment yaml file · Issue #52787 · kubernetes/kubernetes · GitHub

そこで、置き換えにはenvsubstを用いることにした。 具体的には以下のようなコマンドでデプロイすることになる。

kustomize build ./someApp/overlays/development/kustomization.yaml build | envsubst | kubectl apply -f -

別にenvsubstでなくても、置き換えられればいいだけなのでsedでも良い。 ここで1ステップ追加されてしまうのは僕の調べた限りでは回避できなかった。 具体的なDeploymentのファイルは以下のようになる。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    service: server
  name: server
spec:
  template:
    spec:
      volumes:
      - name: src
        hostPath:
          path: "${HOME}/awesomeApps/bar"
          type: DirectoryOrCreate
      containers:
      - name: server
        args:
        - yarn
        - run
        - dev
        image: bar:dev
        volumeMounts:
        - mountPath: /opt/app
          name: src
        workingDir: /opt/app

Macではminikubeではなくdocker-for-mackubernetesを使う

windowsの人はdocker-for-windowsになる。linuxの人はminikubeを使ったとしても、driverにVirtualBoxを使わなければ良い。

Macユーザなので試した訳ではないけど、以下の議論がある。

More documentation around vm-driver=none for local use · Issue #2575 · kubernetes/minikube · GitHub

MacでもVirtualBoxを使わなければいけるのかもしれない。

docsをみると、VirtualBoxの他にVMware Fusion, HyperKitの選択肢があるようだ。

Install Minikube - Kubernetes

VirtualBoxを用いると、ファイルシステムの違いにより、inotifyイベントを検知できないためファイルの変更の監視が難しい。 ファイルの変更を検知してビルドの必要がある場合などに、これは致命的だ。

github.com

docker-for-macは体感として少し遅いが、これは今後改善されていくと信じたい。

おわり

まだ構築したばっかりなので、これから問題は出るかもしれない。 とりあえず今の所は便利に開発することができている。 今回紹介していないところで、細かいtips的なものもあるので、またそれは今度書こうと思う。

Socket.IOとのリアルタイムコミュニケーションをVuexのmodulesを利用してハンドリングする

という趣旨の内容で、先週のVue.js Tokyo v-meetupでLTさせてもらった。

vuejs-meetup.connpass.com

発表資料はこちら

speakerdeck.com

背景

僕が今メインで担当しているプロダクトの性質上、ブラウザ上でのリアルタイムコミュニケーションが必要な機能を扱うことが多い。

そこでは、Socket.IOと一部でGraphQLのsubscriptionを主に使っている。

GraphQLのクライアントにはapolloを使っているので、何も考えずともある程度体制の整った(まぁこうなるよねという感じの)コードになるけど、Socket.IOとのコミュニケーションについては、タイムライン上に発生したイベントをいかにして可読性も担保しつつ、Viewで購読可能な状態のStateに落とすか、という所に課題があった。

ContainerComponentでObservableを利用するか、もしくはreduxで言う所のmiddlewareみたいな層を設けてそこに変換を任せるか、というような選択肢も検討したけど、最終的にはVuexのmutation、actionにそのままSocket.IOのイベントをマッピングして、イベントの発生をトリガーにしてVuexの持つStateを構築することにした。

今の所は可読性を保ったまま、スケールしやすい構成にできている実感がある。なのでより詳細な内容をブログにも書いておこうと思った。

利用しているSocket.IOの機能について

Socket.IOは(利用可能な環境において)WebSocketプロトコルを使って双方向通信を実現する。

提供されている機能にNamespacesとRoomsというものがある。

Socket.IO — Rooms and Namespaces

Namespacesを利用することで、利用するストリーム毎に異なるエンドポイントを得ることができる。 Roomsはその中で、さらに利用者のグループを作ることができる。

利用イメージを雑に書くと以下のような感じ。

f:id:joe-re:20180530075415p:plain

Roomはただ単にグループを定義するだけなので、必ずしも1つのクライアントが1つのルームにしか所属できない訳ではない。構造を整えれば、さらに小さい単位のグループを作ることもできる。

Namespaceは僕たちは機能単位で分けている。

これらを利用することで以下の利点を得られる。

  • イベントを送出する対象を簡単にサーバ側で選択することができる
  • 1つの機能(Namespace)に重大なエラーが発生したとしても、他の機能には影響がない
  • Socket.IOサーバのコードは自然と構造化するのでメンテナンスがしやすい

VuexのNamespaced moduleをSocket.IOのNamespaceに割り当てる

Vuexにはmodulesという機能があり、SPAを構成するSingleStateTreeを小さい単位で分割することができる。

Vuex | Modules

それぞれのmoduleの中にそれぞれのstateを構成するためのaction、mutation、getterを持つ。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

ここにもNamespacingという機能があり、これを有効にしている場合にはmoduleのaction、mutation、getterへのアクセスする際のパスにmodule名が付与される。

const moduleA = {
  namespace: true,
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  namespace: true,
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

// --- View --- //

Vue.extend({
  computed: {
    getterFromModuleA() {
      return this.$store.getters['moduleA/fooGetter']
    },
    getterFromModuleB() {
      return this.$store.getters['moduleB/fooGetter']
    },
  },
  methods: {
    dispatchToModuleA() {
      this.$store.dispatch('moduleA/fooAction')
    },
    dispatchToModuleB() {
      this.$store.dispatch('moduleB/fooAction')
    }
  }
})

Namespacingを利用しない場合には分割されるのはStateだけなのでactionとgetterにおいて名前の衝突を気にしないといけないが、これを有効にすることでその心配はなくなる。

今回はVuexのNamespaced moduleにSocket.IOのNamespaceを割り当てることにした。 そのためにvuex-socketio-pluginという、Vuexのプラグインを作った。

github.com

Vue-Socket.ioというプラグインがあり、もともとはこれを利用しようと思っていたんだけど、マルチチャンネルをサポートしていなかったのとSocket.IOのNamespaceをVuexのNamespaced moduleに割り当てる着想があったので自作することにした。このプラグインのVuex integrationの部分にIFはかなり影響を受けている。

vuex-socketio-pluginの提供するcreateSocketioPluginという関数を利用し、Vuexのpluginを作成できる。

const plugin = createSocketioPlugin([
  'http://localhost:3000/moduleA',
  'http://localhost:3000/moduleB'
])
const store = new Vuex.Store({
  plugins: [plugin],
  modules: { moduleA, moduleB }
})

このプラグインを使うと、Socket.IOの/moduleAというNamespaceはmoduleAに、/moduleBというNamespaceはmoduleBに割り当てられ、そのモジュールの中でイベントを通じたコミュニケーションが利用できるようになる。

Action、MutationをSocket.IOのイベントにマッピングする

vuex-socketio-pluginで作成したpluginを通じてSocket.IOサーバからイベントを受信すると、Namespaceがあれば同名のVuexのNamespaced moduleのActionとMutationが発火される。 Action名にはsocket_、Mutation名にはSOCKET_のプレフィックスがそれぞれデフォルト付与され、その後ろはイベント名となる。(プレフィックスはオプションで変更可能)

const moduleA = {
  namespace: true,
  state: { ... },
  mutations: {
    SOCKET_CONNECT (state, payload) {
      //...
    },
    SOCKET_EVENT1 (state, payload) {
      //...
    }
  },
  actions: {
    socket_connect (context, payload) {
      //...
    },
    socket_EVENT1 (context, payload) {
      //...
    }
  }
}

mutationのCONNECTは大文字で、actionのconnectは小文字なのはこれがSocket.IOが用意しているイベントだからで、ユーザ定義ではないイベントであればmutationは大文字にactionは小文字にすることにしている。(設定で変更できるか、どちらであっても発火できるようにする方が親切なのかもしれない。)

利用できる標準のイベントはSocket.IOのdocsを参照。 Socket.IO — Client API

VuexのNamespaced moduleを利用していない場合のため、globalで以下のような命名規則をしてもイベントを受け取れるようにしている。

  • Mutation: SOCKET_moduleA_EVENT1
  • Action: socket_moduleA_EVENT1

これによって/moduleAというnamespaceのストリームはmoduleA内でハンドリングを完結させることができる。

僕たちはSocket.IOのNamespaceは機能単位になるように設計しているので、この仕組みによってリアルタイムコミュニケーションによるStateの変更について非常に見通しの良いコードにすることができた。

さらにSocket.IOのイベントハンドリングを子のmoduleに分割する

しばらくこの構成で運用していたのだけど、機能を拡張していくにつれて可読性の部分に問題が生じた。

Stateが変更される要因はSocket.IOのイベントのみではないし、ユーザの操作をトリガーにしてSocket.IOサーバにイベントを送ることももちろんある。

そうするとNamespaced moduleにSocket.IOのイベントのハンドリングとそれ以外が混在することになる。

const moduleA = {
  namespace: true,
  state: { ... },
  mutations: {
    SOCKET_CONNECT (state, payload) {
      //...
    },
    SOCKET_EVENT1 (state, payload) {
      //...
    },
    FOO_MUTATION (state, payload) {
       //...
    },
    BAR_MUTATION (state, payload) {
       //...
    },
    FOO_BAR_MUTATION (state, payload) {
       //...
    }
  },
  actions: {
    socket_connect (context, payload) {
      //...
    },
    socket_EVENT1 (context, payload) {
      //...
    },
    fooAction (context, payload) {
      //...
    },
    barAction (context, payload) {
      //...
    },
    fooBarAction (context, payload) {
      //...
    },

  }
}

機能がシンプルなうちはあまり問題にならなかったが、これが増えると非常に煩雑で可読性が失われていった。

Vuexのmoduleは多重にネストすることができるので、Socket.IOのイベントハンドリングは更に子のモジュールに落とすことでこれを解決した。

Socket.IOのイベントに付与されるプレフィックスは変更することができるようにしているので、これを子のモジュール名とすることでこれを実現できる。

const plugin = createSocketioPlugin([
  'http://localhost:3000/moduleA',
  'http://localhost:3000/moduleB'
], {
  actionPrefix: 'socket/',
  mutationPrefix: 'socket/'
})

const store = new Vuex.Store({
  plugins: [plugin],
  modules: { moduleA, moduleB }
})

この場合にはmoduleAは以下のようになる。

const socket = {
  namespace: true,
  state: { ... },
  mutations: {
    CONNECT (state, payload) {
      //...
    },
    EVENT1 (state, payload) {
      //...
    }
  },
  actions: {
    connect (context, payload) {
      //...
    },
    EVENT1 (context, payload) {
      //...
    }
  }
}

const moduleA = {
  namespace: true,
  modules: { socket },
  state: { ... },
  mutations: {
    FOO_MUTATION (state, payload) {
       //...
    },
    BAR_MUTATION (state, payload) {
       //...
    },
    FOO_BAR_MUTATION (state, payload) {
       //...
    }
  },
  actions: {
    fooAction (context, payload) {
      //...
    },
    barAction (context, payload) {
      //...
    },
    fooBarAction (context, payload) {
      //...
    },

  }
}

これによって一目でSocket.IOのイベントとそれ以外とを区別できるようになり、可読性を取り戻すことができた。

この場合には親(moduleA)にViewに通知するStateを持ち、子(moduleA/socket)はほぼState-lessとなる。(Socket.IOとのコネクションの有無などは子に持つけど)

そうすると子のmoduleから親のmoduleのstateを更新する必要があり、アンチパターンっぽくて少し気持ち悪い。 mutationを発行する際にも{ root: true } のオプションが必要になり少し面倒になる。

  actions: {
    EVENT1 (context, payload) {
      context.commit('moduleA/FOO_MUTATION', { root: true })
    }
  }

これは仕方ないトレードオフだし、局所的にここでしか発生しないので目を瞑ることにしている。(良い解決法があるなら知りたい)

おわり

もしこれからVueアプリケーションでSocket.IOのNamespaceを利用しようとしている方は、試してみていただけると嬉しいです。 今のところは自分のアプリケーションに必要な部分しか実装していないので、色々な意見をいただけると非常にありがたいです。

もちろん他にもやり方はたくさんあると思いますので、もしより良い方法や上手くいっている方法などをお知りの方はぜひ教えてください。色々語り合いたいです。