ServiceWorkerで画像を縮小してアップロード

## Service workerとは？

Service workerについては以下などを参考にしてください．

- [Service Worker の紹介]( https://developers.google.com/web/fundamentals/primers/service-workers/?hl=ja)


# 概要

## つくったもの

Service workerを使って画像を縮小してからアップロードするサンプルプログラムを作ってみました．

Webのファイルフォームから画像をアップロードする際に，Service workerが仲介して縮小処理をしてからアップロードを行います．

Service worker上で縮小処理を行うメリットはこれだけでは未知数ですが何かの参考になれば幸いです．

## ソースコード

作ったソースコードはGithubにアップロード済みです．動かし方についてはGithubリポジトリのREADME.mdを参考にしてくださいませ．

- https://github.com/hiroyky/service_worker_image_uploader

個人的な好みからTypeScriptで書いていますが，JavaScriptを普段書いている方なら，すぐに読めるかと思います．

# プログラムの内容
サーバに画像ファイルをアップロードするために，Service workerだけでなく，Webサーバとなるサーバサイドのコードと，UIを提供するクライアントサイドのコードがあります．

サーバサイドとクライントサイドのコードを紹介したあとに，Service workerのコードを掲載します．

なお，サーバサイドとクライアントサイドのコードのみでアップローダとしては機能します．その場合，縮小処理はありません．

画像の縮小処理には[jimp](https://www.npmjs.com/package/jimp)というライブラリを使っています．

## サーバサイド
サーバサイドはnode.jsで， Webサーバを立ち上げます． 各URLに対するシンプルな応答処理を書いています．

- ``/upload``がアップロード受け入れるエントリポイントです．
- ``/serviceworker.js``でService workerのJSファイルを返します．

アップロードされたファイルは[multer](https://www.npmjs.com/package/multer)を使って``./upload_files``ディレクトリ配下に保存します．

```typescript

import express from "express";
import fs from "fs";
import multer from "multer";
import path from "path";

const app = express();
const upload = multer({dest: "./upload_files"});

app.get("/", async (req, res) => {
    const body = await fs.promises.readFile(path.join(__dirname, "views/index.html"));

    res.header({
        "Content-type": "text/html",
    }).end(body);
} );

app.get("/serviceworker.js", async (req, res) => {
    const js = await fs.promises.readFile(path.join(__dirname, "static/js/serviceworker.js"));

    res.header({
        "Content-type": "application/javascript",
    }).end(js);
});

app.post("/upload", upload.single("upload_image"), (req, res) => {
    console.log("/upload");
    console.log(req.headers);
    console.log(req.body);
    res.end("done");
});

app.use(express.static(path.join(__dirname + "/static")));

app.listen(3000, () => {
    console.log("listen port: 3000");
});
```

## クライアントWeb

ウェブブラウザで表示・処理するコードです．fileのフォームでユーザにファイルを選択してもらい，アップロードボタンでアップロードします．

アップロードの通信自体はいわゆるAjaxで行います．通信処理は[axios](https://www.npmjs.com/package/axios)を使っています．

JavaScriptの先頭部分ではService workerの登録処理を行っています．

##### HTML:

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Service workerサンプル</title>
    <script src="js/index.js"></script>
</head>
<body>
<div>

    <input type="file" name="upload_image" accept="image/*" id="uploadImage"><br>
    <input type="button" value="アップロード" id="uploadButton">

</div>
</body>
</html>
```

##### JavaScript(TypeScript):

```typescript

import axios from "axios";

// Service workerの登録処理
if (navigator.serviceWorker !== undefined) {
    window.addEventListener("load", async () => {
        try {
            const registration = await navigator.serviceWorker.register("/serviceworker.js");
            console.log("serviceworkerの登録完了", registration);

        } catch (err) {

            console.error(err);
        }
    });
}

// アップロードボタンクリック時の処理を定義
window.addEventListener("load", () => {
    const uploadButton = document.getElementById("uploadButton");
    if (uploadButton) {
        uploadButton.addEventListener("click", upload);
    }
});

function upload(event: Event) {
    const uploadElement = document.getElementById("uploadImage") as HTMLInputElement;
    if (uploadElement.files === null) {
        return false;
    }

    const formData = new FormData();
    formData.append("upload_image", uploadElement.files[0]);

    axios.post("/upload", formData, {
        headers: {
            "Content-Type": "multipart/form-data",
        },
    });
}
```

## Service worker

Service workerのコードです．（fetchイベントのみ掲載しております．全部はgithubリポジトリで見てください）

fetchイベントの処理を書いています．fetchイベントはウェブブラウザがサーバと通信するときにトリガするイベントです．通信に割り込んで独自の処理を書くことができます．
``event.respondWith``で通常の通信処理ではなく，独自の処理を行って，独自のレスポンスをブラウザに返すことができます．この関数が実行されなかった場合，通常通りの通信が行われます．

ブラウザからのリクエストが``POST /upload``だった場合にのみ，割り込んで画像の縮小処理を行ってからサーバにリクエストを行っています．

```typescript

self.addEventListener("fetch", (event) => {
    console.log("fetch event");

    const req: Request = event.request;
    const path = new URL(req.url).pathname;

    // POST /uploadのリクエストの場合のみ処理を実行する．
    if (path === "/upload" && req.method === "POST") {

        console.log("POST /upload");

        event.respondWith(async function() {
            try {
                // リクエスト情報からフォームのアップロードファイルを抽出
                const formData = await req.formData();
                const file = formData.get("upload_image") as File | null;

                console.log(file);

                if (file === null) {
                    console.log("file is null");
                    return;
                }

                // File(Blob)から画像処理ライブラリのJimpで扱えるBufferに変換してJimpにわたす．
                // JimpのscaleToFitで縮小処理を行う．
                const data = await readFile(file) as ArrayBuffer;
                const image = await Jimp.read(Buffer.from(data));
                const dstImageBuffer = await image.scaleToFit(200, 200).getBufferAsync(image.getMIME());

                // 縮小した画像データをFormDataで送信できる形式に変換
                const dstImageArrayBuffer = bufferToArrayBuffer(dstImageBuffer);
                const dstImageBlob = new Blob([dstImageArrayBuffer]);

                // サーバにリクエストするフォームデータを改めて作成
                const dstFormData = new FormData();
                dstFormData.append("hello", "world");
                dstFormData.append("upload_image", dstImageBlob);

                console.log("begin fetch");

                // 送信
                const res = await fetch("/upload", {
                    method: "POST",
                    body: dstFormData,
                });

                console.log(res);
                return res;

            } catch (err) {
                console.log(err);
                return err;
            }

        }());

    }
});

function readFile(file: File): Promise<string | ArrayBuffer | null> {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = (e) => {
            resolve(reader.result);
        };
        reader.readAsArrayBuffer(file);
    });
}

function bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
    const ab = new ArrayBuffer(buf.length);
    const view = new Uint8Array(ab);
    for (let i = 0; i < buf.length; ++i) {
        view[i] = buf[i];
    }
    return ab;
}

```

# まとめ

Service workerで画像を縮小してからアップロードするプログラムを作ってみました．
どこかで活用できると素敵です．