[Day 52] 留言板後台及前台(八) - 加入圖片上傳

留言板後台及前台

在正文之前要說一下,
其實我覺得在留言板用文字編輯器不是個好主意,
反而應該放在心情隨筆的地方,
(但也不是不行, 譬如像Facebook這樣, 有點像結合了留言板跟心情隨筆功能)
不過因為之前有過失敗的經驗,
所以把最困難的放在最後面,
這兩天研究很久終於研究出來了...

今天要解決的是圖片上傳,
用之前的程式雖然可以成功完成文字編輯器的編輯,
但是缺少了圖片上傳的部分,
感覺好像少了什麼?
所以今天要來做圖片上傳的部分,
收集了各種各樣的資料,
剪剪貼貼修修補補之後,
終於完成了其中一種方式,
(有提供好幾種, 但有的沒想嘗試, 有的嘗試失敗)
我把參考資料放在最後面,
有興趣可以自己研究其他方式.
順帶一提,
我使用的是CKEditor 5,
跟以往的版本可能會有些許的不同.

首先js的部分加入一個自定義的物件

class MyUploadAdapter {
	constructor(loader) {
		// The file loader instance to use during the upload.
		this.loader = loader;
	}

	// Starts the upload process.
	upload() {
		return this.loader.file.then(
			file =>
				new Promise((resolve, reject) => {
					this._initRequest();
					this._initListeners(resolve, reject, file);
					this._sendRequest(file);
				})
		);
	}

	// Aborts the upload process.
	abort() {
		if (this.xhr) {
			this.xhr.abort();
		}
	}

	// Initializes the XMLHttpRequest object using the URL passed to the constructor.
	_initRequest() {
		const xhr = (this.xhr = new XMLHttpRequest());

		// Note that your request may look different. It is up to you and your editor
		// integration to choose the right communication channel. This example uses
		// a POST request with JSON as a data structure but your configuration
		// could be different.
        xhr.open("POST", "/image", true);
        xhr.setRequestHeader('X-CSRF-TOKEN', '<?PHP echo csrf_token() ?>');
		xhr.responseType = "json";
	}

	// Initializes XMLHttpRequest listeners.
	_initListeners(resolve, reject, file) {
		const xhr = this.xhr;
		const loader = this.loader;
		const genericErrorText = `無法上傳檔案: ${file.name}.`;

		xhr.addEventListener("error", () => reject(genericErrorText));
		xhr.addEventListener("abort", () => reject());
		xhr.addEventListener("load", () => {
            const response = xhr.response;
            
            console.log('response', response);

			// This example assumes the XHR server's "response" object will come with
			// an "error" which has its own "message" that can be passed to reject()
			// in the upload promise.
			//
			// Your integration may handle upload errors in a different way so make sure
			// it is done properly. The reject() function must be called when the upload fails.
			if (!response || response.error) {
				return reject(response && response.error ? response.error.message : genericErrorText);
			}

			// If the upload is successful, resolve the upload promise with an object containing
			// at least the "default" URL, pointing to the image on the server.
			// This URL will be used to display the image in the content. Learn more in the
			// UploadAdapter#upload documentation.
			resolve({
				default: response.url,
			});
		});

		// Upload progress when it is supported. The file loader has the #uploadTotal and #uploaded
		// properties which are used e.g. to display the upload progress bar in the editor
		// user interface.
		if (xhr.upload) {
			xhr.upload.addEventListener("progress", evt => {
				if (evt.lengthComputable) {
					loader.uploadTotal = evt.total;
					loader.uploaded = evt.loaded;
				}
			});
		}
	}

	// Prepares the data and sends the request.
	_sendRequest(file) {
		// Prepare the form data.
		const data = new FormData();

        data.append("upload", file);
        
        console.log('file:', file);

		// Important note: This is the right place to implement security mechanisms
		// like authentication and CSRF protection. For instance, you can use
		// XMLHttpRequest.setRequestHeader() to set the request headers containing
		// the CSRF token generated earlier by your application.

		// Send the request.
		this.xhr.send(data);
	}
}

// ...

function MyCustomUploadAdapterPlugin(editor) {
	editor.plugins.get("FileRepository").createUploadAdapter = loader => {
		// Configure the URL to the upload script in your back-end here!
		return new MyUploadAdapter(loader);
	};
}

其中

xhr.open("POST", "/image", true);

裡面的路徑要寫後端上傳檔案的路徑

xhr.setRequestHeader('X-CSRF-TOKEN', '<?PHP echo csrf_token() ?>');

這是Laravel需要的SCRF的驗證

另外也可以自己修改錯誤訊息(但是除非是500 Server Error, 如果傳送成功, 錯誤訊息是從後端過來)

const genericErrorText = `無法上傳檔案: ${file.name}.`;

並且JavaScript要加入CKEditor的宣告

ClassicEditor
   .create(document.querySelector("#editor"), {
        extraPlugins: [MyCustomUploadAdapterPlugin],
        toolbar: ["heading", "|", "alignment:left", "alignment:center", "alignment:right", "alignment:adjust", "|", "bold", "italic", "blockQuote", "link", "|", "bulletedList", "numberedList", "imageUpload", "|", "undo", "redo"],
   })
   .then(editor => {
       myEditor = editor;
   })
   .catch(error => {
       console.error(error);
   });

然後要寫後端接收的部分,
首先是web.php的部分

Route::group(['prefix' => '/'], function(){
    //上傳圖片
    Route::any('/image', 'HomeController@imageProcess');
});

然後是圖片接收的函式
app/Http/Controllers/HomeController.php

//接收檔案上傳
public function imageProcess()
{
    header('Content-Type: application/pdf');

    Log::notice('接收圖片資料');
    //接收輸入資料
    $input = request()->all();
    $result = array();

    Log::notice('接收圖片'.print_r($input, true));

    if(isset($input['upload']))
    {
        $upload = $input['upload'];

        //檔案副檔名
        $extension = $upload->getClientOriginalExtension();
        //產生隨機檔案名稱
        $filename = uniqid().'.'.$extension;
        //相對路徑
        $relative_path = 'images/upload/'.$filename;
        //取得public目錄下的完整位置
        $fullpath = base_path('public_html/'.$relative_path);

        //允許的檔案格式
        switch($upload->getMimeType())
        {
            case 'image/jpeg':
            case 'image/png':
                break;
            default:
                $result['error'] = array(
                    'message' => '很抱歉,只接受JPG和PNG檔案',
                );
                echo json_encode($result);
                exit;
        }

        //移動檔案位置並改名稱
        move_uploaded_file($upload->getRealPath(),$relative_path);

        $result['url'] = '/'.$relative_path;
        echo json_encode($result);
    }
    else
    {
        $result['error'] = array(
            'message' => '很抱歉,上傳檔案失敗了',
        );
        echo json_encode($result);
    }
}

最後再附上成果圖
 

https://ithelp.ithome.com.tw/upload/images/20210502/201056941f1SD8cgKo.png

到這裡這個系列的文章差不多結束了,
雖然還有些東西想寫,
不過就等年底再說了.

參考資料:
Simple upload adapter(官方文件)
[筆記]CKEditor加上CKFinder上傳圖檔更方便
如何套用 CKEditor5 上傳圖片
CKEditor 5圖片的上傳方式
CKEditor 5 教學(三),上傳圖片至 Amazon S3