Contribute to this guide

guide自定义图片上传适配器

在本指南中,您将学习 CKEditor 5 WYSIWYG 编辑器中文件上传架构的基本概念,这将有助于您实现自定义上传适配器。

虽然本指南主要关注图片上传(最常见的上传类型),但所介绍的概念和 API 允许开发针对不同文件类型(如 PDF 文件、电影等)的所有类型的文件上传适配器。

如果您不想阅读本指南,但想要一个简单的上传适配器,请查看我们为您实现的 简单上传适配器 插件。

查看全面的 图片上传概述 以了解将图片上传到 CKEditor 5 的其他方法。

# 术语表

在开始之前,请确保本指南中使用的所有术语都清楚。

术语 描述
上传适配器

一段代码(一个类),它从用户请求图片上传(例如,将文件拖放到内容中)到服务器返回对请求的上传的响应的那一刻起,处理图片上传。功能和服务器之间的桥梁。

其他插件(如 图片上传)使用上传适配器来连接到服务器并获取响应。对于每个用户操作(例如,当文件被拖放到内容中时),都会创建一个新的上传适配器实例。

CKEditor 5 带有一些 官方上传适配器,但您也可以 实现自己的适配器

请参阅 "图片上传是如何工作的?" 部分以了解更多信息。

UploadAdapter 接口

一个定义创建上传适配器所需的最小 API 的接口。换句话说,它告诉您上传适配器类必须包含哪些方法才能工作。

请参阅 "适配器的解剖" 部分以了解更多信息。

文件库 插件

CKEditor 5 中用于管理文件上传的中心点。它使用上传适配器和使用它们的特性进行粘合

图片上传 插件

一个顶级插件,它通过将文件上传到服务器并在上传完成后更新编辑内容来响应用户的操作(例如,当文件被拖放到内容中时)。这个特定的插件处理与上传图片相关的用户操作。

它使用 FileRepository API 生成上传适配器实例,触发图片上传(UploadAdapter.upload()),并使用适配器上传承诺返回的数据来更新编辑器内容中的图片。

请参阅 "图片上传是如何工作的?" 部分以了解更多信息。

# 图片上传是如何工作的?

在创建自定义上传适配器之前,您应该了解 CKEditor 5 中的图片上传过程。整个过程包括以下步骤

  1. 首先,需要将图片(或图片)放入富文本编辑器的内容中。有很多方法可以做到这一点,例如

    • 从剪贴板粘贴图片,
    • 从文件系统中拖放文件,
    • 通过文件系统对话框选择图片。

    图片会被 图片上传 插件截获。

  2. 对于每张图片,图片上传插件都会 创建一个文件加载器实例

    • 文件加载器的作用是读取磁盘上的文件,并使用上传适配器将其上传到服务器。
    • 因此,上传适配器的作用是安全地将文件发送到服务器,并将服务器的响应(例如,已保存文件的 URL)传递回文件加载器(或处理错误,如果有)。
  3. 在图片上传期间,图片上传插件

    • 为这些图片创建占位符。
    • 将它们插入编辑器。
    • 为每个图片显示进度条。
    • 当在上传完成之前从编辑器内容中删除图片时,它会中止上传过程。
  4. 文件上传完成后,上传适配器通过解决其 Promise 来通知编辑器这一事实。它将 URL(或在响应式图片的情况下为 URL)传递给图片上传插件,该插件将替换编辑器内容中图片占位符的 srcsrcset 属性。

这只是图片上传过程的概述。实际上,整个过程更加复杂。例如,可以在 WYSIWYG 编辑器中复制和粘贴图片(在上传进行期间),并且还必须处理所有可能的上传错误。好消息是,这些任务由 图片上传 插件透明地处理,因此您无需担心它们。

总而言之,为了使图片上传在富文本编辑器中正常工作,必须满足两个条件

# 适配器的解剖

自定义上传适配器允许您完全控制将文件发送到服务器以及将服务器的响应传递回富文本编辑器的过程。

任何上传适配器,无论是图片上传适配器还是通用文件上传适配器,都必须实现 UploadAdapter 接口 才能工作,也就是说,它必须自带 upload()abort() 方法。

  • upload() 方法必须返回一个承诺
    • 已解决,成功上传,其中包含有关上传文件的对象的详细信息(请参阅有关 响应式图片 的部分以了解更多信息),
    • 被拒绝,因为发生了错误,在这种情况下,不会将任何内容插入内容中。
  • abort() 方法必须允许编辑器停止上传过程。例如,当用户在上传完成之前从内容中删除图片或编辑器实例被 销毁 时,这是必要的。

在最简单的形式中,实现 UploadAdapter 接口的自定义适配器将如下所示。请注意,server.upload()server.onUploadProgress()server.abortUpload() 应该替换为特定的实现(专用于您的应用程序),并且仅演示上传工作所需的最小通信

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

    // Starts the upload process.
    upload() {
        // Update the loader's progress.
        server.onUploadProgress( data => {
            loader.uploadTotal = data.total;
            loader.uploaded = data.uploaded;
        } );

        // Return a promise that will be resolved when the file is uploaded.
        return loader.file
            .then( file => server.upload( file ) );
    }

    // Aborts the upload process.
    abort() {
        // Reject the promise returned from the upload() method.
        server.abortUpload();
    }
}

定义 FileRepository.createUploadAdapter() 工厂方法,该方法使用 MyUploadAdapter 类在编辑器中启用上传适配器

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

# 实现自定义上传适配器

在本节中,您将实现并启用自定义上传适配器。该适配器将使用原生的 XMLHttpRequest 将加载器返回的文件发送到服务器上的预配置 URL,并处理请求触发的 errorabortloadprogress 事件。

如果您不想阅读本指南,但想要一个简单的基于 XMLHttpRequest 的上传适配器,请查看我们为您实现的 简单上传适配器 插件。它具有几乎相同的功能。只需安装它,配置它,您就可以开始使用它了。

如果 简单上传适配器 不够用,您想要在该指南的基础上开发自定义上传适配器,请直接转到 完整源代码 并开始尝试。

这只是一个示例实现,XMLHttpRequest 可能并不一定适合您的应用程序。

使用提供的代码片段作为您自己的自定义上传适配器的灵感 - 由您决定使用哪些技术和 API。例如,您可能想查看原生的 fetch() API,它可以与 Promises 无缝衔接。

首先,定义 MyUploadAdapter 类及其构造函数。

class MyUploadAdapter {
    constructor( loader ) {
        // The file loader instance to use during the upload. It sounds scary but do not
        // worry — the loader will be passed into the adapter later on in this guide.
        this.loader = loader;
    }

    // More methods.
    // ...
}

实现最小的 UploadAdapter 适配器接口,如 “适配器结构” 部分所述。实现细节将在本指南的后续章节中解释。

class MyUploadAdapter {
    // The constructor method.
    // ...

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

    // More methods.
    // ...
}

# 在适配器中使用 XMLHttpRequest

让我们看看自定义上传适配器中 _initRequest() 方法是什么样的。它应该在使用 XMLHttpRequest 对象上传图像之前准备好该对象。

为了保持代码简洁,在本示例实现中,没有使用任何特定的安全机制来防止您的应用程序和服务被滥用。

我们强烈建议在您的应用程序中使用身份验证和 CSRF 防护 机制(例如 CSRF 令牌)。例如,它们可以作为 XMLHttpRequest 标头实现。

class MyUploadAdapter {
    // More methods.
    // ...

    // 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', 'http://example.com/image/upload/path', true );
        xhr.responseType = 'json';
    }
}

现在,关注 _initListeners() 方法,它将 errorabortloadprogress 事件监听器附加到上一步创建的 XMLHttpRequest 对象。

XMLHttpRequest 请求触发的 load 事件发生时,上传承诺将被解析,成功的图像上传将完成。承诺必须解析为包含有关图像信息的以对象形式。有关详细信息,请参阅 upload() 方法的文档。

class MyUploadAdapter {
    // More methods.
    // ...

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

最后,_sendRequest() 方法发送 XMLHttpRequest。在本例中,FormData 接口用于传递由 文件加载器 提供的文件。

在本示例实现中,传递给 XMLHttpRequest.send() 的数据格式和实际数据都是任意的。您的实现可能有所不同,它将取决于您的应用程序的后端及其提供的接口。

class MyUploadAdapter {
    // More methods.
    // ...

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

# 自适应图像和 srcset 属性

如果上传成功,MyUploadAdapter.upload() 方法返回的 Promise 可以解析为不仅仅是上传图像的 default 路径。请参阅 MyUploadAdapter._initListeners() 的实现。这通常看起来像这样

{
    default: 'http://example.com/images/image–default-size.png'
}

响应中还可以提供其他图像大小,允许在编辑器中使用 自适应图像。包含多个图像大小的响应可能看起来像这样

{
    default: 'http://example.com/images/image–default-size.png',
    '160': 'http://example.com/images/image–size-160.image.png',
    '500': 'http://example.com/images/image–size-500.image.png',
    '1000': 'http://example.com/images/image–size-1000.image.png',
    '1052': 'http://example.com/images/image–default-size.png'
}

返回多个图像时,返回的最宽图像应该是默认图像。正确设置富文本编辑器内容中图像的 width 属性至关重要。

图像上传 插件能够处理上传适配器返回的多个图像大小。它将自动将其他图像大小的 URL 添加到内容中图像的 srcset 属性中。

易于使用图像 功能提供 开箱即用 的自适应图像支持。

了解这一点,您可以实现 上一节 中解析上传承诺的 XMLHttpRequest#load 监听器,以便它将服务器响应的整个 urls 属性传递给图像上传插件

// The rest of the MyUploadAdapter class definition.
// ...

xhr.addEventListener( 'load', () => {
    const response = xhr.response;

    // Response handling.
    // ...

    // response.urls = {
    // 	default: 'http://example.com/images/image–default-size.png',
    // 	'160': '...',
    // 	'500': '...',
    // 	More response urls.
    //  ...
    // 	'1052': 'http://example.com/images/image–default-size.png'
    // }
    resolve( response.urls );
} );

// The rest of the MyUploadAdapter class definition.
// ...

# 将附加数据传递到响应

您可能需要将某些数据从服务器传递到某些功能,以提供附加数据。为此,您需要将 urls 属性中的所有 URL 包裹起来,并在对象的顶层传递附加数据。

对于图像上传,您可以在 uploadComplete 事件中检索该数据。这允许在图像上传后立即根据数据设置模型图像上的新属性并覆盖现有属性。

{
    urls: {
        default: 'http://example.com/images/image–default-size.png',
        // Optional different sizes of images.
    },
    customProperty: 'foo'
}

# 激活自定义上传适配器

实现适配器后,您必须弄清楚如何在 WYSIWYG 编辑器中启用它。好消息是,这非常容易,您不需要 重建编辑器 就可以做到!

您将扩展本指南 “适配器结构” 部分中介绍的基本实现,以便您的自定义适配器成为一个编辑器插件。为此,创建一个简单的独立插件 (MyCustomUploadAdapterPlugin),它将 创建一个文件加载器的实例 并将其与您的自定义 MyUploadAdapter 粘合在一起。

import { ClassicEditor, Essentials, Paragraph, Image, ImageUpload } from 'ckeditor5';

class MyUploadAdapter {
    // MyUploadAdapter class definition.
    // ...
}

function MyCustomUploadAdapterPlugin( editor ) {
    editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
        // Configure the URL to the upload script in your backend here!
        return new MyUploadAdapter( loader );
    };
}

使用 config.extraPlugins 选项在编辑器中启用 MyCustomUploadAdapterPlugin

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ MyCustomUploadAdapterPlugin, Essentials, Paragraph, Image, ImageUpload, /* ... */ ],
        // More configuration options.
        // ...
    } )
    .catch( error => {
        console.log( error );
    } );

运行编辑器,看看您的实现是否有效。将图像拖放到 WYSIWYG 编辑器内容中,它应该通过 MyUploadAdapter 上传到服务器。

# 完整实现

以下是基于 XMLHttpRequest 的上传适配器的完整实现。您可以使用此代码作为基础来构建应用程序的自定义上传适配器。

import { ClassicEditor, Essentials, Paragraph, Image, ImageUpload } from 'ckeditor5';

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', 'http://example.com/image/upload/path', 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 );
    }
}

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

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ MyCustomUploadAdapterPlugin, Essentials, Paragraph, Image, ImageUpload, /* ... */ ],

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

# 下一步

查看全面的 图像上传概述 指南,了解有关在 CKEditor 5 中上传图像的不同方法的更多信息。请参阅 图像功能 指南,了解有关在 CKEditor 5 中处理图像的更多信息。