[Vue + Express] フロントからサーバー側の処理状況をリアルタイムで取得する.

### 実現したいこと
フロントからサーバーにリクエストし、サーバー側の処理状況をフロント側で分かるようにしたい.

今回は動画ファイルをVueからexpress経由でS3へアップロードし、S3へのアップロードの進捗状況をフロントでもわかるようにするのがゴール. 

![スクリーンショット 2021-03-09 12.45.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/ef64a69d-43fd-9c45-7653-7e3cfd33e1a0.png)


(動画など重いデータは本来フロントから直接アップしたほうが早いのですが...DBトランザクション云々の兼ね合いもありそこは割愛.)

なので、「バックエンド側で重い処理をしつつ、その処理状況をフロント側でも分かるようにしたい」という人向けの記事になります.



### どう実現するか
**WebSocket**を使います.
axios等でHTTPでリクエストする場合, アップロードの処理状況は取得できますが、バックエンドの処理状況までは取得できません.

```js

this.$axios.post(
  '/video',
  params,
  {
    headers: { 'Content-Type': 'multipart/form-data' },
    onUploadProgress: ((progress) => {
      // Vue -> Serverへのアップロード状況のみ追跡可能.
      // Server -> S3への追跡は不可
    })
  }
).then(({ data }) => { });
```


これはHTTPリクエストではフロントからバックエンドへの一方向通信しかできないためであり, バックエンドの処理状況もフロントへ通知できるようにする必要があります.(双方間通信)

定期的にAPIへ問い合わせるポーリングというやり方もあるらしいですが、色々調べた結果, 双方間通信を実現するためのWebSocketという通信規格を使えばできそうだったため、ここにメモを残しておきます.


### 実装の流れ

**[フロント側]**
1. バックエンドへのコネクションの確立.
2. 処理状況を取得するイベントを登録する.

**[バックエンド側]**
1. コネクションの確立.
2. 処理状況を通知するイベントを登録する.

### 動作環境
- socket.io(3.x)
- socket.io-client(3.x)
- express(4.x)
- vue(3.x)

事前にフロント側とバックエンド側の環境が整ってる前提のうえ、WebSocketを使えるようにinstallしておく必要があります. 今回はパッケージをinstallしてみました.

```
yarn add socket.io  # Server
yarn add socket.io-client # Client
```
サーバー側: https://socket.io/docs/v3/server-installation/
クライアント側: https://socket.io/docs/v3/client-installation/

### フロント側(Vue)の実装

一例としてやってることはコード内のコメントに残しておきます.

```Video.vue

<template>
  <div>
    <input type="file" @change="setFile">
    <button @click="uploadVideo" :disabled="!selectedVideoFile">
      アップロード
    </button>
    <h3>フロントからサーバーへのアプロード進捗</h3>
    <progress :value="progressValueToServer" max="100" />
    {{ progressValueToServer + '%' }}
    <h3>サーバーからS3へのアプロード進捗</h3>
    <progress :value="progressValueToS3" max="100" />
    {{ progressValueToS3 + '%' }}
  </div>
</template>

<script>
import { io } from "socket.io-client";

export default {
  name: 'Video',
  data() {
    return {
      selectedVideoFile: null,
      progressValueToServer: 0,
      progressValueToS3: 0,
      socket: io('http://localhost:3000')
    };
  },
  created() {
    // [socketの登録をしておく]
    // 接続されているか確認
    this.socket.on("connect", () => {
      console.log('socket.id', this.socket.id);
      console.log('接続できたか?', this.socket.connected);
    });
    // node.jsから進捗状況を取得/表示するイベントを登録.
    this.socket.on('s3UploadProgress', (percentCompleted) => {
      this.progressValueToS3 = percentCompleted;
    });
  },
  methods: {
    setFile(e) {
      const file = e.target.files[0];
      this.selectedVideoFile = file;
    },
    uploadVideo() {
      const formData = new FormData();
      formData.append('title', 'test title');
      formData.append('videoFile', this.selectedVideoFile);
      const config = {
        headers: { 'Content-Type': 'multipart/form-data' },
        onUploadProgress: this.onUpload
      }
      this.$axios.post(
        '/video',
        formData,
        config
      ).then(({ data }) => {
        console.log(data);
      }).catch(err => {
        console.error(err);
      });
    },
    // フロントからサーバーへのアップロード状況を取得.
    onUpload(progressEvent) {
      const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      console.log(percentCompleted);
      this.progressValueToServer = percentCompleted;
    },
  },
};
</script>
```
コンポーネントが作られたらSocketを初期化し、あらかじめ進捗状況が取得できるようにイベントを登録しています.


### バックエンド側(Express)の実装

socket.ioをexpressで利用する設定はこちらを参考にしてください.
https://socket.io/docs/v3/server-initialization/#With-Express

```app.js
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');

// POSTパラメータの受取設定
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// CORS(クロスドメイン)の許可
const cors = require('cors');
app.use(cors());

// サーバーにアップロードされたファイルの一時格納フォルダを指定.
const upload = require('./config/multer');

// API用の設定
const AWS = require('aws-sdk');
AWS.config.update({ region: 'ap-northeast-1' });
const db = require('./models/index.js');
const { sequelize, Video } = db;

// s3アップロード用メソッド. (記事用にメインファイルに移している.)
const uploadS3Object = (model, filePath, io) => {
  const file = fs.readFileSync(filePath);
  fs.unlink(filePath, (e) => {
    if (e) throw e;
  });
  const s3 = new AWS.S3({ apiVersion: '2006-03-01' });
  const s3UpdateParams = {
    Bucket: 'xxx',
    Key: `${model.constructor.getTableName()}/${model.id}/${path.basename(filePath)}`,
    Body: file,
  };
  return new Promise((resolve, reject) => {
    const upload = s3.upload(s3UpdateParams);
    upload.on('httpUploadProgress', (progress) => {
      const percentCompleted = Math.round((progress.loaded * 100) / progress.total);
      console.log('percentCompleted', percentCompleted);
      // ここでS3への進行状況をフロントへ投げている.
      io.emit('s3UploadProgress', percentCompleted);
    });
    upload.send((err, data) => {
      if (err) reject(err);
      console.log('オブジェクトをアップロードしました data.key', data.Key);
      resolve(data.Key);
    });
  });
};

// 動画作成用API.
app.post('/video', upload.single('videoFile'), async (req, res) => {
  // app.set('io', io)でセットしたものを取得している.
  const io = app.get('io');
  const transaction = await sequelize.transaction();
  try {
    const bodyParams = req.body;
    // id取得のため先にテーブル作成 -> idを元にs3へアップロード
    const video = await Video.create(bodyParams, { transaction });
    const file = req.file;
    const Key = await uploadS3Object(video, file.path, io);
    await video.update({ filePath: Key }, { transaction });
    transaction.commit();
    res.json(video);
  } catch (e) {
    transaction.rollback();
    console.error(e);
    res.status(500).json(e.message);
  }
});

// サーバーのlisten設定.
const httpServer = require('http').createServer(app);
httpServer.listen(3000, () => {
  console.log('server listening. Port:' + 3000);
});

// io.socketの設定.
const io = require('socket.io')(httpServer, {
  cors: {
    origin: "http://localhost:8080",
    credentials: true
  }
});
io.on('connection', (socket) => {
  console.log(`connected!, socket.id: ${socket.id}`);
});
// express内でioを使えるようグローバルにセット.
app.set('io', io);
```
少しだけ説明を加えますと、
`io`オブジェクトを外部から利用したい場合にどうすれば良いか分からなかったため、

```js

app.set('io', io) // メインファイルでset
const io = app.get('io'); // 使いたい場所でget
```
とすることで、進捗状況が取れるメソッド内でioを利用できるようにしました..
参考記事: https://stackoverflow.com/questions/47837685/use-socket-io-in-expressjs-routes-instead-of-in-main-server-js-file
(あまり例がなく, もっといいやり方があると思うので良いやり方知ってる方いましたら教えてくださいm)

以下のコードでS3の進行状況をclientにemitしていますが、Socket接続中の全ユーザーに送信(ブロードキャスト)されるので、複数ユーザーが利用するサービスでは気をつけた方が良いかもしれません.

```js

io.emit('s3UploadProgress', percentCompleted);
```
参考: https://socket.io/docs/v3/server-api/index.html


これでサーバー側の処理状況を必要に応じてフロント側へ通知する仕組みが出来上がりました.
![upload_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242611/aa3d3893-25b7-5b9a-afeb-120af0d7f0c4.gif)


### その他参考記事
s3のprogress取得: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3/ManagedUpload.html
axiosのprogress取得: https://github.com/axios/axios#request-config
io.socket: https://socket.io/docs/v3/server-installation/


### 思ったこと
websocketは本来, チャットアプリなどリアルタイムに情報を通信する際に便利ですが、一応サーバーから一方的にデータを送ることも可能でした.
今回は個人的な用途を例としたものなので、複数クライアントに向けた実装では、socketの部分をもう少し工夫する必要があるかもしれません.