[译]用Pusher和vue.js创建一个即时聊天app
Posted 鱼头的Web海洋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]用Pusher和vue.js创建一个即时聊天app相关的知识,希望对你有一定的参考价值。
•译者:蔡晓婷•知乎:https://www.zhihu.com/people/c-x-t/activities•原文:https://www.sitepoint.com/pusher-vue-real-time-chat-app/
前言:三脚猫功夫的英文,仅供参考。
即时通讯社交App如今越来越火,因为它们提供了更顺畅更自然地用户体验。
在这篇教程里,我们将会创建一个即时通讯应用,这个应用使用了vue.js和由Pusher提供的ChatKit[1]服务。ChatKit[2]服务提供给我们构建一个任何设备都能使用的聊天应用完整的后端支持。让我们仅关注构建前端用户界面,通过ChatKit客户端软件包去链接ChatKit服务。
学习前提
本教程是中级到高级的教程,你必须熟悉以下概念:
1.Vue.js基础
2.Vuex基本原理
3.会使用CSS框架
你同时需要安装Node在你的电脑上,你可以去官网[3]上下载,或者使用版本管理工具[4],这可能是最简单的方法,因为它允许你在同一台电脑上管理多个版本的Node。
最后你需要全局安装Vue CLI 通过以下命令
npm install -g @vue/cli
写这个教程时,Node的最新版本是10.14.1,Vue CLI的最新版本是3.2.1
项目介绍
我们将会构建一个类似Slack或者Discord的初级聊天应用,这个应用可以做以下这些事
1.有多个聊天频道和房间
2.列出房间成员列表并检测其目前的状态
3.检测用户何时开始输入
如先前提到的,我们只构建前端,ChatKit服务提供了一个后端界面允许我们去管理用户,用户权限,和房间。
你可以在GitHub[5]上找到完整的代码。
配置一个 ChatKit实例
让我们创建我们的ChatKit实例,它是和服务器实例相似的,如果你熟悉Discord。
选择你最合适的方法,然后写上你的详情信息,比如 姓名,账户类型,用户角色等。
点击完成入门,你将进入主Pusher仪表板。在这里,您应该点击ChatKit产品。Pusher仪表板 点击创建按钮,创建一个ChatKit实例,我把它叫做VueChatTut
.创建一个ChatKit实例 (注:这是译者自己加的)选择个人的 我们将在本教程里使用免费计划,它支持高达1000的单独的用户,这足以满足我们的需要,菜单栏转到Console标签,你需要创建一个新的用户作为开始,点击Create User按钮。我将其命名为“john”(User Identifier)和“John Wick” (Display Name),你可以根据你的需要命名。下一部分简单,创建两个或更多的用户,例如:
•salt, Evelyn Salt•hunt, Ethan Hunt
创建三个或更多的房间并分配用户,例如:
•General (john, salt, hunt)•Weapons (john, salt)•Combat (john, hunt)
以下是控制台界面快照接下来,你可以到ROOMS选项,然后选择一个用户创建一条消息到每个房间里,这是出于测试的目的。然后跳到Credentials选项,并记录实例定位,我们需要激活Test Token Provider,用于生成我们HTTP端点,同时也是做标记。
搭建Vue.js项目
打开你的terminal然后根据以下内容创建一个项目:
vue create vue-chatkit
然后根据下面图片进行选择一定要确保你选择了 Babel, Vuex and Vue Router作为附加功能。接下来,创建下图的一样的文件夹和文件确保创建了上图所有的文件夹和文件,删除上面没有出现的不必要的文件。
对于那些使用控制台的人,以下是执行所有操作的命令:
mkdir src/assets/css
mkdir src/store
touch src/assets/css/{loading.css,loading-btn.css}
touch src/components/{ChatNavBar.vue,LoginForm.vue,MessageForm.vue,MessageList.vue,RoomList.vue,UserList.vue}
touch src/store/{actions.js,index.js,mutations.js}
touch src/views/{ChatDashboard.vue,Login.vue}
touch src/chatkit.js
rm src/components/HelloWorld.vue
rm src/views/{About.vue,Home.vue}
rm src/store.js
当你完成后,src文件夹的内容应该看起来祥这样:
.
├── App.vue
├── assets
│ ├── css
│ │ ├── loading-btn.css
│ │ └── loading.css
│ └── logo.png
├── chatkit.js
├── components
│ ├── ChatNavBar.vue
│ ├── LoginForm.vue
│ ├── MessageForm.vue
│ ├── MessageList.vue
│ ├── RoomList.vue
│ └── UserList.vue
├── main.js
├── router.js
├── store
│ ├── actions.js
│ ├── index.js
│ └── mutations.js
└── views
├── ChatDashboard.vue
└── Login.vue
loading-btn.css[7]和loading.css[8]文件,你可以在loading.io[9]网站上找到它们,这些文件在npm库中不可使用,所以你需要手动下载它们,然后放进你的项目。确保阅读文档能知道怎么去使用他们的自定义程序。
接下来,我们将会安装以下这些依赖:
•@pusher/chatkit-client[10],ChatKit服务的实时客户端界面•bootstrap-vue[11], CSS框架•moment[12],日期和时间格式化程序•vue-chat-scroll[13], 添加新内容时,它将自动滚动到底部•vuex-persist[14], 将Vuex状态保存在浏览器的本地存储中
npm i @pusher/chatkit-client bootstrap-vue moment vue-chat-scroll vuex-persist
点击上面的链接去学习更多每个package做的事,和如何配置它们
现在,让我们配置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
})
vuex-persist可以确保页面在重新加载和刷新之间保存Vuex状态。
我们的项目应该能够正确编译了,但是我们先不运行它,因为我们需要构建用户界面。
构建UI界面
更新 src/App.vue 的代码:
<template>
<div id="app">
<router-view/>
</div>
</template>
接下来,我们需要定义UI组件工作时所需要的Vuex store states,我们在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
是由UI使用去决定是否运行CSS loader,error 在错误发生时保存错误状态信息。
接下来打开 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>
先前提到,这是一个高级教程,如果你对代码有任何困难和不理解的地方,请先看学习前提和项目依赖的信息。
我们现在可以开始运行Vue服务通过 npm run serve 去确保我们的应用运行无任何编译问题。你可以通过输入用户名来确认验证工作正常,输入三个字符后,你应该看到login按钮被激活,login按钮目前还不能工作,因为这一块内容我们并没有代码。我们稍后在研究,现在让我们继续构建我们的用户聊天界面。
到 src/view/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[15]检查所有东西是否运行。检查terminal和控制台以确保此时没有错误,你现在应该看到以下画面非常空,对吗?让我们到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
package.去和ChatKit服务进行交互。
返回ChatKit仪表盘界面。复制instance和test 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/测试登录功能:在上面的示例中,我使用了错误的用户名,只是为了确保错误处理功能正常运行。在上面这张快照里,我使用了正确的用户名,同时我打开了控制台,确认user对象里有数据。如果你有安装Vue.js Dev Tools in Chrome[16] 或者 Firefox[17],那就更好了,这样你能看到更多具体的信息。如果这个时候所有东西都运行正常的话,我们进行下一步
订阅一个房间
现在我们成功验证了登录功能可以顺利工作,我们需要将用户重定向到ChatDashboard
页面, this.$router.push('chat');
代码就是做这个事的。 然而我们进行登录跳转的时候,需要返回一个布尔值去确认何时跳转到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服务提供的事件处理程序来与我们客户端进行通信,你可以在这里[18]看到所有的文件资料,我会快速总结每个hooks方法的目的:
•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) {
//...
}
保存代码,返回登录界面,然后输入正确的用户名之后,你可以看到以下画面:很好!几乎所有的组件都能连接Vuex正常工作了,试着通过ChatKit仪表盘页面的控制台界面发送一条消息。创建一条消息并发送到General
room。你应该可以看到新消息在MessageList
组件中自动弹出。很快,我们将实现从Vue.js应用发送消息的逻辑。
如果你遇到了问题
如果遇到问题,请尝试以下操作:
•重新启动 Vue.js server•清除浏览器缓存•重新设置或刷新•使用浏览器控制台清除localStorage
如果这个时候所有东西都ok的话,继续我们下一部分,我们在changing rooms中实现逻辑。
Changing Rooms
这个部分非常简单,因为我们已经奠定了基础,首先,我们先创建一个允许用户去change rooms的一个操作,到src/store/actions.js
文件,然后添加在登录操作处理之后的异步函数:
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
包通过保存在浏览器的localStorage在页面刷新之间维持了Vuex的状态。
不幸的是,我们的应用程序连接到ChatKit服务的引用被重置为null。为了修复这个问题,我们需要执行重新连接操作。我们还需要一种方法来告诉我们的应用程序刚刚发生了页面重新加载,并且我们的应用程序需要重新连接才能继续正常运行。我们将会实现这个功能在src/components/ChatNavbar.vue
, 更新script模块的代码
<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
是已设置. 如果设置了, 就意味着用户尚未退出. reconnect
设置为true
2.mounted
. 当ChatNavbar.vue
渲染时就会调用此方法.它会事先在页面卸载之前分配一个处理程序去调用监听的事件,同时检查 state.reconnect
是否已经设置为true. 如果设置了, 登录程序就会被执行, 从而将我们的应用程序和ChatKit 服务重新连接。
我还会加入Logout
功能 ,稍后进行添加。
做了这些改变后,试着刷新页面,你将会看到页面自动更新,因为它会在后台执行重新连接的过程。当你切换rooms时,可以看到它正常工作。
发送消息,检测用户输入和用户退出
在src/chatkit.js
加入以下代码:
//...
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
中, 我们需要更新 store 以解决像错误处理和加载状态通知之类的事情like . 在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')
去重置到原始状态,这是一项基本的安全功能,可以从浏览器缓存中删除用户信息和消息。
更新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-group
div后以下代码
<div class="user-typing">
<small class="text-muted" v-if="userTyping">@{{ userTyping }} is typing....</small>
</div>
要测试此功能,只需使用其他浏览器以其他用户身份登录并开始输入即可。你应该会在另一用户的聊天窗口中看到一条消息通知。让我们通过完成最后一个功能,logout来结束本教程,我们vuex中已经有必须的代码去处理logout进程了,所以我们需要更新src/components/ChatNavBar.vue
.只需连接Logout按钮和onLogout
点击事件 :
<b-nav-item href="#" @click="onLogout" active>Logout</b-nav-item>
现在,您可以退出登录并以其他用户身份再次登录。
总结
我们现在到了教程的结尾部分,ChatKit API能够让我们在短时间内构建一个聊天程序,如果我们要从头开始构建一个相似的程序,要花费好几个星期,因为我们还必须充实后端,这个方案的优点在于我们不需要处理托管,管理数据库和其他基础架构问题,我们只需简单得构建和部署前端代码到web、安卓和ios的客户端。
请仔细阅读文档,因为本教程没有显示大量的后端功能。如果有时间,您可以轻松构建一个功能强大的聊天应用,该应用程序可以与Slack和Discord等流行的聊天产品匹敌。
References
[1]
ChatKit: https://link.zhihu.com/?target=https%3A//pusher.com/chatkit[2]
ChatKit: https://link.zhihu.com/?target=https%3A//pusher.com/chatkit[3]
官网: https://link.zhihu.com/?target=https%3A//nodejs.org/en/[4]
版本管理工具: https://link.zhihu.com/?target=https%3A//www.sitepoint.com/quick-tip-multiple-versions-node-nvm/[5]
GitHub: https://link.zhihu.com/?target=https%3A//github.com/sitepoint-editors/vue-chatkit[6]
ChatKit: https://link.zhihu.com/?target=https%3A//pusher.com/chatkit[7]
loading-btn.css: https://link.zhihu.com/?target=https%3A//loading.io/css/loading-btn.css[8]
loading.css: https://link.zhihu.com/?target=https%3A//loading.io/css/loading.css[9]
loading.io: https://link.zhihu.com/?target=https%3A//loading.io/button/[10]
@pusher/chatkit-client: https://link.zhihu.com/?target=https%3A//docs.pusher.com/chatkit/reference/javascript[11]
bootstrap-vue: https://link.zhihu.com/?target=https%3A//bootstrap-vue.js.org/[12]
moment: https://link.zhihu.com/?target=https%3A//momentjs.com/[13]
vue-chat-scroll: https://link.zhihu.com/?target=https%3A//www.npmjs.com/package/vue-chat-scroll[14]
vuex-persist: https://link.zhihu.com/?target=https%3A//www.npmjs.com/package/vuex-persist[15]
http://localhost:8080/chat: https://link.zhihu.com/?target=http%3A//localhost%3A8080/chat[16]
Vue.js Dev Tools in Chrome: https://link.zhihu.com/?target=https%3A//chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd[17]
Firefox: https://link.zhihu.com/?target=https%3A//addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/[18]
这里: https://link.zhihu.com/?target=https%3A//pusher.com/docs/chatkit/reference/javascript
以上是关于[译]用Pusher和vue.js创建一个即时聊天app的主要内容,如果未能解决你的问题,请参考以下文章
使用 laravel5.2 、 vue.js 和 pusher 实时获取数据
Java Server Pusher Chat:需要在Java服务器和android上进行Pusher一对一聊天的架构[关闭]
Laravel Echo 在 Laravel 8-vue js "^2.5.17" 版本中没有收到 Pusher 事件