作者: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产品。



- John Wick
- salt, Evelyn Salt
- hunt, Ethan Hunt
再创建三个房间并指定相应的用户。例如:
- General (john, salt, hunt)
- Weapons (john, salt)
- Combat (john, hunt)
最后,控制台界面是这样子:


搭建Vue.js项目
打开终端,像下面这样创建项目
vue create vue-chatkit
选择Manually select features并且像下面这样选择相关问题。


对于loading-btn.css他loading.css这两个文件,可以在loading.io上找到,这两件文件无法通过npm仓库获取,所以需要自己手动下载,然后放在项目中。在此最好知道这两个文件是做什么的,怎样定制化加载条。
下面,安装下面依赖:
- @pusher/chatkit-client,ChatKit服务的实时客户端接口
- bootstrap-vue, 一个CSS框架
- moment, 日期和时间格式化的实用工具
- vue-chat-scroll,当内容添加完成滚动条自动滚动到底部
- vuex-persist, 在浏览器的本地存储当中存储Vuex状态
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服务端来确认一下应用执行没有任何兼容性问题。

打开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检查一下所有内容都能正常执行。检查一下终端和浏览器的控制面板确保在这里没有错误,那么现在页面看起来是下图这个样子的。

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: {
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/测试一下登录功能:



订阅房间
现在已经成功验证过登录功能,需要将用户重定向到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>
为了测试这一功能,只需要使用另外一个浏览器登录其他用户并开始输入内容,会看到在其他用户的聊天窗口中有通知出现。

<b-nav-item href="#" @click="onLogout" active>Logout</b-nav-item>
这样就可以了,现在可以登出然后再用另外的用户登录。

总结
终于到了文章的最后,ChatKit API让我们能在很短的时间内快速的创建一个聊天应用。如果要重头构建一个聊天程序可能需要好几周的时间,因为我们还得把后台补上。这个解决方案的优点是我们不必处理托管、数据库管理和其他基础设施问题。我们可以构建并发布前端代码到web、Android和IOS平台的客户端设备上。