Posted at

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


作ったもの

https://tubuyakiya.gigalixirapp.com/

スクリーンショット 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:

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