【问题标题】:How to enable image upload support in CKEditor 5?如何在 CKEditor 5 中启用图像上传支持?
【发布时间】:2018-03-27 16:08:33
【问题描述】:

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

如果您看到 Demoe here,您可以通过拖放轻松上传图片。但是当我尝试使用下载气球 zip 时,当我尝试拖放图像时没有任何反应。也没有错误。

有没有办法在可下载的变体中使用此图像支持?

【问题讨论】:

    标签: javascript ckeditor ckeditor5


    【解决方案1】:

    是的,图片上传包含在所有可用的版本中。但是,为了使其工作,您需要配置现有的上传适配器之一或编写自己的。简而言之,上传适配器是一个简单的类,它的作用是将文件发送到服务器(以任何它想要的方式)并在完成后解析返回的承诺。

    您可以在官方Image upload 指南中阅读更多信息,或查看下面可用选项的简短摘要。

    官方上传适配器

    有两个内置适配器:

    • 对于CKFinder,需要您在服务器上安装 CKFinder 连接器。

      在服务器上安装连接器后,您可以通过设置config.ckfinder.uploadUrl 选项来配置 CKEditor 以将文件上传到该连接器:

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

      您还可以启用与 CKFinder 的客户端文件管理器的完全集成。查看CKFinder integration demos 并阅读CKFinder integration 指南中的更多信息。

    • 对于Easy Image 服务,它是CKEditor Cloud Services 的一部分。

      您需要set up a Cloud Services account 并且一旦您created a token endpoint 配置编辑器以使用它:

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

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

    自定义上传适配器

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

    请参阅Custom image upload adapter 指南以了解如何实施它。

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

    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 保护)。

    【讨论】:

    • 所以两者都是付费服务?有没有免费的上传方式?像ckeditor4中的filebrowserUploadUrl?
    • 正如我的回答所说 - 您也可以编写自己的上传适配器。甚至还有一个 3rd 方插件可以做到这一点 (npmjs.com/package/ckeditor5-simple-upload)。
    • thx .. 测试了简单上传,但它目前因 ckeditor5-beta1 而中断。
    • 感谢@Reinmar 的链接,我终于可以将 ES6 语法转换为基于浏览器的通用 JavaScript 语法here,以防万一有人需要它来制作简单的 app .
    • 似乎 loader.file.name 显示未定义。我无法获取文件名和扩展名。你能帮忙吗?
    【解决方案2】:

    我正在搜索有关如何使用此控件的信息,发现官方文档非常少。然而,经过多次试验和错误,我确实让它工作了,所以我想我会分享。

    最后我使用了 CKEditor 5 简单的上传适配器和 Angular 8,它工作得很好。但是,您确实需要创建一个安装了上传适配器的自定义版本的 ckeditor。这很容易做到。我假设您已经拥有 ckeditor Angular 文件。

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

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

    转到 github 并将项目克隆或下载到新的闪亮构建目录中。

    如果您使用的是 VS 代码,请打开目录并打开终端框并获取依赖项:

    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 的魔力将发挥作用,并在您的项目中创建一个“构建”目录。它用于自定义构建。

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

    在“src/assets”中创建一个名为“ngClassicEditor”的目录,不管你怎么称呼它,然后将构建文件复制到其中(你刚刚创建的)。接下来,在您要使用编辑器的组件中,添加带有新构建路径的 import 语句。

    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>'
      }
    }
    

    };

    我实际上在这里使用environment transform,因为 URI 从开发更改为生产,但如果需要,您可以在其中硬编码一个直接 URL。

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

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

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

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

    【讨论】:

    • 我尝试了您的示例,并按照它正在工作的所有步骤向服务器 API 发送请求,但是,没有我上传的图像文件,请求为空。这是什么原因?知道原因就好了?我的服务器在 Spring Boot
    • 我什至尝试了自定义硬编码响应,例如 {"url": "image-url"} 但它仍然给出错误
    • 我想将图像保存到我的资源文件夹并希望返回每个图像 url,但图像甚至无法到达服务器。我不确定 SimpleUploadAdapter 是否正在向请求中添加图像。
    • 我的服务器响应作为文档返回
    • 是的,我明白了,但是您的 API 将返回一个 HTTP 响应代码来表示帖子的进展情况。老实说,我没有任何 Spring Boot 经验,所以你可能想发布一个关于如何调试传入 API POST 操作的问题。
    【解决方案3】:

    它对我很好。感谢所有的答案。这是我的实现。


    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>
    

    组件.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() {}
    }
    

    【讨论】:

      【解决方案4】:

      在反应中

      使用 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
        />

      【讨论】:

        【解决方案5】:

        我使用了这个配置:

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

        图片上传成功,响应为 {"url": "image-url"}。 但在前端 ckeditor 的警报中说

        无法上传文件:未定义。

        【讨论】:

          【解决方案6】:

          对于遇到 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
                     );
                  })
                });
              }
          

          【讨论】:

            猜你喜欢
            • 2013-04-28
            • 1970-01-01
            • 1970-01-01
            • 2015-02-20
            • 2018-08-29
            • 1970-01-01
            • 2019-01-19
            • 2021-01-06
            相关资源
            最近更新 更多