Vue.js × Electron × Firebaseで ニコ生コメント風アプリ を作る

フロントやらIoTやらやっている「てぬ太」です。
先日、オンライン勉強会を開催するにあたって、参加者の反応が見えないことが怖かったので画面にコメントが流れるアプリを作成しました。

はじめに、このアプリ作成にあたっては、@yunecoさんの記事およびソースコードを参考にさせていただきました。

[Vue.jsと物理演算とElectronで仕事中にデスクトップでお寿司をつまめるようになったのでソースと解説【クソアプリ】](https://qiita.com/yuneco/items/1fa72413ab011d1c0ac5)
[VueとFirebaseの基本機能全部使ってぬるぬる動くポートフォリオサイトを作ったのでソースと解説](https://qiita.com/yuneco/items/b14a695a060c96309239)

__本投稿内では差分の工夫・苦労した点を主に書いていきます。__

[1. デモ画面](#1-デモ画面)
[2. 使った技術](#2-使った技術)
[3. 参加者が使用するwebアプリ](#3-参加者が使用するwebアプリ)
[4. 主催者が使用するデスクトップアプリ](#4-主催者が使用するデスクトップアプリ)
[5. まとめ](#5-まとめ)

細かい実装はこちらで
github：
https://github.com/tenugui-taro/nico_come_form_public
https://github.com/tenugui-taro/nico_come_screen_public

##1. デモ画面
参加者：Webアプリからコメント書き込み
![form1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/1b8023aa-69c4-2f80-4d1e-f20fc06cb411.gif)
主催者：共有している画面にコメントが流れる
![screen1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/b3ceb34a-193f-c727-2f9c-d393e4bac363.gif)

##2. 使った技術
[Vue.js](https://jp.vuejs.org/index.html)：投稿時点で最新は Vue3ですが Vuetifyを使用するために、Vue2を使用しています。
[@vue/composition-api](https://github.com/vuejs/composition-api)：Vue2でも composition-api の記述が出来るようになる。
[Vuetify](https://vuetifyjs.com/ja/)：素敵なデザイン・動きを手軽に使えるようになるVue UIライブラリ
投稿時点で Vue3 対応版リリースは、2021年Q3予定（[ロードマップ](https://vuetifyjs.com/ja/introduction/roadmap/)）
[Electron](https://www.electronjs.org/)：背景透過からデスクトップアプリ作成までを担ってくれる。
[Firebase](https://firebase.google.com/)：ログイン、データ更新・取得程度の機能ならバックエンド実装ほぼなしの手軽さ

##3. 参加者が使用するwebアプリ
https://github.com/tenugui-taro/nico_come_form_public
参加者にインストールなどの手間をかけさせないよう、Webアプリを用意しました。
機能・仕様としては以下の通りです。
【Firebase】
・匿名ユーザーでログイン可能
・リアルタイムでのデータ送信・取得　※Realtime Database 使用

【Vue】
・コメント投稿・履歴取得
・手軽に投稿できる定型文を用意
・色のカスタマイズ
・ハートの個数＝投稿可能数

VuetifyのUIコンポーネントが豊富でデザインは特に困らず。
[v-rating](https://vuetifyjs.com/ja/components/ratings/)
[v-color-picker](https://vuetifyjs.com/ja/components/color-pickers/)
[v-data-table](https://vuetifyjs.com/ja/components/data-tables/)

###firebaseを連携させたナビゲーションガード
サインインしていなければリダイレクトというよくある実装。

```javascript:src/router/index.ts
router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth) {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        signIn.value = true;
        next();
      } else {
        // サインインしていない -> ログインページへリダイレクト
        next({
          path: "/"
        });
      }
    });
  } else {
    next();
  }
});
```

###composition-api を切り出す
作るなかで、階層が深くなりコンポーネントをまたぐ値が増えてしまいました。
そこで、composition-api を別ファイルに切り出すことで、Webアプリ全体でリアクティブな値を保持するようにしました。

```javascript:src/plugins/composition-api.ts
import Vue from "vue";
import VueCompositionAPI from "@vue/composition-api";

Vue.use(VueCompositionAPI);
```
```javascript:src/main.ts
import Vue from "vue";
import App from "@/App.vue";
import router from "./router";
import "@/plugins/composition-api";
import vuetify from "@/plugins/vuetify";

Vue.config.productionTip = false;

new Vue({
  router,
  vuetify,
  render: h => h(App)
}).$mount("#app");
```
```javascript:src/store/commentColor.ts
import "@/plugins/composition-api";
import { ref } from "@vue/composition-api";

export const commentColor = ref("#000000");
```
###子のイベントを親側でまとめて処理する
複数の子コンポーネントの処理の内容が同じだったので、親側の１つの関数で処理をまとめました。

```vuejs:src/views/Home.vue
<template>
    <!-- 関係ない要素は省略しています -->
    <CommentForm @sendComment="sendComment" />
    <FlashComments @sendFlashComment="sendComment"/>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";
export default defineComponent({
  setup() {
    const sendComment = () => {
       // 処理 
    };

    return {
      sendComment
    };
  }
});
</script>
```

##4. 主催者が使用するデスクトップアプリ
https://github.com/tenugui-taro/nico_come_screen_public
機能・仕様としては以下の通りです。
【Firebase】
・リアルタイムでのデータ取得　※Realtime Database 使用

【Vue】
・コメントを画面に流す

【Electron】
・透明なウィンドウを画面に表示する

###透明なウィンドウを画面に表示する
詳しくはこちら→[Vue.jsと物理演算とElectronで仕事中にデスクトップでお寿司をつまめるようになったのでソースと解説【クソアプリ】](https://qiita.com/yuneco/items/1fa72413ab011d1c0ac5#step3-%E3%82%A6%E3%82%A4%E3%83%B3%E3%83%89%E3%82%A6%E3%82%92%E9%80%8F%E9%81%8E%E3%81%95%E3%81%9B%E3%82%8B)

以下、２点を追加で設定しています。
・ウィンドウサイズを画面に合わせて最大化
・デベロッパーツールを別ウィンドウで表示させる


```javascript:src/background.ts
async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    transparent: true,
    frame: false,
    resizable: false,
    backgroundColor: '#00FFFFFF',
    hasShadow: false,
    alwaysOnTop: true,
    webPreferences: {
      enableRemoteModule: true,
      nodeIntegration: true,
    },
  });
  win.setIgnoreMouseEvents(true, { forward: true });
  win.maximize(); // ウィンドウサイズを画面に合わせて最大化

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
    // デベロッパーツールを別ウィンドウで表示させる
    if (!process.env.IS_TEST) win.webContents.openDevTools({ mode: 'detach' });
  } else {
    createProtocol('app');
    win.loadURL('app://./index.html');
  }
}
```

###コメントを画面に流す
流れるコメントの高さを固定する実装にかなりハマってしまいました。
最終的には top: 〇〇vh を設定することで他の要素に影響されなくなり、無事固定できました。

画面を流れるアニメーションはtransition-groupを用いてcssで設定しています。

```vuejs:src/views/App.vue
 
<transition-group appear>
  <template v-for="comment in comments">
    <FlowComment :key="comment.id" :comment="comment" />
  </template>
</transition-group>
```

```vuejs:src/components/FlowComment.vue
<template>
  <h1
    class="comment-text"
    :style="{
      top: `${comment.posY}`,
      left: `-100vw`,
      color: `${comment.color}`,
    }"
    v-text="comment.text"
  />
</template>

<style lang="scss" scoped>
.comment-text {
  position: absolute;
}

.v-enter-active {
  transition: all 13s linear;
}

.v-enter {
  transform: translateX(200vw);
}

.v-enter-to {
  transform: translateX(-100vw);
}
</style>
```

##5. まとめ
試行錯誤していたはずが、終わってみると特に書くネタがないのはなぜ.......
せめて Vuetify使いつつcomposition-api使いたい！という方などの役に立てば幸いです。