如何在CKEditor 5中启用图像上传支持?

27

我将在我的项目中使用ckeditor v5。我尝试使用图像插件,但未找到足够的信息。

如果您在这里查看演示,您可以轻松地通过拖放上传图像。但是当我尝试使用下载的zip文件时拖放图片时没有反应,也没有错误提示。

有没有办法在可下载版本中使用此图像支持?

6个回答

46
是的,所有可用版本都包含图像上传功能。但要使其正常工作,您需要配置现有的上传适配器之一或编写自己的适配器。简而言之,上传适配器是一个简单的类,其作用是将文件发送到服务器(以任何希望的方式),并在完成后解决返回的承诺。
您可以在官方图像上传指南中阅读更多信息,或查看下面提供的可用选项的简短摘要。

官方上传适配器

内置了两个适配器:

  • For CKFinder which require you to install the CKFinder connectors on your server.

    Once you have the connector installed on your server, you can configure CKEditor to upload files to that connector by setting the config.ckfinder.uploadUrl option:

    ClassicEditor
        .create( editorElement, {
            ckfinder: {
                uploadUrl: '/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json'
            }
        } )
        .then( ... )
        .catch( ... );
    

    You can also enable full integration with CKFinder's client-side file manager. Check out the CKFinder integration demos and read more in the CKFinder integration guide.

  • For the Easy Image service which is a part of CKEditor Cloud Services.

    You need to set up a Cloud Services account and once you created a token endpoint configure the editor to use it:

    ClassicEditor
        .create( editorElement, {
            cloudServices: {
                tokenUrl: 'https://example.com/cs-token-endpoint',
                uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/'
            }
        } )
        .then( ... )
        .catch( ... );
    

免责声明:这些是专有服务。

自定义上传适配器

您还可以编写自己的上传适配器,以您想要的方式将文件发送到您的服务器(或任何您想要发送的地方)。

请参见自定义图像上传适配器指南,了解如何实现它。

一个示例(即没有内置安全性)的上传适配器可能如下所示:

class MyUploadAdapter {
    constructor( loader ) {
        // CKEditor 5's FileLoader instance.
        this.loader = loader;

        // URL where to send files.
        this.url = 'https://example.com/image/upload/path';
    }

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

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

    // Example implementation using XMLHttpRequest.
    _initRequest() {
        const xhr = this.xhr = new XMLHttpRequest();

        xhr.open( 'POST', this.url, true );
        xhr.responseType = 'json';
    }

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

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

            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.
            resolve( {
                default: response.url
            } );
        } );

        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() {
        const data = new FormData();

        data.append( 'upload', this.loader.file );

        this.xhr.send( data );
    }
}

然后可以像这样启用:

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

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        extraPlugins: [ MyCustomUploadAdapterPlugin ],

        // ...
    } )
    .catch( error => {
        console.log( error );
    } );

注意:上述仅为上传适配器示例。因此,它没有内置的安全机制(如CSRF保护)。


1
两个都是付费服务吗?有没有免费的上传方式?例如在ckeditor4中的filebrowserUploadUrl? - keatwei
3
正如我的回答所说,您也可以编写自己的上传适配器。甚至有一个第三方插件专门做这个(https://www.npmjs.com/package/ckeditor5-simple-upload)。 - Reinmar
1
感谢@Reinmar提供的链接,我终于可以将ES6语法转换为通用的基于浏览器的JavaScript语法在这里,以防有人需要它来制作简单的_app_。 - Taufik Nurrohman
3
似乎 loader.file.name 显示为 undefined。我无法获得文件名称和扩展名。你能帮忙吗? - user3550587
1
可能是一个愚蠢的问题,但是你如何在服务器端处理这个问题(例如使用PHP)?我找不到任何代码,似乎到达服务器端的是一些没有方法的“Promise”对象。 - IceFire
显示剩余8条评论

16

我正在寻找如何使用这个控件的信息,但官方文档相当简略。经过多次尝试和错误,我终于让它正常工作了,因此我想分享一下。

最终,我在 Angular 8 中使用了 CKEditor 5 简单上传适配器,并且它可以很好地工作。但是,您需要创建一个自定义版本的 ckeditor,并安装上传适配器才行。这非常容易做到。 我假设您已经有了 ckeditor Angular 文件。

首先,创建一个新的Angular项目目录,将其命名为“cKEditor-Custom-Build”或其他名称。不要运行ng new(Angular CLI),而是使用npm获取您想要展示的编辑器的基本构建。 在此示例中,我正在使用经典编辑器。

https://github.com/ckeditor/ckeditor5-build-classic

前往 Github 并将项目克隆或下载到您的新构建目录中。

如果您使用 VS Code,请打开该目录并打开终端框,然后获取依赖项:

npm i

您现在已经拥有了基本构建,并且需要安装上传适配器。CKEditor有一个适配器。安装此软件包以获取简单的上传适配器:

npm install --save @ckeditor/ckeditor5-upload

完成后,打开项目中的ckeditor.js文件。它在“src”目录中。如果您已经在使用ckEditor,则其内容应该很熟悉。

将新的js文件导入到ckeditor.js文件中。此文件中会有很多导入项,请将其全部放置在底部。

import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter';

然后将其导入到您的插件数组中。由于我正在使用经典编辑器,所以我的部分称为"ClassicEditor.builtinPlugins",请在TableToolbar旁边添加它。这样就完成了所有配置。无需在此结束处添加其他工具栏或配置。

构建您的ckeditor-custom-build。

npm run build

Angular的神奇之处会自行完成,并在您的项目中创建一个名为“build”的目录。这就是自定义构建的全部过程。

现在打开您的Angular项目,并创建一个新的构建目录。实际上,我将它放在“assets”子目录中,但您可以将其放在任何您可以引用的地方。

在“src/assets”中创建一个名为“ngClassicEditor”的目录,名称不重要,然后将构建文件复制到其中(刚刚创建的文件)。接下来,在要使用编辑器的组件中,添加一个导入语句,其中包含指向新构建的路径。

import * as Editor from '@app/../src/assets/ngClassicEditor/build/ckeditor.js';

即将完成...

最后一步是使用 API 端点配置上传适配器以上传图像。在组件类中创建一个配置。

  public editorConfig = {
simpleUpload: {
  // The URL that the images are uploaded to.
  uploadUrl: environment.postSaveRteImage,

  // Headers sent along with the XMLHttpRequest to the upload server.
  headers: {
    'X-CSRF-TOKEN': 'CSFR-Token',
    Authorization: 'Bearer <JSON Web Token>'
  }
}

};

我实际上在这里使用环境转换,因为从dev到production的URI会发生变化,但如果您愿意,您可以在其中硬编码一个直接的URL。

最后一步是在模板中配置编辑器以使用新的配置值。打开您的component.html文件并修改您的ckeditor编辑器标签。

     <ckeditor [editor]="Editor" id="editor"  [config]="editorConfig">
      </ckeditor>

就是这样了,你完成了。测试、测试、测试。

我的 API 是 .Net API,如果您需要一些示例代码,我很乐意分享。我真的希望这能帮助到你。


1
我甚至尝试了自定义硬编码响应,例如{"url": "image-url"},但仍然出现错误。 - Rahimjon Rustamov
1
我明白你的意思,但是你的API会返回一个HTTP响应代码来表示POST请求的状态。老实说,我没有使用Spring Boot的经验,所以你可能需要发布一个问题来调试传入的API POST操作。 - Darren Street
如果你能帮助我解决最后这个问题,我会很快完成。不管怎样,谢谢你,你救了我的一天。 - Rahimjon Rustamov
回答取决于你的编码方式。HTTP规范指出,你应该得到一个201创建响应代码和新创建实体的副本,但这取决于API的最终配置方式。 - Darren Street
让我们在聊天中继续讨论 - Darren Street
显示剩余5条评论

5

对我来说它运行良好。感谢您的所有答案。这是我的实现。


myUploadAdapter.ts

import { environment } from "./../../../environments/environment";

export class MyUploadAdapter {
  public loader: any;
  public url: string;
  public xhr: XMLHttpRequest;
  public token: string;

  constructor(loader) {
    this.loader = loader;

    // change "environment.BASE_URL" key and API path
    this.url = `${environment.BASE_URL}/api/v1/upload/attachments`;

    // change "token" value with your token
    this.token = localStorage.getItem("token");
  }

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

  abort() {
    if (this.xhr) {
      this.xhr.abort();
    }
  }

  _initRequest() {
    const xhr = (this.xhr = new XMLHttpRequest());
    xhr.open("POST", this.url, true);

    // change "Authorization" header with your header
    xhr.setRequestHeader("Authorization", this.token);

    xhr.responseType = "json";
  }

  _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;

      if (!response || response.error) {
        return reject(
          response && response.error ? response.error.message : genericErrorText
        );
      }

      // change "response.data.fullPaths[0]" with image URL
      resolve({
        default: response.data.fullPaths[0],
      });
    });

    if (xhr.upload) {
      xhr.upload.addEventListener("progress", (evt) => {
        if (evt.lengthComputable) {
          loader.uploadTotal = evt.total;
          loader.uploaded = evt.loaded;
        }
      });
    }
  }

  _sendRequest(file) {
    const data = new FormData();

    // change "attachments" key
    data.append("attachments", file);

    this.xhr.send(data);
  }
}


component.html

<ckeditor
  (ready)="onReady($event)"
  [editor]="editor"
  [(ngModel)]="html"
></ckeditor>

component.ts

import { MyUploadAdapter } from "./myUploadAdapter";
import { Component, OnInit } from "@angular/core";
import * as DecoupledEditor from "@ckeditor/ckeditor5-build-decoupled-document";

@Component({
  selector: "xxx",
  templateUrl: "xxx.html",
})
export class XXX implements OnInit {
  public editor: DecoupledEditor;
  public html: string;

  constructor() {
    this.editor = DecoupledEditor;
    this.html = "";
  }

  public onReady(editor) {
    editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
      return new MyUploadAdapter(loader);
    };
    editor.ui
      .getEditableElement()
      .parentElement.insertBefore(
        editor.ui.view.toolbar.element,
        editor.ui.getEditableElement()
      );
  }

  public ngOnInit() {}
}

3
在React中,创建一个名为MyCustomUploadAdapterPlugin的新文件。

import Fetch from './Fetch'; //my common fetch function 

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 ) => {

                const toBase64 = file => new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => resolve(reader.result);
                    reader.onerror = error => reject(error);
                });
                
                return toBase64(file).then(cFile=>{
                    return  Fetch("admin/uploadimage", {
                        imageBinary: cFile
                    }).then((d) => {
                        if (d.status) {
                            this.loader.uploaded = true;
                            resolve( {
                                default: d.response.url
                            } );
                        } else {
                            reject(`Couldn't upload file: ${ file.name }.`)
                        }
                    });
                })
                
            } ) );
    }

   
}

// ...

export default 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 );
    };
}

并且在

import MyCustomUploadAdapterPlugin from '../common/ckImageUploader';
import CKEditor from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';



  <CKEditor
         editor={ClassicEditor}
         data={quesText}
         placeholder="Question Text"
         config={{extraPlugins:[MyCustomUploadAdapterPlugin]}} //use
  />


1

对于遇到XHR问题的人,你也可以使用fetch API,这似乎也能正常工作。

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

    request(file) {
      return fetch(this.url, { // Your POST endpoint
        method: 'POST',
        headers: {
          'x-csrf-token': _token
        },
        body: file // This is your file object
      });
    }

upload() {
        const formData = new FormData();

        this.loader.file.then((filenew) => {
          console.log(filenew);
          formData.append('file', filenew, filenew.name);
  
          return new Promise((resolve, reject) => {
            this.request(formData).then(
             response => response.json() // if the response is a JSON object
           ).then(
             success => console.log(success) // Handle the success response object
           ).catch(
             error => console.log(error) // Handle the error response object
           );
        })
      });
    }

0

我使用了这个配置:

public editorConfig = {
 simpleUpload: {
 uploadUrl: environment.postSaveRteImage,
 headers: {
'X-CSRF-TOKEN': 'CSFR-Token',
 Authorization: 'Bearer <JSON Web Token>'
 }
 }

图片上传成功,返回 {"url": "image-url"}。 但在前端 ckeditor 的警告中显示: 无法上传文件:未定义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接