基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十一):进入聊天室后获取历史聊天记录

上篇教程中,学院君给大家演示了如何在用户登录后获取未读消息,今天我们进入聊天室房间,看看聊天室里发生的那些事儿。

聊天室页面初始化逻辑

聊天室页面对应的 Vue 组件是 resources/js/pages/Chat.vue,我们打开这个文件,看看该页面的初始化渲染逻辑以及与后端接口的交互。

先来看 createdmounted 这两个页面初始化阶段调用的钩子函数。

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 发起请求,告知服务端有用户进入该房间,并且在客户端通过监听 roomroomout 事件接收服务端进入房间和退出房间返回消息:

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 服务器后端编写用户进入房间和退出房间的实现代码,并且在进入和退出后更新在线用户信息,并推送给聊天室内的所有在线用户。

上一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十):用户登录后获取未读消息数

下一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十二):加入和退出聊天室房间功能实现