新卒エンジニアが初めてのVue.jsとFirebaseで社内サイネージを作った話

この投稿はアイスタイル Advent Calender 2018 の8日目の記事です。

はじめに

皆様こんにちは!
今年新卒エンジニアとしてアイスタイルに入社したnakazawayです!
普段は古くからある弊社サービスの会員基盤を担っており、日々レガシーな技術と奮闘しております!!

そんな中、日々の業務もいいけど、「新しいもの開発したいな〜」とか、「何か新しい技術も触ってみたいな〜」なんて心の中で思っていた僕でした。そんな時でした!上司から、

「nakazaway君、君に作って欲しいものがあるんだけど…」

と、ご依頼が!もちろん僕は即答で「yes」です。

nakazaway : 作って欲しいものとは…??

僕の上司   : @cosme Beauty Day の売り上げを社内サイネージにリアルタイムで表示させて欲しいんだ!!

nakazaway : 任せてください!

@cosme Beauty Dayとは?

弊社が運営するECサイト@cosme shoppingにて、12月3日に「全品20%ポイントバック」や「@cosme限定キットの販売」などを行う大規模イベントのことです。
@cosmeが生まれた日でもある12月3日を、「キレイが見つかる コスメ祭り」として、日本でコスメが最も売れる日に育てていこうと弊社社員が総力を挙げて取り組みました!

詳しくはこちら↓
https://www.istyle.co.jp/news/press/2018/09/0910.html
https://prtimes.jp/main/html/rd/p/000000075.000005126.html

今回は、そのイベントによる売り上げを集計して、社内のサイネージでリアルタイムに表示するのがミッションということです!(おぉ…割と大役!?)

設計と役割

実は…今回のサイネージ、全部自分で作った!なんてことではないのです。結論としては、表示部分のフロントエンドの実装を担当しました。それを説明するためにも、まずは設計からお話しさせていただきます!

まず、売り上げのデータなのですが、こちらはすでに社内のKafkaにメッセージとして溜まっています。今回は、このKafkaからデータを取得して、別DBに保存してフロントエンドで表示させる仕組みにしました。バックエンドはSpring Boot、フロントエンドはVue.js、DBはFirebaseのRealtime Databaseを使用しました。

バックエンドは上司に担当していただき、僕の担当としてはVue.jsからRealtime Databaseのデータを取得して、表示させることになりました。

なぜVue.jsか

かっこいい!新しい!緑が好き!とか、完全なる主観も入っていますが…以下の特徴に着目して、Vue.jsを選びました。

  • JavaScriptの他フレームワークに比べると学習コストが低い
     - HTML,CSS,JavaScriptの基礎がわかれば、ある程度書き始めることができる
     - 今回は時間の無い中での開発だったので助かりました

  • プログレッシブフレームワークである
     - 規模に合わせた開発ができる
     - 売り上げの表示のみということもあって、シンプルな実装のため、Vue.jsの必要な機能のみ使用しました

  • DOMとインスタンスデータをバインドできる(データバインディング)
     - インスタンスデータの変更に合わせてDOM要素を書き換える処理があらかじめ備わっている
     - コードがシンプルかつわかりやすくなりました

  • 単一ファイルコンポーネントが実現できる
     - 機能や関心ごと単位でファイルを分けることができる
     - 可読性があがり、改修もしやすかったです

他にもたくさんいいところはあるのですが、今回の実装に関係のある特徴は、上記の通りです!

なぜFirebaseか

NoSQL クラウド データベースでデータの保管と同期を行うことができます。データはすべてのクライアントにわたってリアルタイムで同期され、アプリがオフラインになっても、利用可能な状態を保ちます。

公式ドキュメントから引用

ということで、Realtime Databaseを使うことで、アプリケーション側ではリアルタイムなデータ取得を特に意識することなく行うことができるのです。
その手軽さから今回使用することにしました。MySQLやSQL ServerなどのRDBしか使った事の無い僕としては、初めてのNoSQLでした!しかし、具体的なデータの構造はJSONツリーなので、WebAPIを扱ったことがある身としては親しみやすい形式でした!
では、さっそく実装していきます!

プロジェクト作成

今回は、Vue CLIという公式が出しているコマンドラインツールで環境構築を行います。Vue CLIは、webpackなどのモジュールバンドラやBabelなどのトランスパイラなど、JavaScriptのアプリケーション構築には欠かせないツールを一括でセットアップしてくれます。よって、セットアップ後すぐに開発に着手できます。

1.Vue CLIをインストール

$ npm install -g vue-cli

2.プロジェクトを作成

プロジェクト作成時に必要な項目をいくつか聞かれるので、答えていく

$ vue init webpack {project-name}

? Project name {入力}
? Project description {入力}
? Author {入力}
? Vue build (Use arrow keys){下のどちらかを選択}
❯ Runtime + Compiler: recommended for most users
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere
? Install vue-router? {Yes or No} 
? Use ESLint to lint your code? {Yes or No} 
? Set up unit tests {Yes or No} 
? Setup e2e tests with Nightwatch? {Yes or No} 
? Should we run `npm install` for you after the project has been created? (recommended) // {下のどちらかを選択}
❯ Yes, use NPM
  Yes, use Yarn
  No, I will handle that myself

3.アプリケーションの実行と確認

プロジェクトフォルダに移動して、コマンドを実行

$ cd {project-name}
$ npm run dev

デフォルトでは、localhost:8080 で確認できます。

4.ファイル構成

出来上がったファイル群は以下のような感じです!
src配下にアプリケーションの肝となる、Vueファイルなどが書き出されています。
今回は主にこのsrc配下に変更を加えていきます。

コンポーネントに分ける

1.レイアウトから関心ごとを分ける

売り上げサイネージに載せる情報と、レイアウトの草案はこちらです↓

こちらを見ていただくとわかる通り、関心ごとを大きく「前日の売り上げ」「当日の売り上げ」「総売り上げ」の3つに分けることができると思います。
この3つをそれぞれ単一ファイルに分けて、実装しました。

2.単一ファイルコンポーネントの実装方法

main.jsでVueインスタンスを生成して、App.vueを呼び出し、App.vueからコンポーネントであるComponent.vueを呼び出しています。
Vueファイルは <template></template> , <script></script> , <style></style> の3つの要素で成り立っています。

// src/main.js
import Vue from 'vue'
import App from './App'

new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

// src/App.vue
<template>
  <div id="app">
    <Component/>
  </div>
</template>

<script>
import Component from './components/Component'
export default {
  name: 'App',
  components: {
    Component
  }
}
</script>

<style>
</style>

// src/components/Component.vue
<template>
<dir>
  <p>{{ msg }}</p>
</dir>
</template>

<script>
export default {
  name: 'Component',
  data () {
    return {
      msg: '単一ファイルコンポーネントだよ'
    }
  }
}
</script>

<style>
</style>

3.トランスパイル

拡張子が「.vue」のVueファイルは、Vue CLIが用意してくれたbabelによって、$ vue run dev のコマンド実行時にトランスパイルされます。

データバインディングとライフサイクルフック

リアルタイムに切り替わっていく売り上げ金額の数字(DOM要素)を、データバインディングの機能を使って切り替えていきます。まずは、データバインディングの調査からやっていきましょう。

1.インスタンスデータの定義と参照

以下のコードで使われている、 namedataは、Vueインスタンスの、オプションオブジェクトと呼びます。

// src/components/Component.vue
<template>
<dir>
  <p>{{ msg }}</p> // mustache記法で参照
</dir>
</template>

<script>
export default {
  name: 'Component',
  data () { // オプションオブジェクトのdataで、インスタンスデータの定義を行う
    return {
      msg: '単一ファイルコンポーネントだよ' 
    }
  }
}
</script>

<style>
</style>

2.DOM要素の切り替わりを確認

1秒ごとにインスタンスデータを1ずつカウントアップし、それをDOM要素にバインディングする実装をしてみました。実行して確認します。

// src/components/Component.vue
<template>
<dir>
  <p>1秒ごとに数字が増えていくよ {{ count }}</p>
</dir>
</template>

<script>
export default {
  name: 'Component',
  data () {
    return {
      count: 0
    }
  },
  created: function () {
    setInterval(() => { this.count++ }, 1000); // インスタンスデータを1秒ごとに1ずつカウントアップする処理
  }
}
</script>

<style>
</style>

結果
以下のURLから動作確認ができます。
https://xr08l4lxyw.codesandbox.io/

お!1がカウントアップされていき、DOM要素もそれに合わせて変更されたことが確認できましたね!

3.ライフサイクルフック

上のコードで、関数を実行している部分に注目してください。

created: function () {
    setInterval(() => { this.count++ }, 1000);
  }

このcreatedもオプションオブジェクトで、ライフサイクルフックの1つです。
Vue.jsもReact.jsやAngularJSなどの他のJavaScriptフレームワークのように、ライフサイクルフックが用意されています。これによって、関数などの実行タイミングを指定することができます。
Vue.jsのライフサイクルにはcreated以外にいくつか用意されています。
詳しくは公式ドキュメントをご覧ください。

Firebaseの導入

次は、Firebaseを導入していきます。

1.Firebase consoleでRealtime Databaseのプロジェクトを作成

手順
1. まずは、webツールであるFirebase consoleにアクセス。
2. 「プロジェクトを追加」をクリック
3. プロジェクト名などを設定して「プロジェクトを作成」をクリック
4. しばらく待つとプロジェクト内設定画面に移りますので、左メニューからDatabaseをクリック
5. Realtime Databaseの「データベース作成」をクリック
6. 「テストモードで開始」を選択

2.Firebase用のライブラリを追加

$ npm install --save firebase

3.Firebaseのイニシャライズ

main.jsでイニシャライズして、全コンポーネント内でFirebaseを扱えるようにします。

// src/main.js
import Vue from 'vue'
import App from './App'
import firebase from 'firebase'

const config = {
  apiKey: "{.....}",
  authDomain: "{.....}",
  databaseURL: "{.....}",
  projectId: "{.....}",
  storageBucket: "{.....}",
  messagingSenderId: "{.....}"
}

firebase.initializeApp(config)

new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

{…..}の部分には、先ほど作成したFirebaseプロジェクトの認証情報を設定していきます。
認証情報は、Firebase consoleの以下の部分をクリックすると現れます。

4.スキーマ

今回は以下のようなスキーマにしました。総売上はtotal_salesから、日にちごとの売り上げはdaily_salesから取得します。

{
   "sales": {
       "total_sales": {
           "amount": <long> // 総売り上げ
       },
       "daily_sales": {
           "yyyy-MM-dd": {
               "amount": <long> // 日にちごとの売り上げ
           },
           "yyyy-MM-dd": {
               ...
           },
           ...
       }
   }
}

Firebaseを使う

次は、実際にFirebaseを使っていきます。

1.パスの指定

まず、アクセスしたいスキーマへのパスを指定します。

const accessPath = firebase.database().ref('sales/daily_sales/YYYY-MM-DD/')

2.パス指定に日付を使う

まず、momentライブラリを使って、実行時の日付を取得します。
取得した日付をフォーマットして、先ほどのスキーマに合わせて、パスの指定部分に埋め込みます。
これで、指定したい日付のデータが取得できます。

let today = moment().format('YYYY-MM-DD')
const accessPath = firebase.database().ref('sales/daily_sales/' + today + '/')

3.Firebaseのデータ取得関数

Firebaseのデータ取得関数は、主に2つあります。

accessPath.on() //Firebaseの値の更新をリッスンして、変更があった時に都度実行される
accessPath.once() // 関数実行時の1度だけ実行される

今回は、リアルタイムでの監視なので、on()を使います。

4.データのアクセス方法

説明は公式ドキュメントに頼ります。

value イベントを使用して、特定のパスにあるコンテンツの静的スナップショットを、イベントの発生時に存在していたとおりに読み取ることができます。このメソッドはリスナーがアタッチされたときに 1 回トリガーされます。さらに、データ(子も含む)が変更されると、そのたびに再びトリガーされます。イベントのコールバックには、その場所にあるすべてのデータ(子のデータも含む)を含んでいるスナップショットが渡されます。

つまり、コードに起こすと以下のような感じです!

accessPath.on('value', (snapshot) => {
    console.log(snapshot.val()) // {amount:987654}
    console.log(snapshot.val().amount) // 987654
}

5.インスタンスデータに代入

更新の度に、インスタンスデータにFirebaseのデータを代入します。

accessPath.on('value', (snapshot) => {
    let snapshotValue = snapshot.val()
    if (snapshotValue) {
        this.count = snapshotValue.amount
    }
})

これで、リアルタイムに更新されていくFirebaseのデータの更新に合わせて、Vue.jsのインスタンスデータも更新され、バインディングによってDOM要素も更新されていきます。
最後にcreatedで関数を呼び出して完成です。

<template>
<dir>
  <h2> {{ title }} </h2>
  <h1> {{ count }} </h1>
</dir>
</template>

<script>
import firebase from 'firebase'
import moment from 'moment'
export default {
  name: 'Component',
  data () {
    return {
      title: 'today',
      count: 0
    }
  },
  methods: {
    countUp: function () {
      let today = moment().format('YYYY-MM-DD')
      const accessPath = firebase.database().ref('sales/daily_sales/' + today + '/')
      accessPath.on('value', (snapshot) => {
        let snapshotValue = snapshot.val()
        if (snapshotValue) {
          this.count = snapshotValue.amount
        }
      })
    }
  },
  created: function () {
    this.countUp()
  }
}
</script>

<style>
</style>

あとは上のコードに習って「前日の売り上げ」と「当日の売り上げ」と「総売り上げ」の3つそれぞれの実装を行い、cssを当てれば完成!

最後に

1.作り終えての感想

こんな感じで、今回無事に作り終える事ができました!反響もすごくて、社内の色々な人から「サイネージ見たよ!」とか「売り上げ見れて嬉しい!助かる!」などのお声がけをいただきました。そしてイベント当日には、社員みんなでサイネージを見て盛り上がっている姿を見ることができました。自分が作ったものが、こうやってたくさんの人に影響を与えたということがとても嬉しく、これこそエンジニアの醍醐味なのではないかと思いました!モチベーションも爆上がりです!笑
この@cosme Beauty Day をはじめとして、これからも売り上げなどの数字をサイネージに出したいという要望はいくつか出てくると思っています。その時は、今回作ったものをベースとして、「グラフ化」などの追加コンテンツを入れて、コンテンツの幅を広げていければなと思っています。

2.Vue.jsを使ってみて

素のJavaScriptでDOM要素をいじる事しかしたことがなかったフロントエンド初心者でしたが、そんな僕でもすぐにVue.jsで実装することができました!そのくらい入りやすいフレームワークだと思っています。
フロントエンドに興味のある方は是非、Vue.jsを一度試してみてはいかがでしょうか!!!

新卒1年目 @cosmeの会員周りを担当しているサーバーサイドエンジニアです。フロントエンドは趣味でやってます。 カメラと映画と緑が好き!!