基于 Swoole 开发实时在线聊天室(十一):进入聊天室后获取历史聊天记录
在上篇教程中,学院君给大家演示了如何在用户登录后获取未读消息,今天我们进入聊天室房间,看看聊天室里发生的那些事儿。
聊天室页面初始化逻辑
聊天室页面对应的 Vue 组件是 resources/js/pages/Chat.vue
,我们打开这个文件,看看该页面的初始化渲染逻辑以及与后端接口的交互。
先来看 created
和 mounted
这两个页面初始化阶段调用的钩子函数。
created 钩子函数
在 created
方法中,会从当前页面 URL 查询字符串中获取房间信息(Tips:聊天室房间的 URL 是 http://webchats.test/#/chat?roomId=1
):
async created() {
const roomId = queryString(window.location.href, 'roomId');
this.roomid = roomId;
if (!roomId) {
this.$router.push({path: '/'});
}
if (!this.userid) {
// 防止未登录
this.$router.push({path: '/login'});
}
const res = await url.getNotice();
this.noticeList = res.data.noticeList;
if (res.data.version !== res.data.version) {
this.noticeBar = false;
}
this.noticeVersion = res.data.version;
},
如果房间信息为空会跳转到首页,如果用户未登录,会跳转到登录界面。
然后调用 url.getNotice()
获取公告信息,对应的后端接口调用位于 resources/js/api/server.js
:
// 请求公告
getNotice: () => Axios.get('https://s3.qiufengh.com/config/notice-config.js'
这一块不属于核心逻辑,具体细节忽略。
mounted 钩子函数
接下来看 mounted
函数的逻辑:
async mounted() {
// 微信 回弹 bug
ios();
this.container = document.querySelector('.chat-inner');
// socket内部,this指针指向问题
const that = this;
await this.$store.commit('setRoomDetailInfos');
await this.$store.commit('setTotal', 0);
const obj = {
name: this.userid,
src: this.src,
roomid: this.roomid
};
socket.emit('room', obj);
socket.on('room', function (obj) {
that.$store.commit('setUsers', obj);
});
socket.on('roomout', function (obj) {
that.$store.commit('setUsers', obj);
});
loading.show();
setTimeout(async () => {
const data = {
total: +this.getTotal,
current: +this.current,
roomid: this.roomid
};
this.isloading = true;
await this.$store.dispatch('getAllMessHistory', data);
this.isloading = false;
loading.hide();
this.$nextTick(() => {
this.container.scrollTop = 10000;
});
}, 500);
this.container.addEventListener('scroll', debounce(async (e) => {
if (e.target.scrollTop >= 0 && e.target.scrollTop < 50) {
this.$store.commit('setCurrent', +this.getCurrent + 1);
const data = {
total: +this.getTotal,
current: +this.getCurrent,
roomid: this.roomid
};
this.isloading = true;
await this.$store.dispatch('getAllMessHistory', data);
this.isloading = false;
}
}, 50));
this.$refs.emoji.addEventListener('click', function(e) {
var target = e.target || e.srcElement;
if (!!target && target.tagName.toLowerCase() === 'span') {
that.chatValue = that.chatValue + target.innerHTML;
}
e.stopPropagation();
});
},
这里首先会初始化房间信息和消息总数,然后对 Websocket 服务器通道路由 room
发起请求,告知服务端有用户进入该房间,并且在客户端通过监听 room
和 roomout
事件接收服务端进入房间和退出房间返回消息:
socket.emit('room', obj);
socket.on('room', function (obj) {
that.$store.commit('setUsers', obj);
});
socket.on('roomout', function (obj) {
that.$store.commit('setUsers', obj);
});
在回调函数中我们通过 Vuex 设置房间在线用户信息。
这里为了实现 Laravel 后端基于 API 接口的认证,我们统一在 obj
中新增 api_token
字段:
const obj = {
name: this.userid,
src: this.src,
roomid: this.roomid,
api_token: this.auth_token
};
当然,我们需要在计算属性 computed
中设置 auth_token
才能让上述代码生效:
computed: {
... // 其他配置
...mapState({
userid: state => state.userInfo.userid,
src: state => state.userInfo.src,
auth_token: state => state.userInfo.token,
})
},
继续看 mounted
中的逻辑,接下来通过 setTimeout
定义了一个定时器,主要是从服务端获取该聊天室房间的历史聊天记录,同样,我们在请求字段中新增了 api_token
字段:
const data = {
total: +this.getTotal,
current: +this.current,
roomid: this.roomid,
api_token: this.auth_token
};
this.isloading = true;
await this.$store.dispatch('getAllMessHistory', data);
getAllMessHistory
最终请求的后端接口定义在 resources/js/api/server.js
中:
// 获取当前房间所有历史聊天记录
RoomHistoryAll: data => Axios.get('/history/message', {
params: data
}),
这里面请求参数中的 total
表示消息总数,current
表示当前页面(或者说当前这一屏消息,因为消息总量可能很大,这里有必要做分页处理),roomid
表示房间 ID,api_token
用于接口认证。
我们稍后需要在后端实现这个历史聊天记录接口,这是一个 HTTP 请求。
最后是两个组件的事件监听定义:一个是聊天窗口滚动操作的处理,一个是 Emoji 表情图标点击后的处理。需要注意的是滚动聊天窗口时,也会涉及到调用历史聊天记录接口,这其实就是我们在手机 App 中非常熟悉的下拉刷新,我们需要在这里的 obj
参数中也加入 api_token
字段:
const data = {
total: +this.getTotal,
current: +this.getCurrent,
roomid: this.roomid,
api_token: this.auth_token
};
可以看到在聊天室页面初始化阶段,最重要的两件事情是与 WebSocket 服务器建立联系,告诉它用户进入某个房间了,然后接下来获取这个房间的历史聊天记录并渲染到聊天界面中。
接下来,我们在后端来实现相应的 Websocket 交互逻辑和 HTTP 路由接口。
历史聊天记录后端接口
我们先来实现从后端获取历史聊天记录的接口。
先创建控制器 MessageController
:
php artisan make:controller MessageController
然后编写这个控制器 app/Http/Controllers/MessageController.php
实现代码如下:
<?php
namespace App\Http\Controllers;
use App\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MessageController extends Controller
{
/**
* 获取历史聊天记录
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function history(Request $request)
{
$roomId = intval($request->get('roomid'));
$current = intval($request->get('current'));
$total = intval($request->get('total'));
if ($roomId <= 0 || $current <= 0) {
Log::error('无效的房间和页面信息');
return response()->json([
'data' => [
'errno' => 1
]
]);
}
// 获取消息总数
$messageTotal = Message::where('room_id', $roomId)->count();
$limit = 20; // 每页显示20条消息
$skip = ($current - 1) * 20; // 从第多少条消息开始
// 分页查询消息
$messageData = Message::where('room_id', $roomId)->skip($skip)->take($limit)->orderBy('created_at', 'desc')->get();
// 返回响应信息
return response()->json([
'data' => [
'errno' => 0,
'data' => $messageData,
'total' => $messageTotal,
'current' => $current
]
]);
}
}
目前这个控制器只提供了 history
方法,用于返回 JSON 格式的历史聊天记录。
最后我们在 API 路由文件 routes/api.php
中定义这个路由,该路由需要认证后才能访问,而由于这个请求会包含 api_token
字段,所以认证工作会通过 auth:api
中间件自动完成:
Route::middleware('auth:api')->group(function () {
...
Route::get('/history/message', 'MessageController@history');
});
接下来,就可以在前端测试这个请求了,由于目前还没有任何消息,所以现在返回的数据为空:
下一篇教程,我们将会在 Websocket 服务器后端编写用户进入房间和退出房间的实现代码,并且在进入和退出后更新在线用户信息,并推送给聊天室内的所有在线用户。
1 Comment
学院君,有个问题请教下: 聊天页面中显示 历史聊天记录 时,因为每次请求20条历史记录显示,
如果现在有26条聊天记录,
聊天页面中最下方显示的是 当前用户 最早的20条聊天记录,而不是 当前用户 最近的聊天记录,向上滑动页面刷新后,当前用户最新的 历史聊天记录 是显示在页面的上部。
能否在将 当前用户的聊天记录 在分段页显示时,按时间轴的先后次序由上到下(顶部是最早的20条,依次往下到最下方显示的就是最新的20条)显示? 谢谢。