作ったもの
みんな大好きいらすとやさんの人物いらすとを使って、
Twitter風につぶやけるサービスです
アバターは「変更」ボタンを押すとランダムで変わります(10種類ぐらいしかないです・・・)
WebSocketを使っており、
他の人のつぶやきがリアルタイムで表示されたり | Likeされていくと、アニメーション付きで数字がリアルタイムで増えていきます |
---|---|
使った技術
フロントエンド
- 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フォルダに向けます
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がどんなものか勉強してみたかったので。
状態管理のライブラリが欲しくなる場面として、複数コンポーネント感での
同じ状態をいじりたいというのがあると思ってますが、
今回は以下のように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まわりがすごい簡単で、感動しますね
@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に参加しにいくように
したいのですが、なかなか難しいですね
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みたいに使えるのですごい楽ですね
ただし、いろいろと設定ファイル等が必要でした
デフォルトだとElixirのバージョンが1.5.xで、Phoenix1.4で動かすには少し古いため、
Elixirのバージョンを指定する必要がありました
elixir_version=1.7.4
erlang_version=21.0
Nodeのバージョンはデフォルトで6.9ぐらいでこれまた古かったので、以下のファイルでバージョンを指定
# 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
ファイルを作成して
ビルドコマンドを指定
cd $phoenix_dir/assets
yarn build
cd $phoenix_dir
mix "${phoenix_ex}.digest"
最後に
Vue/Mobxの勉強したくて、色々触っているうちにBulmaのMediaObjectに気づいて、
「Twitter作れるじゃん!」ってなって今回作成してみました
またVue + Phoenixの組み合わせはWebアプリを作成するには最高の組み合わせだと感じました!
今年は色々と作ってみたいと思います
なにか意見などありましたら、コメントいただけると嬉しいです!