[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


<<:  在k8s上架设ELK教学

>>:  使用档案救援软件是否安全?

【从零开始的 C 语言笔记】第二十篇-While Loop(2)

不怎麽重要的前言 上一篇介绍了while loop的概念,让大家在回圈的使用上可以相对的弹性。 这次...

Day 04 : 找不出的零钱 Non-constructible Change

先来看一下题目 Given an array of positive integers (repre...

JavaScript入门 Day16_阵列2

昨天讲到了阵列,那今天要讲怎麽让阵列的资料呈现在网页上 在阵列里,第一个资料的位置不是1而是0 所以...

Day 27: 暴力破解 WPA/WPA2 加密 wifi 密码

Day 27: 暴力破解 WPA/WPA2 加密 wifi 密码 tags: Others 自我挑战...

【在厨房想30天的演算法】Day 05 资料结构之冰箱整理术

Aloha!又是我少女人妻 Uerica!昨晚跟朋友聊天突然发现,如果没有最终目标或目标不够明确,那...