基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十四):发送图片消息

上篇教程我们演示了聊天室中文本/表情消息发布,今天我们来看看图片消息如何发布。

前端交互代码

我们还是从前端组件开始,在聊天室组件 Chat.Vue 中,客户端图片发送核心逻辑位于 fileup 方法中,我们需要对原来的代码进行调整以适配基于 Laravel + Swoole 的后端接口:

fileup() {
    const that = this;
    const file1 = document.getElementById('inputFile').files[0];
    if (file1) {
      const formdata = new window.FormData();
      formdata.append('file', file1);
      formdata.append('api_token', this.auth_token);
      formdata.append('roomid', that.roomid);
      this.$store.dispatch('uploadImg', formdata);
      const fr = new window.FileReader();
      fr.onload = function () {
        const obj = {
          username: that.userid,
          src: that.src,
          img: fr.result,
          msg: '',
          roomid: that.roomid,
          time: new Date(),
          api_token: that.auth_token
        };
        socket.emit('message', obj);
      };
      fr.readAsDataURL(file1);
      this.$nextTick(() => {
        this.container.scrollTop = 10000;
      });
    } else {
      console.log('必须有文件');
    }
},

当我们点击聊天室房间里的相机图标,即可弹开图片上传窗口:

Swoole聊天室发送图片消息

选中图片后,就会调用上述 fileup 方法上传图片。

这里面涉及两块逻辑:首先会调用后端基于 HTTP 协议的接口上传图片并保存消息到 messages 表,上传成功后才会发送消息到 Websocket 服务器,再由 Websocket 服务器将消息广播给所有在线用户。

上传图片对应的是这行代码:

this.$store.dispatch('uploadImg', formdata);

最终调用后端接口的代码位于 resources/js/api/server.js 中:

// 上传图片
postUploadFile: data => Axios.post('/file/uploadimg', data, {
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
}),

我们马上会在后端编写这个接口。

基于 Websocket 发送图片消息对应的是这行代码:

socket.emit('message', obj);

这与之前发送文本消息的代码并无区别,无非是 obj 里面新增了 img 字段而已。

图片上传接口

接下来,我们在 Laravel 后端编写图片上传接口。

routes/api.php 中新增路由 file/uploadimg

Route::middleware('auth:api')->group(function () {
    ...
    Route::post('/file/uploadimg', 'FileController@uploadImage');
}

然后通过 Artisan 命令创建控制器 FileController

php artisan make:controller FileController

在新生成的控制器文件 app/Http/Controllers/FileController.php 中编写 uploadImage 实现代码如下:

<?php
namespace App\Http\Controllers;

use App\Message;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function uploadImage(Request $request)
    {
        if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->has('roomid')) {
            return response()->json([
                'data' => [
                    'errno' => 500,
                    'msg'   => '无效的参数(房间号/图片文件为空或者无效)'
                ]
            ]);
        }
        $image = $request->file('file');
        $time = time();
        $filename = md5($time . mt_rand(0, 10000)) . '.' . $image->extension();
        $path = $image->storeAs('images/' . date('Y/m/d', $time), $filename, ['disk' => 'public']);
        if ($path) {
            // 图片上传成功则将对应图片消息保存到 messages 表
            $message = new Message();
            $message->user_id = auth('api')->id();
            $message->room_id = $request->post('roomid');
            $message->msg = '';  // 文本消息留空
            $message->img = Storage::disk('public')->url($path);
            $message->created_at = Carbon::now();
            $message->save();
            return response()->json([
                'data' => [
                    'errno' => 200,
                    'msg'   => '保存成功'
                ]
            ]);
        } else {
            return response()->json([
                'data' => [
                    'errno' => 500,
                    'msg'   => '文件上传失败,请重试'
                ]
            ]);
        }
    }
}

这里主要涉及到图片上传和消息保存逻辑。由于我们将图片保存到了 storage/public 目录下,为了让图片可以通过 Web URL 请求到,需要为 storage 目录创建软链:

php artisan storage:link

注:想了解更多图片上传及保存的细节,请参考 Laravel 请求文档文件存储文档

Websocket 服务端广播

最后我们在 routes/websocket.phpmessage 频道中补充图片消息处理逻辑:

WebsocketProxy::on('message', function (WebSocket $websocket, $data) {
    ...
    // 获取消息内容
    $msg = $data['msg'];
    $img = $data['img'];
    $roomId = intval($data['roomid']);
    $time = $data['time'];
    // 消息内容(含图片)或房间号不能为空
    if((empty($msg)  && empty($img))|| empty($roomId)) {
        return;
    }
    // 记录日志
    Log::info($user->name . '在房间' . $roomId . '中发布消息: ' . $msg);
    // 将消息保存到数据库(图片消息除外,因为在上传过程中已保存)
    if (empty($img)) {
        $message = new Message();
        $message->user_id = $user->id;
        $message->room_id = $roomId;
        $message->msg = $msg;  // 文本消息
        $message->img = '';  // 图片消息留空
        $message->created_at = Carbon::now();
        $message->save();
    }
    // 将消息广播给房间内所有用户
    $room = Count::$ROOMLIST[$roomId];
    $messageData = [
        'userid' => $user->email,
        'username' => $user->name,
        'src' => $user->avatar,
        'msg' => $msg,
        'img' => $img,
        'roomid' => $roomId,
        'time' => $time
    ];
    $websocket->to($room)->emit('message', $messageData);
    ...

非常简单,只是在之前广播消息的字段中添加了客户端上传的图片消息字段,没有任何其它逻辑。

至此,我们就可以完成了图片消息发送相关的前后端代码,接下来,我们在聊天室界面测试下图片消息的发送。

测试图片消息发布

开始之前,重新编译前端资源:

npm run dev

使前端代码修改生效。以及重启 Swoole HTTP 及 WebSocket 服务器:

bin/laravels restart

让后端代码修改生效。

然后,分别在 Chrome 和 Firefox 浏览器打开聊天室,登录并进入同一个房间,即可相互实时发送图片消息:

Swoole聊天室发送图片消息

到这里,我们已经完成了聊天室的主体功能,接下来,我们将优化项目代码,尤其是后端 WebSocket 通信的性能和优雅性。

上一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十三):发送文本/表情消息

下一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十五):实现用户头像上传功能