如何将TinyMCE图片上传至KeystoneJS的Apollo GraphQL服务器
我刚好做过类似的集成,核心就是把TinyMCE的images_upload_handler和Apollo Client的mutation调用结合起来,不用再写传统的XHR请求。下面是具体的实现步骤和代码示例:
核心思路
TinyMCE的images_upload_handler会提供图片的Blob信息,我们可以直接把这个Blob包装成File对象,然后调用Keystone自动生成的createImage mutation完成上传,最后把返回的图片URL交给TinyMCE插入到编辑器中。
步骤1:定义GraphQL Mutation
首先,我们需要定义Keystone生成的图片上传mutation,和你提到的结构一致:
const CREATE_IMAGE_MUTATION = gql` mutation CreateImage($file: Upload!) { createImage(data: { file: $file }) { id url # 这里根据你的Keystone Image字段配置调整,可能是publicUrl或其他字段 } } `;
步骤2:在TinyMCE上传处理器中调用Apollo Mutation
在你的Keystone自定义字段组件里,使用Apollo Client的useMutation hook获取mutation函数,然后在TinyMCE的上传处理器里调用它:
import { useMutation } from '@apollo/client'; import { gql } from '@keystone-6/core/admin-ui/apollo'; import tinymce from 'tinymce'; // 引入TinyMCE的样式和插件(根据你的项目配置调整) import 'tinymce/skins/ui/oxide/skin.css'; import 'tinymce/plugins/image/plugin'; const TinyMCECustomField = ({ field, value, onChange }) => { // 获取Apollo mutation函数 const [createImage] = useMutation(CREATE_IMAGE_MUTATION); const handleImageUpload = async (blobInfo, success, failure) => { try { // 从TinyMCE获取Blob和文件名 const blob = blobInfo.blob(); // 将Blob包装成File对象(确保文件名被正确传递) const file = new File([blob], blobInfo.filename(), { type: blob.type }); // 调用Keystone的GraphQL mutation上传图片 const { data } = await createImage({ variables: { file } }); // 将Keystone返回的图片URL传给TinyMCE,完成插入 success(data.createImage.url); } catch (error) { // 上传失败时调用TinyMCE的failure回调提示用户 failure(`图片上传失败: ${error.message}`); } }; return ( <textarea ref={el => { if (el && !el._tinymce) { tinymce.init({ target: el, plugins: 'image', toolbar: 'undo redo | bold italic | image', // 配置自定义上传处理器 images_upload_handler: handleImageUpload, // 禁用TinyMCE默认的Base64图片处理 images_upload_base64: false, setup: editor => { // 同步编辑器内容到Keystone字段 editor.on('input', () => onChange(editor.getContent())); // 初始化编辑器内容 editor.setContent(value || ''); } }); // 标记已初始化,避免重复创建 el._tinymce = true; } }} /> ); }; export default TinyMCECustomField;
关键细节说明
Blob转File的原因:
TinyMCE提供的blobInfo.blob()是一个Blob对象,而Apollo Client的Upload类型更期望接收File对象(因为File包含文件名、文件类型等元数据)。用new File()包装一下可以确保这些信息被正确传递给Keystone的mutation。Apollo Client配置:
Keystone 6的AdminUI默认已经集成了apollo-upload-client,所以不需要额外配置文件上传支持。如果是你自己搭建的Apollo Client,记得用createUploadLink替代普通的createHttpLink。权限控制:
这种方式会自动继承Keystone的权限规则,只有拥有createImage权限的用户才能上传图片,比单独写上传端点更安全。错误处理:
通过try-catch捕获mutation的错误,然后调用TinyMCE的failure回调,让编辑器显示错误提示,提升用户体验。
关于你的疑问
你担心的“TinyMCE提供Blob,Apollo需要文件名”的问题,确实只需要用blobInfo.filename()来获取文件名,然后把Blob包装成File对象即可——这也是最稳妥的方式,确保文件名和文件类型都能正确传递。
这种集成方式的优势在于完全复用Keystone的GraphQL生态,不需要额外维护上传接口,而且能和Keystone的权限、存储配置无缝对接。
内容的提问来源于stack exchange,提问作者Cerulean

