Blazor整併CKEditor5(二)_圖檔上傳處理

 
通常富文本編輯器中還涉及到圖檔上傳的處裡




我們可在Blazor Server專案目錄創建webapi 目錄並新增一個API 控制器
ImageHandlerController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace XXXProj.WebApi
{
    [Route("api/Images")]
    [ApiController]
    public class ImageHandlerController : ControllerBase
    {

        [HttpPost, DisableRequestSizeLimit]
        public IActionResult Upload()
        {
            try
            {
                IFormFile file = Request.Form.Files[0];

                if (file.Length > 0)
                {
                    string strFileName = $"{Guid.NewGuid()}_{file.FileName}";
                    using var stream = new FileStream(Path.Combine($"{System.IO.Directory.GetCurrentDirectory()}{@"\wwwroot"}", "images", strFileName), FileMode.Create);
                    file.CopyTo(stream);
                    string cur_url = GetFullUrl();

                    return Ok(new { url = $"{cur_url}/{strFileName}" });
                }
                else
                {
                    return BadRequest("Image File not found");
                }
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }

        }

        private string GetFullUrl()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(HttpContext.Request.Scheme)
                .Append("://")
                .Append(HttpContext.Request.Host)
                .Append(HttpContext.Request.PathBase)
                .Append(HttpContext.Request.Path)
                .Append(HttpContext.Request.QueryString)
                .ToString();
            return sb.ToString();
        }


        [HttpGet("{fileName}")]
        public async Task<IActionResult> LoadFile(string fileName)
        {
            try
            {
                string path = Path.Combine($"{System.IO.Directory.GetCurrentDirectory()}{@"\wwwroot"}", "images", fileName);

                MemoryStream memory = new MemoryStream();
                using (FileStream stream = new FileStream(path, FileMode.Open))
                {
                    await stream.CopyToAsync(memory);
                }
                memory.Position = 0;
                return File(memory, @"application/octet-stream", Path.GetFileName(path));
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
    }
}


新增client端接收圖檔上傳用的js
editor.js外掛UploadAdapter.js

UploadAdapter.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class UploadAdapter {
    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.
        const full_cur_url = location.protocol + '//' + location.host;
        console.log("_initRequest_current url:" + full_cur_url);
        const api_endpoint = full_cur_url + '/api/Images'
        xhr.open('POST', api_endpoint, true);
        //xhr.open('POST', 'http://localhost:55322/api/Images', true);
        xhr.responseType = 'json';
    }

    // Initializes XMLHttpRequest listeners.
    _initListeners(resolve, reject, file) {
        const xhr = this.xhr;
        const loader = this.loader;
        const genericErrorText = `Couldn't upload file: ${file.name}.`;

        xhr.addEventListener('error', () => reject(genericErrorText));
        xhr.addEventListener('abort', () => reject());
        xhr.addEventListener('load', () => {
            const response = xhr.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);

        // 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);
    }
}

這邊要注意
前後端call api的end point都要記得改為動態獲取方式
避免直接寫死url endpoint 避免程式要頻繁更動來更動去的問題


再到editor.js進行擴充編輯


editor.js


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
editors = {};

function CustomUploadAdapterPlugin(editor) {
	editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
		return new UploadAdapter(loader);
	};
}

function CreateEditor(editorId, defaultValue, height, dotNetReference) {

	ClassicEditor
		.create(document.getElementById(editorId), {
			extraPlugins: [CustomUploadAdapterPlugin],
			toolbar: {
				items: [
					'heading',
					'|',
					'bold',
					'italic',
					'link',
					'bulletedList',
					'numberedList',
					'|',
					'outdent',
					'indent',
					'|',
					'imageUpload',
					'blockQuote',
					'insertTable',
					'mediaEmbed',
					'undo',
					'redo'
				]
			},
			language: 'en',
			licenseKey: '',
		})
		.then(editor => {
			editors[editorId] = editor;
			editor.setData(defaultValue);
			editor.editing.view.change(writer => {
				writer.setStyle('height', height, editor.editing.view.document.getRoot());
			});

			editor.model.document.on('change:data', () => {
				let data = editor.getData();
				dotNetReference.invokeMethodAsync('OnEditorChanged', data);
			});
		})
		.catch(error => {
			console.error(error);
		});
}
function DestroyEditor(editorId) {
	editors[editorId].destroy().then(() => delete editors[editorId])
		.catch(error => console.log(error));
}

記得_Host.cshtml要回去添加額外新建的外掛js(要在editor.js之前)


那還要記得
到Startup.cs處理同源政策的配置問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void ConfigureServices(IServiceCollection services)
{
	//....省
	services.AddCors(options =>
            {
                options.AddPolicy("any", builder =>
                {
                    //允許任何不同來源的站台應用存取
                    builder.SetIsOriginAllowed(_ => true)
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials();
                });
            });
			
	//....省
	
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	//....省
	app.UseCors("any");//必須要在app.UseRouting();之後,app.UseAuthorization();跟app.UseEndpoints之前
	//....省
}



藉此就能完善FCKEditor 富文本編輯器的圖檔上傳了


















留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header