ジャンルから探す

Web Design English

Quillの画像をAjaxでサーバーにアップロードする【Laravel】

この記事では、LaravelとQuillを組み合わせて、Quillで選んだ画像をAjaxでサーバーにアップロードする方法をまとめます。

結論から書くと、画像ボタンのハンドラを差し替えて、選ばれたファイルを FormData でPOSTし、返ってきたURLを本文に挿入します。Quill標準のままだと画像がBase64で本文に埋め込まれ、データベースに巨大な文字列が保存されてしまいます。

サーバー側は、Laravelのバリデーションとストレージを使えば数行で書けます。

スポンサーリンク

Laravelプロジェクトのセットアップ

まず、Laravelプロジェクトをセットアップします。Laravelがすでにインストールされている場合は、このステップをスキップして大丈夫です。

composer create-project --prefer-dist laravel/laravel your-project-name
cd your-project-name

Quillのセットアップと設定

Quillをプロジェクトに統合します。QuillはCDNから読み込むことができますが、ローカルにファイルをダウンロードして使用することもできます。その場合は、必要なファイルをダウンロードして、publicフォルダー内に配置します。

public/
  └── quill/
      ├── quill.js
      └── quill.css

Quillの初期化には、以下のようなJavaScriptコードを使用します。

<!DOCTYPE html>
<html>
<head>
  <!-- head部分のコード省略 -->
</head>
<body>
  <div id="editor">
    <!-- Quillエディターが表示される部分 -->
  </div>

  <script src="{{ asset('quill/quill.js') }}"></script>
  <script>
    const quill = new Quill('#editor', {
      theme: 'snow'
    });

    // 本記事の後半で、Ajaxのコード記述例と解説があります
  </script>
</body>
</html>

スポンサーリンク

画像のAjaxアップロード

Quillは、画像をサーバーにアップロードする機能がデフォルトで備わっていません。そこで、Ajaxを使用して独自の画像アップロード機能を実装します。

まず、画像をアップロードするためのルートとコントローラーを作成します。

php artisan make:controller ImageUploadController

次に、ルートをroutes/web.phpに追加します。

Route::post('/upload-image', 'ImageUploadController@upload')->name('upload.image');

画像を受け取るコントローラーの作成

結論:受け取り側では必ずバリデーションをかけ、ファイル名はサーバー側で生成してください。アップロードされた元のファイル名をそのまま使うのは危険です。

// ImageUploadController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class ImageUploadController extends Controller
{
    public function upload(Request $request)
    {
        // 種別とサイズを必ず検証する
        $request->validate([
            'image' => ['required', 'image', 'mimes:jpeg,png,webp', 'max:5120'],
        ]);

        // ファイル名はLaravelに生成させる(元のファイル名は使わない)
        $path = $request->file('image')->store('uploads', 'public');

        return response()->json([
            'success' => true,
            'image_url' => Storage::url($path),
        ]);
    }
}

元のファイル名をそのまま使わない理由は2つあります。ひとつは、日本語や記号を含む名前がサーバーによって扱えないこと。もうひとつは、細工したファイル名によって意図しない場所に保存される危険があることです。store() はランダムな名前を生成するので、この2つを同時に回避できます。

image ルールは拡張子だけでなくファイルの中身も検証するため、拡張子を偽装したファイルを弾けます。max:5120 はキロバイト単位で5MBの制限です。

Storage::url() を使うには、シンボリックリンクを作成しておく必要があります。

php artisan storage:link

スポンサーリンク

Quillの画像アップロードに関するAjaxの実装例

結論:画像ボタンのハンドラを差し替えて、選ばれたファイルをその場でアップロードします。本文を書き終えてから一括で送る方式は、Base64が本文に残ったままになるため機能しません。

<!DOCTYPE html>
<html>
<head>
  <meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
  <div id="editor"></div>

  <script src="{{ asset('quill/quill.js') }}"></script>
  <script>
    const quill = new Quill('#editor', { theme: 'snow' });

    // 画像ボタンの動作を差し替える
    const toolbar = quill.getModule('toolbar');
    toolbar.addHandler('image', function () {
      const input = document.createElement('input');
      input.setAttribute('type', 'file');
      input.setAttribute('accept', 'image/*');
      input.click();

      input.onchange = async () => {
        const formData = new FormData();
        // src の文字列ではなく File オブジェクトを渡す
        formData.append('image', input.files[0]);

        const res = await fetch('/upload-image', {
          method: 'POST',
          headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
          },
          body: formData
        });
        const data = await res.json();

        if (data.success) {
          const range = quill.getSelection(true);
          quill.insertEmbed(range.index, 'image', data.image_url);
          quill.setSelection(range.index + 1);
        }
      };
    });
  </script>
</body>
</html>

formData.append() には File オブジェクトを渡してください。image.src のような文字列を渡すと、サーバー側の hasFile()file() が反応せず、アップロードは成立しません。

CSRFトークンは <meta> タグから読みます。Bladeの {{ csrf_token() }} をJavaScriptの文字列に直接書くと、スクリプトを外部ファイルに切り出した時点で動かなくなります。

getSelection(true)true は「フォーカスが外れていたら戻す」指定です。ツールバーのボタンを押すとエディタからフォーカスが外れるため、省くと挿入位置が取得できません。

アップロード時に確認すること

結論:アップロードを受け付けるエンドポイントには、必ずファイル種別とサイズの制限をかけてください。

制限が無いと、画像以外のファイルや極端に大きなファイルを送り込まれます。Laravelならバリデーションで書けます。

$request->validate([
    'image' => ['required', 'image', 'mimes:jpeg,png,webp', 'max:5120'],
]);

$path = $request->file('image')->store('uploads', 'public');

return response()->json([
    'url' => Storage::url($path),
]);

max:5120 はキロバイト単位で、5MBの制限になります。image ルールは拡張子だけでなく実際の中身も見るので、拡張子を偽装したファイルを弾けます。

Storage::url() を使うには、シンボリックリンクを作っておく必要があります。

php artisan storage:link

アップロードされた画像は、そのままだと容量が大きいままです。圧縮の考え方はPythonで画像圧縮GUIアプリを作るにまとめています。写真はJPEG、図版はPNGを減色、という判断基準は言語を問わず同じです。

関連する記事

Quillの導入がまだの場合は、Laravelにリッチテキストエディタ『Quill』を実装するから先にどうぞ。

画像挿入モジュールの設定はQuillエディタにimgタグで画像を添付するモジュールに、ツールバーのボタン追加はQuillのツールバーにカスタムボタンを追加するにまとめています。

まとめ

この記事では、LaravelとQuillを組み合わせて、Quillでアップロードされた画像をAjaxを使用してサーバーにアップロードする方法をまとめてみました。

Quillに画像アップロード機能を実装することで、ユーザーが簡単に画像を挿入できるようになります。

▼HTML/CSS/JavaScriptの学習本を探してる方はこちらの記事をどうぞ