用Vue.js和Pusher建立實時聊天應用

做者:Michael Wanyoike
原文:www.sitepoint.com/pusher-vue-…javascript

現現在,即時通迅已經愈來愈廣泛,而且用戶體驗也愈來愈天然和流暢。css

本文將使用ChatKit增強過的Vue.js建立一個實時聊天應用,ChatKit服務爲咱們提供了一個建立聊天應用的後端,而且能夠運行於任何設備上,讓咱們只需關注前端用戶接口,這個接口經過ChatKit client包鏈接到ChatKit服務。前端

準備條件

這是一篇中到高級的教程,理解本文須要對如下概念都比較熟悉:vue

  • Vue.js基礎
  • Vuex基本原理
  • 使用CSS框架

還須要安裝Node.js,能夠直接在官網上下載安裝包。 最後須要使用如下命令安裝全局的Vue CLI。java

npm install -g @vue/cli
複製代碼

在寫這篇文章的時候Node版本是 10.14.1,Vue CLI的最新版本是 3.2.1。node

關於例子

咱們要建立一個基礎的聊天應用,應用須要有以下功能:git

  • 多個通道和房間
  • 列出房間內的成員並檢測成員的在線狀態
  • 當其餘用戶開始輸入消息時進行監測

就像先前提到的,這裏只建立前端,ChatKit服務有個能夠管理用戶、受權和房間的後端接口。github

能夠在GitHub上找到完整的代碼。web

設置ChatKit實例

建立ChatKit實例,相似於建立服務端實例。進入Puser網站的ChatKit頁面,先註冊,完成登陸後進入Pusher的儀表板,而後選擇ChatKit產品。vue-router

點擊Create按鈕建立一個新的ChatKit實例,好比輸入VueChatTut。

在這一教程中使用免費版,支持1000個用戶,足夠咱們在本例中使用了,轉到Console選項卡,須要建立一個新的用戶開始咱們的應用。直接點擊Create User按鈕。

能夠添加兩到三個用戶,例如:

  • John Wick
  • salt, Evelyn Salt
  • hunt, Ethan Hunt

再建立三個房間並指定相應的用戶。例如:

  • General (john, salt, hunt)
  • Weapons (john, salt)
  • Combat (john, hunt)

最後,控制檯界面是這樣子:

下一步,能夠進入Rooms選項卡並選擇用戶,而後輸入消息測試一下。接下來,進入Credentials選項卡記錄下Instance Locator,而且激活Test Token Provider,它是用來生成HTTP終端結點的,這個也記錄下來。

ChatKit的後端也準備好了,如今開始構建Vue.js的前端。

搭建Vue.js項目

打開終端,像下面這樣建立項目

vue create vue-chatkit
複製代碼

選擇Manually select features而且像下面這樣選擇相關問題。

確保選擇了Babel, Vuex和Vue Router做爲附加功能。接下來,建立以下結構的文件夾和文件:

確保建立了上圖中全部的文件夾和文件,刪除不須要的文件也就是上圖中不存在的文件。

對於loading-btn.css他loading.css這兩個文件,能夠在loading.io上找到,這兩件文件沒法經過npm倉庫獲取,因此須要本身手動下載,而後放在項目中。在此最好知道這兩個文件是作什麼的,怎樣定製化加載條。

下面,安裝下面依賴:

npm i @pusher/chatkit-client bootstrap-vue moment vue-chat-scroll vuex-persist
複製代碼

能夠點擊連接看看每一個包都是作什麼的,怎樣配置。

如今配置Vue.js項目。打開src/main.js更新代碼爲以下內容:

import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import VueChatScroll from 'vue-chat-scroll'

import App from './App.vue'
import router from './router'
import store from './store/index'

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import './assets/css/loading.css'
import './assets/css/loading-btn.css'

Vue.config.productionTip = false
Vue.use(BootstrapVue)
Vue.use(VueChatScroll)

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
複製代碼

更新src/router.js:

import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/Login.vue'
import ChatDashboard from './views/ChatDashboard.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'login',
      component: Login
    },
    {
      path: '/chat',
      name: 'chat',
      component: ChatDashboard,
    }
  ]
})
複製代碼

更新src/store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import mutations from './mutations'
import actions from './actions'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

const vuexLocal = new VuexPersistence({
  storage: window.localStorage
})

export default new Vuex.Store({
  state: {
  },
  mutations,
  actions,
  getters: {
  },
  plugins: [vuexLocal.plugin],
  strict: debug
})
複製代碼

Vue-persist是爲了讓Vuex的state在頁面刷新和從新加載的時候可以保存下來。

目前,代碼應該是可以編譯並無錯誤的,但如今不執行,還須要建立用戶界面。

構建UI界面

如今開始更新src/App.vue:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>
複製代碼

接下來須要定義UI組件運行所須要的Vuex store的state ,經過進入src/store/index.js,更新一下state和getters部分,像下面這樣:

state: {
  loading: false,
  sending: false,
  error: null,
  user: [],
  reconnect: false,
  activeRoom: null,
  rooms: [],
  users: [],
  messages: [],
  userTyping: null
},
getters: {
  hasError: state => state.error ? true : false
},
複製代碼

這是這個聊天應用所須要的全部的state變量了,loading state用於在UI上決定是否顯示CSS 加載條。error state用於存儲剛發生的錯誤信息,其餘的變量會在用到的時候再解釋。

接下來打開src/view/Login.vue更新以下:

<template>
  <div class="login">
    <b-jumbotron  header="Vue.js Chat"
                  lead="Powered by Chatkit SDK and Bootstrap-Vue"
                  bg-variant="info"
                  text-variant="white">
      <p>For more information visit website</p>
      <b-btn target="_blank" href="https://pusher.com/chatkit">More Info</b-btn>
    </b-jumbotron>
    <b-container>
      <b-row>
        <b-col lg="4" md="3"></b-col>
        <b-col lg="4" md="6">
          <LoginForm />
        </b-col>
        <b-col lg="4" md="3"></b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
import LoginForm from '@/components/LoginForm.vue'

export default {
  name: 'login',
  components: {
    LoginForm
  }
}
</script>
複製代碼

而後,向src/components/LoginForm.vue插入以下代碼:

<template>
  <div class="login-form">
    <h5 class="text-center">Chat Login</h5>
    <hr>
    <b-form @submit.prevent="onSubmit">
       <b-alert variant="danger" :show="hasError">{{ error }} </b-alert>

      <b-form-group id="userInputGroup"
                    label="User Name"
                    label-for="userInput">
        <b-form-input id="userInput"
                      type="text"
                      placeholder="Enter user name"
                      v-model="userId"
                      autocomplete="off"
                      :disabled="loading"
                      required>
        </b-form-input>
      </b-form-group>

      <b-button type="submit"
                variant="primary"
                class="ld-ext-right"
                v-bind:class="{ running: loading }"
                :disabled="isValid">
                Login <div class="ld ld-ring ld-spin"></div>
      </b-button>
    </b-form>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  name: 'login-form',
  data() {
    return {
      userId: '',
    }
  },
  computed: {
    isValid: function() {
      const result = this.userId.length < 3;
      return result ? result : this.loading
    },
    ...mapState([
      'loading',
      'error'
    ]),
    ...mapGetters([
      'hasError'
    ])
  }
}
</script>
複製代碼

正如先前提到的,這是高級教程,若是理解這些代碼有任何問題,能夠看準備條件或項目依賴的相關信息。

如今能夠經過npm run serve啓動Vue dev服務端來確認一下應用執行沒有任何兼容性問題。

能夠輸入用戶名確認一下校驗功能是否有效,輸入三個單詞後就能夠看到Login按鈕被激活了。Login按鈕如今是不起做用的,由於咱們尚未針對這部分的編碼,後面咱們會繼續這部分。如今繼續構建聊天的用戶界面。

打開src/vie/ChatDashboard.vue插入如下代碼:

<template>
  <div class="chat-dashboard">
    <ChatNavBar />
    <b-container fluid class="ld-over" v-bind:class="{ running: loading }">
      <div class="ld ld-ring ld-spin"></div>
      <b-row>
        <b-col cols="2">
          <RoomList />
        </b-col>

        <b-col cols="8">
          <b-row>
            <b-col id="chat-content">
              <MessageList />
            </b-col>
          </b-row>
          <b-row>
            <b-col>
              <MessageForm />
            </b-col>
          </b-row>
        </b-col>

        <b-col cols="2">
          <UserList />
        </b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
import ChatNavBar from '@/components/ChatNavBar.vue'
import RoomList from '@/components/RoomList.vue'
import MessageList from '@/components/MessageList.vue'
import MessageForm from '@/components/MessageForm.vue'
import UserList from '@/components/UserList.vue'
import { mapState } from 'vuex';

export default {
  name: 'Chat',
  components: {
    ChatNavBar,
    RoomList,
    UserList,
    MessageList,
    MessageForm
  },
  computed: {
    ...mapState([
      'loading'
    ])
  }
}
</script>
複製代碼

ChatDashboard至關於下面子組件的一個用於佈局的父頁面。

  • ChatNavBar,基礎的導航欄
  • RoomList,列出了登陸用戶能夠訪問的房間,它也是一個房間選擇器
  • UserList,列出了所選房間的成員
  • MessageList,展現了所選房間的所發送的消息
  • MessageForm,向所選房間發送消息的表單
    讓咱們在每一個組件中放入一些樣例代碼,以確保全部內容都顯示出來。 向src/components/ChatNavBar.vue中添加以下樣例代碼:
<template>
  <b-navbar id="chat-navbar" toggleable="md" type="dark" variant="info">
    <b-navbar-brand href="#">
      Vue Chat
    </b-navbar-brand>
    <b-navbar-nav class="ml-auto">
      <b-nav-text>{{ user.name }} | </b-nav-text>
      <b-nav-item href="#" active>Logout</b-nav-item>
    </b-navbar-nav>
  </b-navbar>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'ChatNavBar',
  computed: {
    ...mapState([
      'user',
    ])
  },
}
</script>

<style>
  #chat-navbar {
    margin-bottom: 15px;
  }
</style>
複製代碼

向src/components/RoomList.vue添加以下樣例代碼:

<template>
  <div class="room-list">
    <h4>Channels</h4>
    <hr>
    <b-list-group v-if="activeRoom">
      <b-list-group-item v-for="room in rooms"
                        :key="room.name"
                        :active="activeRoom.id === room.id"
                        href="#"
                        @click="onChange(room)">
        # {{ room.name }}
      </b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'RoomList',
  computed: {
    ...mapState([
      'rooms',
      'activeRoom'
    ]),
  }
}
</script>
複製代碼

向src/components/UserList.vue添加以下樣例代碼:

<template>
  <div class="user-list">
    <h4>Members</h4>
    <hr>
    <b-list-group>
      <b-list-group-item v-for="user in users" :key="user.username">
        {{ user.name }}
        <b-badge v-if="user.presence"
        :variant="statusColor(user.presence)"
        pill>
        {{ user.presence }}</b-badge>
      </b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'user-list',
  computed: {
    ...mapState([
      'loading',
      'users'
    ])
  },
  methods: {
    statusColor(status) {
      return status === 'online' ? 'success' : 'warning'
    }
  }
}
</script>
複製代碼

向src/components/MessageList.vue添加以下樣例代碼:

<template>
  <div class="message-list">
    <h4>Messages</h4>
    <hr>
    <div id="chat-messages" class="message-group" v-chat-scroll="{smooth: true}">
      <div class="message" v-for="(message, index) in messages" :key="index">
        <div class="clearfix">
          <h4 class="message-title">{{ message.name }}</h4>
          <small class="text-muted float-right">@{{ message.username }}</small>
        </div>
        <p class="message-text">
          {{ message.text }}
        </p>
        <div class="clearfix">
          <small class="text-muted float-right">{{ message.date }}</small>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'message-list',
  computed: {
    ...mapState([
      'messages',
    ])
  }
}
</script>

<style>
.message-list {
  margin-bottom: 15px;
  padding-right: 15px;
}
.message-group {
  height: 65vh !important;
  overflow-y: scroll;
}
.message {
  border: 1px solid lightblue;
  border-radius: 4px;
  padding: 10px;
  margin-bottom: 15px;
}
.message-title {
  font-size: 1rem;
  display:inline;
}
.message-text {
  color: gray;
  margin-bottom: 0;
}
.user-typing {
  height: 1rem;
}
</style>
複製代碼

向src/components/MessageForm.vue添加以下樣例代碼:

<template>
  <div class="message-form ld-over">
    <small class="text-muted">@{{ user.username }}</small>
    <b-form @submit.prevent="onSubmit" class="ld-over" v-bind:class="{ running: sending }">
      <div class="ld ld-ring ld-spin"></div>
      <b-alert variant="danger" :show="hasError">{{ error }} </b-alert>
      <b-form-group>
        <b-form-input id="message-input"
                      type="text"
                      v-model="message"
                      placeholder="Enter Message"
                      autocomplete="off"
                      required>
        </b-form-input>
      </b-form-group>
      <div class="clearfix">
        <b-button type="submit" variant="primary" class="float-right">
          Send
        </b-button>
      </div>
    </b-form>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  name: 'message-form',
  data() {
    return {
      message: ''
    }
  },
  computed: {
    ...mapState([
      'user',
      'sending',
      'error',
      'activeRoom'
    ]),
    ...mapGetters([
      'hasError'
    ])
  }
}
</script>
複製代碼

檢查一下代碼確保沒有什麼是很神祕的。導航到http://localhost:8080/chat檢查一下全部內容都能正常執行。檢查一下終端和瀏覽器的控制面板確保在這裏沒有錯誤,那麼如今頁面看起來是下圖這個樣子的。

很空是否是?進入src/store/index.js而後在state上插入一些Mock數據:

state: {
  loading: false,
  sending: false,
  error: 'Relax! This is just a drill error message',
  user: {
    username: 'Jack',
    name: 'Jack Sparrow'
  },
  reconnect: false,
  activeRoom: {
    id: '124'
  },
  rooms: [
    {
      id: '123',
      name: 'Ships'
    },
    {
      id: '124',
      name: 'Treasure'
    }
  ],
  users: [
    {
      username: 'Jack',
      name: 'Jack Sparrow',
      presence: 'online'
    },
    {
      username: 'Barbossa',
      name: 'Hector Barbossa',
      presence: 'offline'
    }
  ],
  messages: [
    {
      username: 'Jack',
      date: '11/12/1644',
      text: 'Not all treasure is silver and gold mate'
    },
    {
      username: 'Jack',
      date: '12/12/1644',
      text: 'If you were waiting for the opportune moment, that was it'
    },
    {
      username: 'Hector',
      date: '12/12/1644',
      text: 'You know Jack, I thought I had you figured out'
    }
  ],
  userTyping: null
},
複製代碼

保存這個文件後,就能夠看到下圖的內容了。

這個簡單的測試確保全部的組件和state是正常綁定的。如今能夠恢復到原來的state代碼:

state: {
  loading: false,
  sending: false,
  error: null,
  user: null,
  reconnect: false,
  activeRoom: null,
  rooms: [],
  users: [],
  messages: [],
  userTyping: null
}
複製代碼

如今開始實現具體特性,從登陸表單開始。

無密碼認證

這部分將引入一個無密碼非安全的認證系統。本文不涉及合適的安全認證方面。首先,須要開始構建本身的接口,它將經過@pusher/ ChatKit -client包與ChatKit服務進行交互。

回到ChatKit控制面板,將原來提到的instance和測試token參數拷到項目根目錄下的.env.local文件中並保存:

VUE_APP_INSTANCE_LOCATOR=
VUE_APP_TOKEN_URL=
VUE_APP_MESSAGE_LIMIT=10
複製代碼

咱們添加了MESSAGE_LIMIT參數,這個值是限制聊天應用將獲取的消息數量。而後確保把credentials選項卡中的其餘參數也填上了。

接下來,進入src/chatkit.js開始構建聊天應用的基礎:

import { ChatManager, TokenProvider } from '@pusher/chatkit-client'

const INSTANCE_LOCATOR = process.env.VUE_APP_INSTANCE_LOCATOR;
const TOKEN_URL = process.env.VUE_APP_TOKEN_URL;
const MESSAGE_LIMIT = Number(process.env.VUE_APP_MESSAGE_LIMIT) || 10;

let currentUser = null;
let activeRoom = null;

async function connectUser(userId) {
  const chatManager = new ChatManager({
    instanceLocator: INSTANCE_LOCATOR,
    tokenProvider: new TokenProvider({ url: TOKEN_URL }),
    userId
  });
  currentUser = await chatManager.connect();
  return currentUser;
}

export default {
  connectUser
}
複製代碼

注意咱們須要將常量MESSAGE_LIMIT轉換成數值,由於默認狀況下process.env對象會強制全部的屬性是字符串類型的。 向src/store/mutations插入以下代碼:

export default {
  setError(state, error) {
    state.error = error;
  },
  setLoading(state, loading) {
    state.loading = loading;
  },
  setUser(state, user) {
    state.user = user;
  },
  setReconnect(state, reconnect) {
    state.reconnect = reconnect;
  },
  setActiveRoom(state, roomId) {
    state.activeRoom = roomId;
  },
  setRooms(state, rooms) {
    state.rooms = rooms
  },
  setUsers(state, users) {
    state.users = users
  },
 clearChatRoom(state) {
    state.users = [];
    state.messages = [];
  },
  setMessages(state, messages) {
    state.messages = messages
  },
  addMessage(state, message) {
    state.messages.push(message)
  },
  setSending(state, status) {
    state.sending = status
  },
  setUserTyping(state, userId) {
    state.userTyping = userId
  },
  reset(state) {
    state.error = null;
    state.users = [];
    state.messages = [];
    state.rooms = [];
    state.user = null
  }
}
複製代碼

mutations中的代碼至關簡單,就是一堆setters,在後面的幾節裏,你很快就會理解每一個mutation函數的用途。接下來,更新src/store/actions.js的代碼:

import chatkit from '../chatkit';

// Helper function for displaying error messages
function handleError(commit, error) {
  const message = error.message || error.info.error_description;
  commit('setError', message);
}

export default {
  async login({ commit, state }, userId) {
    try {
      commit('setError', '');
      commit('setLoading', true);
      // Connect user to ChatKit service
      const currentUser = await chatkit.connectUser(userId);
      commit('setUser', {
        username: currentUser.id,
        name: currentUser.name
      });
      commit('setReconnect', false);

      // Test state.user
      console.log(state.user);
    } catch (error) {
      handleError(commit, error)
    } finally {
      commit('setLoading', false);
    }
  }
}
複製代碼

像下面這樣更新src/components/LoginForm.vue的內容:

import { mapState, mapGetters, mapActions } from 'vuex'

//...
export default {
  //...
  methods: {
    ...mapActions([
      'login'
    ]),
    async onSubmit() {
      const result = await this.login(this.userId);
      if(result) {
        this.$router.push('chat');
      }
    }
  }
}
複製代碼

爲了加載env.local的數據須要重啓Vue.js服務,若是看到任何未使用變量的錯誤,先忽略它們,一旦完成這些,導航到http://localhost:8080/測試一下登陸功能:

在上面的例子中,我使用不正確的用戶名,就是要確認一下錯誤處理功能能夠成功執行。

上面的截屏中,使用的是正確的用戶名。我還打開了瀏覽器的console選項卡確保user對象有值。若是你在Chrome或Firefox上安裝了Vue.js Dev Tools的話會更好,能夠看到更多詳細的信息。

到目前爲止,若是全部的功能正確執行的話,請看下一步。

訂閱房間

如今已經成功驗證過登陸功能,須要將用戶重定向到ChatDashboard視圖。使用this.$router.push('chat');進行跳轉。然而login操做須要返回一個Boolean值來決定何時是能夠跳轉到ChatDashboard視圖,還須要從ChatKit服務上獲取實際的數據填充RoomList和UserList組件。

更新src/chatkit.js的代碼:

//...
import moment from 'moment'
import store from './store/index'

//...
function setMembers() {
  const members = activeRoom.users.map(user => ({
    username: user.id,
    name: user.name,
    presence: user.presence.state
  }));
  store.commit('setUsers', members);
}

async function subscribeToRoom(roomId) {
  store.commit('clearChatRoom');
  activeRoom = await currentUser.subscribeToRoom({
    roomId,
    messageLimit: MESSAGE_LIMIT,
    hooks: {
      onMessage: message => {
        store.commit('addMessage', {
          name: message.sender.name,
          username: message.senderId,
          text: message.text,
          date: moment(message.createdAt).format('h:mm:ss a D-MM-YYYY')
        });
      },
      onPresenceChanged: () => {
        setMembers();
      },
      onUserStartedTyping: user => {
        store.commit('setUserTyping', user.id)
      },
      onUserStoppedTyping: () => {
        store.commit('setUserTyping', null)
      }
    }
  });
  setMembers();
  return activeRoom;
}

export default {
  connectUser,
  subscribeToRoom
}
複製代碼

若是看過hooks這一節,就知道ChatKit服務有用於和客戶端應用進行通迅的事件處理器,能夠在這裏看完整的文檔。我將快速的總結一下每一個鉤子方法的做用:

  • onMessage 接收消息
  • onPresenceChanged 當用戶登進登出觸發的事件
  • onUserStartedTyping 用戶鍵入觸發的事件
  • onUserStoppedTyping 用戶中止鍵入觸發的事件 要使onUserStartedTyping實現,須要在用戶輸入時從MessageForm中發出一個鍵入事件,下一節中咱們再對此進行研究。

用下面的代碼更新src/store/actions.js中的login函數:

//...
try {
  //... (place right after the `setUser` commit statement)
  // Save list of user's rooms in store const rooms = currentUser.rooms.map(room => ({ id: room.id, name: room.name })) commit('setRooms', rooms); // Subscribe user to a room const activeRoom = state.activeRoom || rooms[0]; // pick last used room, or the first one commit('setActiveRoom', { id: activeRoom.id, name: activeRoom.name }); await chatkit.subscribeToRoom(activeRoom.id); return true; } catch (error) { //... } 複製代碼

在保存代碼以後,回到登陸頁,再輸入正確的用戶名,應該是看到下面這樣的頁面。

若是遇到了問題

若是遇到了問題,能夠嘗試如下操做:

  • 重啓Vue.js服務
  • 清徐瀏覽器緩存
  • 強重置或刷新(在Chrome下若是Console選項卡打開,能夠按住刷新5秒鐘)
  • 使用瀏覽器控制檯清除localStorage
    若是目前一切正常執行,繼續下一節,下一節實現切換房間的邏輯。

切換房間

這部分很是簡單,由於基礎已經打好了。首先,建立一個容許用戶切換房間的方法,打開src/store/actions.js在login方法處理器後添加該函數:

async changeRoom({ commit }, roomId) {
  try {
    const { id, name } = await chatkit.subscribeToRoom(roomId);
    commit('setActiveRoom', { id, name });
  } catch (error) {
    handleError(commit, error)
  }
},
複製代碼

接下來,打開src/componenents/RoomList.vue更新script部分代碼以下:

import { mapState, mapActions } from 'vuex'
//...
export default {
  //...
  methods: {
    ...mapActions([
      'changeRoom'
    ]),
    onChange(room) {
      this.changeRoom(room.id)
    }
  }
}
複製代碼

回想一下,已經在b-list-group-item元素中定義了@click="onChange(room)",點擊RoomList組件中的項測試一下這個新功能。

點擊每一個房間,UI應該都會更新,每次選擇房間,MessageList和UserList組件都應該顯示正確的信息。下一節,將一次實現多個功能。

頁面刷新後從新鏈接

你可能注意到了,當對store/index.js作一些更新,或者刷新頁面的時候,會出現以下 錯誤:Cannot read property 'subscribeToRoom' of null,這是由於應用的state進行了重置。幸虧,在頁面刷新時,vuex-persist包將Vuex state維護在了瀏覽器的本地存儲裏。

鏈接應用和ChatKit服務端的引用也被置回了null值,爲了解決這個問題,須要執行重連操做。同時須要一種方式告訴應用頁面進行過刷新,爲了繼續進行正常的功能應用須要重連。在src/components/ChatNavbar.vue中實現了這部分的代碼,更新腳本以下:

<script>
import { mapState, mapActions, mapMutations } from 'vuex'

export default {
  name: 'ChatNavBar',
  computed: {
    ...mapState([
      'user',
       'reconnect'
    ])
  },
  methods: {
    ...mapActions([
      'logout',
      'login'
    ]),
    ...mapMutations([
      'setReconnect'
    ]),
    onLogout() {
      this.$router.push({ path: '/' });
      this.logout();
    },
    unload() {
      if(this.user.username) { // User hasn't logged out this.setReconnect(true); } } }, mounted() { window.addEventListener('beforeunload', this.unload); if(this.reconnect) { this.login(this.user.username); } } } </script> 複製代碼

分析一下事件的順序,以便可以理解從新鏈接到ChatKit服務背後的邏輯:
1.unload 當頁面刷新時,該方法會被調用,它先檢查user.username state是否進行過設置,若是是,意味着用戶沒有登出,reconnect state設置爲true
2. mounted 每次ChatNavbar.vue完成渲染該方法就會被調用,它先向事件監聽器分派一個處理器(unload),在頁面卸載前調用這個處理器(unload)。mounted內還檢查了若是 state.reconnect是true的話,登陸程序會被執行,經過這樣將聊天應用重連到ChatKit服務上。

還有個Logout功能,後面會細述這個功能。

作了以上更新以後,再試關刷新一下頁面,會看到頁面會自動,由於重連的過程是在後臺完成的,當切換房間的時候,也能完美的運行。

發送消息,檢測用戶輸入和退出登陸

先添加以下代碼來實現以上功能:

//...
async function sendMessage(text) {
  const messageId = await currentUser.sendMessage({
    text,
    roomId: activeRoom.id
  });
  return messageId;
}

export function isTyping(roomId) {
  currentUser.isTypingIn({ roomId });
}

function disconnectUser() {
  currentUser.disconnect();
}

export default {
  connectUser,
  subscribeToRoom,
  sendMessage,
  disconnectUser
}
複製代碼

函數sendMessage和disconnectUser會打包在ChatKit的模塊裏,isTyping函數會被單獨export出來。這是爲了容許MessageForm在不涉及Vuex存儲的狀況下直接發送鍵入事件。

對於sendMessage和disconnectUser,須要更新存儲以知足錯誤處理和加載狀態通知等要求。打開src/store/actions.js在changeRoom後插入以下代碼:

async sendMessage({ commit }, message) {
  try {
    commit('setError', '');
    commit('setSending', true);
    const messageId = await chatkit.sendMessage(message);
    return messageId;
  } catch (error) {
    handleError(commit, error)
  } finally {
    commit('setSending', false);
  }
},
async logout({ commit }) {
  commit('reset');
  chatkit.disconnectUser();
  window.localStorage.clear();
}
複製代碼

對於logout函數,咱們調用commit('reset')來將state重置爲原始state。這是一個基礎的從瀏覽器移除用戶信息和消息的安全功能。

下面開始更新src/components/MessageForm.vue內的表單文本框,經過添加@input指令來觸發鍵入事件。

<b-form-input id="message-input"
              type="text"
              v-model="message"
              @input="isTyping"
              placeholder="Enter Message"
              autocomplete="off"
              required>
</b-form-input>
複製代碼

如今更新src/components/MessageForm.vue中的script部分,爲了處理消息發送和觸發鍵入事件。更新以下:

<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import { isTyping } from '../chatkit.js'

export default {
  name: 'message-form',
  data() {
    return {
      message: ''
    }
  },
  computed: {
    ...mapState([
      'user',
      'sending',
      'error',
      'activeRoom'
    ]),
    ...mapGetters([
      'hasError'
    ])
  },
  methods: {
    ...mapActions([
      'sendMessage',
    ]),
    async onSubmit() {
      const result = await this.sendMessage(this.message);
      if(result) {
        this.message = '';
      }
    },
     async isTyping() {
      await isTyping(this.activeRoom.id);
    }
  }
}
</script>
複製代碼

還有在src/MessageList.vue中:

import { mapState } from 'vuex'

export default {
  name: 'message-list',
  computed: {
    ...mapState([
      'messages',
      'userTyping'
    ])
  }
}
複製代碼

如今發送消息的功能應該實現了。爲了顯示另外用戶的輸入,須要一個顯示這些信息的元素。在src/components/MessageList.vue的template中添加以下代碼片斷,添加到message-troup div以後。

<div class="user-typing">
  <small class="text-muted" v-if="userTyping">@{{ userTyping }} is typing....</small>
</div>
複製代碼

爲了測試這一功能,只須要使用另一個瀏覽器登陸其餘用戶並開始輸入內容,會看到在其餘用戶的聊天窗口中有通知出現。

完成本文只須要再完成最後一個功能logout。Vuex存儲已經有登出程序必要的代碼,咱們只須要更新一下src/components/ChatNavBar.vue,將Logout按鈕與以前指定好的onLogout函數關聯起來:

<b-nav-item href="#" @click="onLogout" active>Logout</b-nav-item>
複製代碼

這樣就能夠了,如今能夠登出而後再用另外的用戶登陸。

總結

終於到了文章的最後,ChatKit API讓咱們能在很短的時間內快速的建立一個聊天應用。若是要重頭構建一個聊天程序可能須要好幾周的時間,由於咱們還得把後臺補上。這個解決方案的優勢是咱們沒必要處理託管、數據庫管理和其餘基礎設施問題。咱們能夠構建併發布前端代碼到web、Android和IOS平臺的客戶端設備上。

相關文章
相關標籤/搜索