如何在 Angular 中实现 CKEditor5 并将图像上传到 ASP.NET Core Web API?
How to implement CKEditor5 in Angular and upload image to ASP.NET Core Web API?
请参阅我的回答,了解 Angular 中 CkEditor5 的详细实现以及 ASP.NET Core
中的图片上传
你也可以在下面找到这篇文章,类似于VS代码格式。
可能的错误:
Uncaught (in promise): TypeError: Cannot read property 'data-ck-expando' of undefined in CKEditor
上述错误的解决方法:
remove all the code in polyfill.ts and add below code
import 'zone.js/dist/zone.js'; // Included with Angular CLI.
(window as any).__Zone_disable_toString = true;
安装以下软件包
npm install --save @ckeditor/ckeditor5-angular
安装官方编辑器版本之一或创建自定义版本。
假设您选择了@ckeditor/ckeditor5-build-classic:
npm install --save @ckeditor/ckeditor5-build-classic
现在,将 CKEditorModule 添加到您的应用程序模块导入中:
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
@NgModule( {
imports: [
CKEditorModule,
// ... ],
// ... } )
ckeditorEx.html:
<ckeditor [config]="editorConfig" [editor]="Editor" [(ngModel)]="description"
#desc="ngModel" spellcheck="false" required minlength="50" name="editor5" (ready)="onReady($event)">
ckeditorEx.ts
look for this url and point to your api url
xhr.open('POST', 'https://www.example.com/api/articles/CkEditorUploads', true);
import { Component, OnInit } from '@angular/core';
import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic';
@Component({
selector: 'app-ckeditorEx',
templateUrl: './ckeditorEx.component.html',
styleUrls: ['./ckeditorEx.component.css']
})
export class ckeditorExComponent implements OnInit {
public Editor = ClassicEditor;
editorConfig = {
placeholder: 'Type here..',
};
description: any;
constructor() {
}
ngOnInit() {
}
onReady($event) {
$event.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new MyUploadAdapter(loader);
};
}
//ckeditorExComponent class Ends here and MyUploadAdapter class begins here in the same ckeditorEx.ts
class MyUploadAdapter {
xhr: any;
loader: any;
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.
//Replace below url with your API url
xhr.open('POST', 'https://www.example.com/api/articles/CkEditorUploads', 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);
}
}
ASP.NET 网页 API
Controller Constructor:
// Import host in your controller cconstructor like below
private readonly IHostingEnvironment host;
public ExampleController(
..........................
IHostingEnvironment host)
{
this.host = host;
}
Controller Method:
[AllowAnonymous]
[HttpPost("CkEditorUploads2")]
// Don't change "IFormFile upload" text below.
public async Task<IActionResult> UploadImage(IFormFile upload)
{
var file = upload;
var allowedExtensions = new[] {
".Jpg", ".png",".PNG",".JPG","JPEG", ".jpg", ".jpeg",".Heif",".tiff"
};
if (file == null) return BadRequest("Null file");
if (file.Length > 10 * 1024 * 1024) return BadRequest("Max file size exceeded.");
if (file.Length == 0) return BadRequest("Empty file");
var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
var uploadsFolderPath = Path.Combine(host.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolderPath))
Directory.CreateDirectory(uploadsFolderPath);
var ext = Path.GetExtension(file.FileName);
// if (allowedExtensions.fin(ext)) return BadRequest("Invalid Image type.");
var filePath = Path.Combine(uploadsFolderPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// Converting to EST timezone
var timeUtc = DateTime.UtcNow;
TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime easternTime = TimeZoneInfo.ConvertTime(timeUtc, easternZone);
var editorID = Guid.NewGuid().ToString();
var editorImages = new editorImages
{
id = editorID,
url = "https://example.com/uploads/" + fileName,
Logintime = easternTime,
fileName = fileName,
};
// Save the image details to your database
ArticlesRepository.saveEditorImagesInfo(editorImages);
await unitofWork.Complete();
var res = await ArticlesRepository.getEdiorImagebyID(editorID);
if (res == null)
// Important to send return the result in this format
return NotFound(new
{
uploaded = false,
error = "Could not upload this image"
});
// Important to send return the result in this format
return Ok(new
{
uploaded = true,
//return imagelocation
url = res.url
});
}
请参阅我的回答,了解 Angular 中 CkEditor5 的详细实现以及 ASP.NET Core
中的图片上传你也可以在下面找到这篇文章,类似于VS代码格式。
可能的错误:
Uncaught (in promise): TypeError: Cannot read property 'data-ck-expando' of undefined in CKEditor
上述错误的解决方法:
remove all the code in polyfill.ts and add below code
import 'zone.js/dist/zone.js'; // Included with Angular CLI.
(window as any).__Zone_disable_toString = true;
安装以下软件包
npm install --save @ckeditor/ckeditor5-angular
安装官方编辑器版本之一或创建自定义版本。
假设您选择了@ckeditor/ckeditor5-build-classic:
npm install --save @ckeditor/ckeditor5-build-classic
现在,将 CKEditorModule 添加到您的应用程序模块导入中:
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
@NgModule( {
imports: [
CKEditorModule,
// ... ],
// ... } )
ckeditorEx.html:
<ckeditor [config]="editorConfig" [editor]="Editor" [(ngModel)]="description"
#desc="ngModel" spellcheck="false" required minlength="50" name="editor5" (ready)="onReady($event)">
ckeditorEx.ts
look for this url and point to your api url xhr.open('POST', 'https://www.example.com/api/articles/CkEditorUploads', true);
import { Component, OnInit } from '@angular/core';
import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic';
@Component({
selector: 'app-ckeditorEx',
templateUrl: './ckeditorEx.component.html',
styleUrls: ['./ckeditorEx.component.css']
})
export class ckeditorExComponent implements OnInit {
public Editor = ClassicEditor;
editorConfig = {
placeholder: 'Type here..',
};
description: any;
constructor() {
}
ngOnInit() {
}
onReady($event) {
$event.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new MyUploadAdapter(loader);
};
}
//ckeditorExComponent class Ends here and MyUploadAdapter class begins here in the same ckeditorEx.ts
class MyUploadAdapter {
xhr: any;
loader: any;
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.
//Replace below url with your API url
xhr.open('POST', 'https://www.example.com/api/articles/CkEditorUploads', 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);
}
}
ASP.NET 网页 API
Controller Constructor:
// Import host in your controller cconstructor like below
private readonly IHostingEnvironment host;
public ExampleController(
..........................
IHostingEnvironment host)
{
this.host = host;
}
Controller Method:
[AllowAnonymous]
[HttpPost("CkEditorUploads2")]
// Don't change "IFormFile upload" text below.
public async Task<IActionResult> UploadImage(IFormFile upload)
{
var file = upload;
var allowedExtensions = new[] {
".Jpg", ".png",".PNG",".JPG","JPEG", ".jpg", ".jpeg",".Heif",".tiff"
};
if (file == null) return BadRequest("Null file");
if (file.Length > 10 * 1024 * 1024) return BadRequest("Max file size exceeded.");
if (file.Length == 0) return BadRequest("Empty file");
var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
var uploadsFolderPath = Path.Combine(host.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolderPath))
Directory.CreateDirectory(uploadsFolderPath);
var ext = Path.GetExtension(file.FileName);
// if (allowedExtensions.fin(ext)) return BadRequest("Invalid Image type.");
var filePath = Path.Combine(uploadsFolderPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// Converting to EST timezone
var timeUtc = DateTime.UtcNow;
TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime easternTime = TimeZoneInfo.ConvertTime(timeUtc, easternZone);
var editorID = Guid.NewGuid().ToString();
var editorImages = new editorImages
{
id = editorID,
url = "https://example.com/uploads/" + fileName,
Logintime = easternTime,
fileName = fileName,
};
// Save the image details to your database
ArticlesRepository.saveEditorImagesInfo(editorImages);
await unitofWork.Complete();
var res = await ArticlesRepository.getEdiorImagebyID(editorID);
if (res == null)
// Important to send return the result in this format
return NotFound(new
{
uploaded = false,
error = "Could not upload this image"
});
// Important to send return the result in this format
return Ok(new
{
uploaded = true,
//return imagelocation
url = res.url
});
}