4
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

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

作ったもの

スクリーンショット 2019-01-13 21.23.16.png

みんな大好きいらすとやさんの人物いらすとを使って、
Twitter風につぶやけるサービスです :sparkles:

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

WebSocketを使っており、

他の人のつぶやきがリアルタイムで表示されたり Likeされていくと、アニメーション付きで数字がリアルタイムで増えていきます
gamen2.gif gamen1.gif

使った技術

フロントエンド

  • Vue.js (Vue CLI 3)
  • Mobx
  • rxjs
  • bulma

バックエンド

  • Elixir/Phoenix

ホスティング

  • gigalixir

参考: 【Gigalixir編①】Elixir/Phoenix本番リリース: 初期PJリリースまで

プロジェクト構成

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

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

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

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

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

状態管理

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

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

<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:

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

<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を使ってやっています

    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:

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?