Vue/Mobx + Elixir/Phoenix で匿名SNS「つぶやきや」を作ってみた

# 作ったもの

https://tubuyakiya.gigalixirapp.com/

![スクリーンショット 2019-01-13 21.23.16.png](https://qiita-image-store.s3.amazonaws.com/0/55219/6c44c767-bdcb-7ed8-9e4e-cdfffa9243e6.png)

みんな大好き[いらすとや](https://www.irasutoya.com/)さんの人物いらすとを使って、
Twitter風につぶやけるサービスです :sparkles: 

アバターは「変更」ボタンを押すとランダムで変わります(10種類ぐらいしかないです・・・)

WebSocketを使っており、

|他の人のつぶやきがリアルタイムで表示されたり|Likeされていくと、アニメーション付きで数字がリアルタイムで増えていきます|
|:--:|:--:|
|![gamen2.gif](https://qiita-image-store.s3.amazonaws.com/0/55219/ef7491b6-f25d-f32c-aef9-73b1c518bba7.gif)|![gamen1.gif](https://qiita-image-store.s3.amazonaws.com/0/55219/fa81e7aa-2b22-9181-0e3e-b6014f9c60ad.gif)|

# 使った技術

**フロントエンド**

- Vue.js (Vue CLI 3)
- Mobx
- rxjs
- [bulma](https://bulma.io/)

**バックエンド**

- Elixir/Phoenix

**ホスティング**

- gigalixir

参考: [【Gigalixir編①】Elixir／Phoenix本番リリース： 初期PJリリースまで
](https://qiita.com/piacere_ex/items/1a9cbcc740ca3707eaec)

## プロジェクト構成

Vue, Phoenixそれぞれ分けてつくるのではなく、Phoenixの中にVueプロジェクトを作成する構成で
今回は作りました

```
$ mix phx.new hogehoge
$ cd hogehoge
$ rm -rf assets
$ vue create asssets
```

`vue.config.js` を作成して、 vueのビルド先をphoenixのstaticフォルダに向けます

```js:assets/vue.config.js
module.exports = {
  outputDir: '../priv/static/js',
  assetsDir: '../',
  filenameHashing: false
}
```

あとは `app.html.eex` に `<div id="app"></div>` を追加すればOKです。
開発のときは `mix phx.server` と `yarn build --watch` をそれぞれ別窓で実行すれば
いい感じに自動更新も走るので開発しやすかったです。

## 状態管理

フロントエンドでは状態管理のライブラリとしてMobxを使用しています。
特に深い理由はなく、Mobxがどんなものか勉強してみたかったので。

状態管理のライブラリが欲しくなる場面として、複数コンポーネント感での
同じ状態をいじりたいというのがあると思ってますが、
今回は以下のように１番親のコンポーネントで初期化して、
子コンポーネントに渡しています

```vue
<template lang="pug">
  .section
    .board
      .input-area
        InputArea(:vm = "vm")
      transition-group(name="list-complete", tag="div")
        .media-wrapper(v-for="message in vm.messages", :key="message.id", class="list-complete-item")
          MediaObject(:vm="vm", :message="message")
    AvatarSelector
</template>

<script lang="ts">
...

@Observer
@Component({
  components: {
    MediaObject,
    InputArea,
    AvatarSelector,
  },
})
export default class Home extends Vue {
  private vm = new MessageViewModel()

  ...
}
</script>
```

これが正解なのかどうなのかはよくわかりません。。。
`main.ts`で初期化して `vue`オブジェクトにぶちこむ方がいいのかなって
思ったりもします。

mobx自体はものすごくシンプルなので、個人開発レベルでは気軽に導入できていいなって感じました。
vuexはいろいろと用意するものが多く、大変なイメージがあるので。。。

## WebSocketへの接続

WebSocketへの接続はPhoenix.jsを使ってやっています。
Phoenix使うとWebSocektまわりがすごい簡単で、感動しますね :sparkles:

```ts:viewmodel.ts
  @observable messages: Message[] = []
  private socket = new Socket('/socket')
  private channel: Channel | undefined = undefined

  private shoutSubject = new Subject<Message>()
  private likeSubject = new Subject<Message>()
  private likeAnimationSubject = new Subject<Message>()

  constructor() {
    this.socket.connect()
  }

  private channelObs() {
    return new Observable<Channel>(observer => {
      const channel = this.socket.channel('room:lobby')
      channel.join()
        .receive('ok', resp => {observer.next(channel)})
        .receive('error', resp => {
          console.error(resp)
        })
    })
  }

  private joinRoom() {
    this.channelObs()
      .subscribe(channel => {
        channel.on('shout', res => { this.shoutSubject.next(res) })
        channel.on('like', res => { this.likeSubject.next(res) })

        this.channel = channel
      })
  }

  @action.bound subscribe() {
    this.joinRoom()

    this.shoutSubject.subscribe(res => {
      console.log(res)
      res.hearted = false
      this.messages = [res, ...this.messages]
    })

    this.likeSubject.subscribe(({id, heart}) => {
      const idx = this.messages.findIndex(x => x.id === id)
      const targetMessage = this.messages[idx]
      targetMessage.heart = heart

      if (!targetMessage.hearted) {
        this.likeAnimationSubject.next(targetMessage)
      }
    })

...
```

rxjsとchannelまわりをうまく組み合わせてsubscribeしたらchannelに参加しにいくように
したいのですが、なかなか難しいですね :sweat_drops: 

channelのイベントごとにSubjectを用意するのはなかなかよさそうな気がしています

## つぶやきのアニメーション

他の人のつぶやきが少しアニメーションするのは Vue.jsのTransitionを使いました
参考: https://jp.vuejs.org/v2/guide/transitions.html

```vue
<template lang="pug">
...
      transition-group(name="list-complete", tag="div")
        .media-wrapper(v-for="message in vm.messages", :key="message.id", class="list-complete-item")
          MediaObject(:vm="vm", :message="message")
...
</template>

<style lang="stylus">
.list-complete-item 
  transition: all 1s;
  display: inline-block;
  margin-right: 10px;
  width 100%

.list-complete-enter, .list-complete-leave-to
/* .list-complete-leave-active for below version 2.1.8 */ 
  opacity: 0;
  transform: translateY(-60px);

.list-complete-leave-active 
  position: absolute;
</style>
```

ほぼ公式からのコピペでそれっぽいアニメーションが実装できたので、とても楽でした。

# Likeのアニメーション

Likeのアニメーションは以下のページからいただきました
https://ics.media/entry/15970

`.hearted` というクラスを付け替えてアニメーションを実行しています

クラスの付替えはMobx内で以下のようにRxjsのSubjectを使ってやっています

```javascript
    this.likeAnimationSubject
      .pipe(
        tap(m => {m.hearted = true}),
        delay(500),
      )
      .subscribe(message => {
        message.hearted = false
      })
```

アニメーション完了後にまたLikeが飛んできたらアニメーションを行いたいため、
500msほどディレイさせてクラスを外しています

## ホスティング

ホスティングはgigalixirというElixir向けのPaasを使いました。
herokuみたいに使えるのですごい楽ですね :sparkles:

ただし、いろいろと設定ファイル等が必要でした


デフォルトだとElixirのバージョンが1.5.xで、Phoenix1.4で動かすには少し古いため、
Elixirのバージョンを指定する必要がありました

```:elixir_buildpack.config
elixir_version=1.7.4
erlang_version=21.0
```

Nodeのバージョンはデフォルトで6.9ぐらいでこれまた古かったので、以下のファイルでバージョンを指定

```:phoenix_static_buildpack.config
# Clean out cache contents from previous deploys
clean_cache=false

# We can change the filename for the compile script with this option
compile="compile"

# We can set the version of Node to use for the app here
node_version=10.14.2

# We can set the version of NPM to use for the app here
npm_version=6.4.1

# Remove node and node_modules directory to keep slug size down if it is not needed.
remove_node=false
```

gigalixirにあげたあとに静的ファイルをビルドする必要があるので、`compile`ファイルを作成して
ビルドコマンドを指定

```:compile
cd $phoenix_dir/assets
yarn build

cd $phoenix_dir
mix "${phoenix_ex}.digest"
```

# 最後に

Vue/Mobxの勉強したくて、色々触っているうちにBulmaのMediaObjectに気づいて、
「Twitter作れるじゃん！」ってなって今回作成してみました :sparkles:

またVue + Phoenixの組み合わせはWebアプリを作成するには最高の組み合わせだと感じました！
今年は色々と作ってみたいと思います :thumbsup: 

なにか意見などありましたら、コメントいただけると嬉しいです！