用 Vue.js + Firebase 製作即時聊天功能

這幾天在練習用vue & firebase刻一個仿line即時同步聊天的功能,
直接初體驗vue.js + firebase + webpack三種願望一次滿足XD!

update: 20170923更新
[用 Vue.js + Firebase 製作即時聊天功能(2) - storage]
(https://guahsu.io/2017/09/vue-firebase-realtime-line-chat-2-storage/)

>DEMO<

>原始碼-GitHub<

這幾天想到就會再稍微更新,GitHub與下方文章可能會略有不同

環境設定步驟

  1. 首先安裝node.js,理論目前的版本都會有內建npm了。

    下載位置:https://nodejs.org
    可以透過node -vnpm -v來查證是否已安裝完成node與npm。

  2. 安裝vue-cli,透過指令npm install --g vue-cli安裝。

    可以透過vue -V來查證是否已安裝完成(-V是大寫唷)。

Vue-cli & webpack

  1. 先起一個資料夾來存放專案,並進入該資料夾內,
  2. 接著建立專案,透過vue-cli可以透過指令直接建立一包專案,
    這裡我們使用指令vue init webpack來建立webpack的專案包。

    安裝vue-cli後,可以在命令列下vue list列出可用的template
    建立專案使用vue init <template>這次用到的是webpack。

  3. 建立設定項目:[]<方框內的是我的設定選項

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ? Generate project in current directory? //建於當前資料夾[Yes]
    ? Project name (folder_name) //專案名稱,注意需小寫 [自訂專案名稱]
    ? Project description A Vue.js project //專案描述 [自訂描述]
    ? Author //作者,預設抓當前環境git user [自訂]
    ? Vue build standalone [Enter]
    ? Install vue-router? //安裝vue-router [Yes]
    ? Use ESLint to lint your code? [No]
    ? Setup unit tests with Karma + Mocha? [No]
    ? Setup e2e tests with Nightwatch? [No]
  4. 當環境建立好後,輸入指令npm install使相依套件都下載到當前專案中

  5. 主要目錄結構:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    |- build (webpack的設定檔)
    |- config (專案設定檔)
    |- dist (編譯後產出的位置)
    |- src (專案程式碼目錄)
    |- assets (其他 css, js, images)
    |- components (主要 vue 元件)
    |- router (vue 的路由器)
    |- App.vue (主要樣版檔)
    |- main.js (vue js 主檔案)
    |- index.js 靜態首頁(進入點)

Firebase

  1. 建立一個專案
  2. 將Firebase的連結資訊複製起來
  3. 進入Database
  4. 修改權限並發布

vue-router設定

  1. 到index.html把firebase剛才複製的那串載入至head中
  2. 到router/index.js修改程式:
    vue-router是vue的路由器,備註內部使用方式如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import Vue from 'vue'
    import Router from 'vue-router'
    //載入元件ChatRoom
    import ChatRoom from '@/components/ChatRoom'

    Vue.use(Router)
    export default new Router({
    routes: [
    {
    //路徑用於網址列
    path: '/',
    //name用於設定連結,例如樣板頁中可用下面方式來寫連結,就不用寫<a>掛path了
    //<router-link :to="{ name: 'ChatRoom' }>ChatRoom Page</router-link>
    name: 'ChatRoom',
    //到這個ChatRoom(/)時,使用ChatRoom元件
    component: ChatRoom
    }
    ]
    })

而routes內是陣列包覆物件,所以要再新增一個就只要透過逗號(,)的物件新增方式即可,
而router的結果都會被呈現在<router-view></router-view>中(參考main.js)。

在我的程式碼中,Hello已被替換為ChatRoom(預設範例為Hello)
其實這個練習中目前並沒有實際用到router的功能,因為僅載入一頁XD。
詳細設定可參閱官方文件vue-router 2官方文件

流程

  1. 輸入使用者名稱後才能發文
  2. 然後自己的發文是綠底,其他人是灰色(跟line一樣)
  3. 就這樣XD
  4. (傳圖功能請參考第二篇->用 Vue.js + Firebase 製作即時聊天功能(2) - storage)

主程式撰寫ChatRoom

  1. 到components/ChatRoom.vue(預設是Hello.vue我改名了)
  2. HTML與JS都有用到vue的寫法,我將撰寫的程式已備註如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    <template>
    <div class="container">
    <!-- 區塊:name area -->
    <div class="name">
    <h3>Name:{{ userName }}</h3>
    <!-- 註解:使用@click來偵測click,觸發時執行method中的setName() -->
    <div class="reset" @click="setName()">Reset Name</div>
    </div>
    <!-- 區塊:chat room -->
    <div class="chatRoom">
    <!-- 區塊:head -->
    <div class="roomHead">
    <div class="roomHead__topButtons">
    <div class="roomHead__button close"></div>
    <div class="roomHead__button minimize"></div>
    <div class="roomHead__button zoom"></div>
    </div>
    <img src="https://lorempixel.com/50/50/" class="roomHead__img" draggable="false">
    <div class="roomHead__title">Test Room</div>
    </div>
    <!-- 區塊:body -->
    <div id="js-roomBody" class="roomBody">
    <!-- 註解:使用template來當迴圈容器,或是判斷用的容器,當條件達成時產出template內容 -->
    <template v-for="item in messages">
    <!-- other people -->
    <template v-if="item.userName != userName">
    <div class="messageBox">
    <img src="https://lorempixel.com/40/40/" class="messageBox__img" draggable="false">
    <div class="messageBox__content">
    <!-- 註解:Vue使用雙花括號{{}}來顯示script中data:的資料 -->
    <div class="messageBox__name">{{item.userName}}</div>
    <div class="messageBox__text">{{item.message}}</div>
    </div>
    <div class="messageBox__time">{{item.timeStamp}}</div>
    </div>
    </template>
    <!-- 區塊:self -->
    <template v-if="item.userName == userName">
    <div class="messageBox messageBox--self">
    <div class="messageBox__time messageBox__time--self">{{item.timeStamp}}</div>
    <div class="messageBox__content messageBox__content--self">
    <div class="messageBox__text messageBox__text--self">{{item.message}}</div>
    </div>
    </div>
    </template>
    </template>
    </div>
    <!-- 區塊:bottom -->
    <!-- 註解:使用:class來寫class是否顯示的判斷式{ class: 判斷式 } -->
    <div class="roomBottom" :class="{ disable: !userName }">
    <div class="roomBottom__tools"></div>
    <div class="roomBottom__input">
    <!-- 若要再帶入原生js的event(e)到function中,必須使用$event當參數傳入 -->
    <textarea id="js-message" class="roomBottom__input__textarea"
    :class="{ disable: !userName }"
    @keydown.enter="sendMessage($event)"></textarea>
    </div>
    </div>
    </div>
    <!-- 區塊:modal -->
    <div id="js-modal" class="modal">
    <div class="modal__container">
    <header class="modal__header">
    <h2 class="view-title">輸入名稱</h2>
    </header>
    <div class="modal__body">
    <!-- 註解:使用@keydown.enter來偵測keydown enter,觸發時執行method中的saveName() -->
    <input type="text" id="js-userName" class="userName" maxlength="6" @keydown.enter.="saveName()">
    <div class="button" @click="saveName()">設定</div>
    </div>
    <footer class="modal__footer"></footer>
    </div>
    </div>
    </div>
    </template>

    <script>
    // msgRef = firebase中的資料表/messages/,若沒有的會自動建立
    const msgRef = firebase.database().ref('/messages/');
    export default {
    // 指定此頁使用的name
    name: 'ChatRoom',
    // 資料位置,於html中可用{{}}渲染出來
    data() {
    return {
    userName: '',
    messages: []
    }
    },
    // 這個頁面的functions
    methods: {
    /** 彈出設定視窗 */
    setName() {
    document.querySelector('#js-modal').style.display = 'block';
    },
    /** 儲存設定名稱 */
    saveName() {
    // vue的mtthod中this是指export中這整塊的資料
    const vm = this;
    const userName = document.querySelector('#js-userName').value;
    if (userName.trim() == '') { return; }
    // 這裡的vm.userName(this.userName)就是data()裡面的userName
    vm.userName = userName;
    document.querySelector('#js-modal').style.display = 'none';
    },
    /** 取得時間 */
    getTime() {
    const now = new Date();
    const hours = now.getHours();
    const minutes = now.getMinutes();
    const format = (hours >= 12) ? "下午" : "上午";
    return `${format} ${hours}:${minutes}`;
    },
    /** 傳送訊息 */
    sendMessage(e) {
    const vm = this;
    let userName = document.querySelector('#js-userName');
    let message = document.querySelector('#js-message');
    // 如果是按住shift則不傳送訊息(多行輸入)
    if (e.shiftKey) {
    return false;
    }
    // 如果輸入是空則不傳送訊息
    if(message.value.length <=1 && message.value.trim() == '') {
    // 避免enter產生的空白換行
    e.preventDefault();
    return false;
    }
    // 對firebase的db做push,db只能接受json物件格式,若要用陣列要先轉字串來存
    msgRef.push({
    userName: userName.value,
    message: message.value,
    // 取得時間,這裡的vm.getTime()就是method中的getTime
    timeStamp: vm.getTime()
    })
    // 清空輸入欄位並避免enter產生的空白換行
    message.value = '';
    e.preventDefault();
    }
    },
    // mounted是vue的生命週期之一,代表模板已編譯完成,已經取值準備渲染HTML畫面了
    mounted() {
    const vm = this;
    msgRef.on('value', function(snapshot) {
    const val = snapshot.val();
    vm.messages = val;
    })
    },
    // update是vue的生命週期之一,接再munted後方代表HTML元件渲染完成後
    updated() {
    // 當畫面渲染完成,把聊天視窗滾到最底部(讀取最新消息)
    const roomBody = document.querySelector('#js-roomBody');
    roomBody.scrollTop = roomBody.scrollHeight;
    }
    }
    </script>

    <style scoped>
    /* CSS太多,不占版面放於github供參考 */
    </style>

執行與編譯

  1. 使用指令npm run dev來讓這專案在本機掛server起來(預設8080port),
    之後每次調整檔案內容,網頁就會自動刷新,非常方便開發及測試:D!
  2. 編譯使用npm run build會將src中所撰寫的資訊都壓縮至dist資料夾內。

    稍微備註,編譯後的index在載入js/css時的路徑有多一個.
    會導致放靜態主機時讀取錯誤(因為其實是在同一層),
    所以可以到config/index.js中將設定調整為assetsPublicPat = ''來解決

心得

目前還有卡著JS30尚未練習及寫完心得,
但看著各路大神分享的資源,就一直很想寫看看XD
vue-cli & firebase & webpack都是第一次使用,
這個練習後,對這三神器終於有很基礎的理解了。

vue.js

我最初會想學習vue是因為有中文文件(遮臉),
以及方便載入(可以直接掛載一個vue.js在html中來使用,像jQuery一樣),
目前這專案我學習到的是HTML中的template及v-if/v-for,
以及on(@),bind(:)的用法,很方便可以組織動態的前端邏輯,
而不用在js中組大量的字串模板。

在js控制中,我覺得生命周期的設定很棒,
可以很方便且”清楚”的在預想的狀況中設定應該出現的效果,
例如整個渲染完成前可以掛一個加載的效果等等..

但這小練習我都把邏輯整在同一個vue中,
還未學到/使用在vue中正確拆分邏輯的做法。

Firebase

之前有稍微聽過被google合併,但就僅此於而已此從未使用及了解過XD
這次練習中使用到的database覺得很新奇阿,是一個雲端即時同步的noSQL,
設定非常簡單方便,也是第一次親身寫出/感受到websoket的效果感(超酷)。
其他相關的功能也很多,之後有機會一定要在多研究一下(越來越多待讀項目..)

Webpack

一直有聽到,看過,但從未使用過,
但其實這次使用後的算是知道如何使用,但不了解內容,
對於設定檔目前沒有細讀,並不是很熟悉各相關設定檔,
反而覺得最特別的是熱加載及編譯後的程式碼壓縮混淆!
但日後練習都用webpack起,遇到問題找解答,應該會越來越熟吧XD

感謝

六角學院放出的Vue教學系列,從幼幼班入門到vue-cli & firebase介紹,
讓我能從零學習相關知識,從而建立完這個練習:)

六角學院在本週日也要釋放Bootstrap的課程了!
但不知何時才有空可以把全系列看過並實作完啊QQ….

六角學院-FB
洧杰老師-FB
卡斯伯老師-FB