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

相关文章
相关标签/搜索