用Vue.js和Pusher创建实时聊天应用

2,203 阅读7分钟

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

现如今,即时通迅已经越来越普遍,并且用户体验也越来越自然和流畅。

本文将使用ChatKit加强过的Vue.js创建一个实时聊天应用,ChatKit服务为我们提供了一个创建聊天应用的后端,并且可以运行于任何设备上,让我们只需关注前端用户接口,这个接口通过ChatKit client包连接到ChatKit服务。

准备条件

这是一篇中到高级的教程,理解本文需要对以下概念都比较熟悉:

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

还需要安装Node.js,可以直接在官网上下载安装包。 最后需要使用以下命令安装全局的Vue CLI。

npm install -g @vue/cli

在写这篇文章的时候Node版本是 10.14.1,Vue CLI的最新版本是 3.2.1。

关于例子

我们要创建一个基础的聊天应用,应用需要有如下功能:

  • 多个通道和房间
  • 列出房间内的成员并检测成员的在线状态
  • 当其他用户开始输入消息时进行监测

就像先前提到的,这里只创建前端,ChatKit服务有个可以管理用户、授权和房间的后端接口。

可以在GitHub上找到完整的代码。

设置ChatKit实例

创建ChatKit实例,类似于创建服务端实例。进入Puser网站的ChatKit页面,先注册,完成登录后进入Pusher的仪表板,然后选择ChatKit产品。

点击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平台的客户端设备上。