转换过程中保留图像
Available in Start planBetav0.8.0
您导入的一些文档可能包含您希望保留在转换文档中的图像。
注意
Tiptap 不提供图像上传服务。您需要实现自己的服务器来处理图像上传。
导入图像
如果您导入的 DOCX 文件包含图像,只有当您提供了图像上传配置时,转换服务才会将这些图像包含在生成的 Tiptap JSON 中。
使用 imageUploadConfig 选项来指定您服务器上的一个端点,转换服务将在导入过程中将图像上传到该端点。
import { Editor } from '@tiptap/core'
import { ImportDocx } from '@tiptap-pro/extension-import-docx'
const editor = new Editor({
// ... 其他编辑器选项,
extensions: [
ImportDocx.configure({
appId: '<your-app-id>',
token: '<your-jwt>',
imageUploadConfig: {
url: 'https://your-server.com/upload-image',
},
})
]
})在此配置中,imageUploadConfig.url 设置为您服务器上的一个端点,该端点将负责接收图像文件。如果未提供此项,导入器将从文档中剥离图像。
当触发导入时,转换服务将把每个嵌入的图像上传到您提供的 URL。
经过身份验证的图像上传
如果您的上传端点需要身份验证或自定义请求头,您可以直接进行配置:
ImportDocx.configure({
appId: '<your-app-id>',
token: '<your-jwt>',
imageUploadConfig: {
url: 'https://your-server.com/upload-image',
headers: {
Authorization: 'Bearer your-upload-token',
},
method: 'PUT',
queryParams: {
bucket: 'my-bucket',
},
},
})请参阅图像上传配置参考,了解所有可用选项。
回调处理流程
此端点可以使用任何 Web 框架或云函数实现。您需要集成的关键步骤如下:
- 接收文件: 请求将包含图像文件数据,您需要在服务器上解析这些数据。
- 存储图像: 将图像保存到可通过 URL 访问的位置。这可以是 AWS S3 存储桶、Cloudinary 等存储服务,或您服务器上的公共文件夹。为保存的文件生成公共 URL 或路径。
- 返回 URL: 发送包含图像 URL 的 JSON 响应。例如:
{ "url": "https://my-cdn.com/uploads/unique-image-name.png" }。确保发送 HTTP 200 状态码。转换器将在编辑器内容中使用提供的 URL。
Tiptap 转换服务随后将该 URL 插入到 Tiptap JSON 中,作为图像节点的 src。
重要注意事项
- 公共可访问性: 您提供的端点 URL 必须能够从互联网访问,因为 Tiptap 的云服务将调用它。它不能是 localhost 或位于防火墙后面。同样,返回的图像 URL 应该是公开可访问的(或者至少对需要查看文档的任何人可访问)
- 正确的响应格式: 您的端点应该准确返回一个带有 url 字段的 JSON 对象。如果转换服务无法解析响应或找不到 URL,图像将不会被插入。
- 安全性: Tiptap 不限制您使用哪个端点。您可以通过
imageUploadConfig选项传递自定义请求头(例如Authorization: Bearer ...)和查询参数进行身份验证。转换服务将在上传图像时转发这些请求头。在您的端实现任何必要的身份验证(例如,在请求头中验证 Bearer 令牌或 API 密钥)。 - 图像持久性: 您返回的 URL 将在编辑器内容中继续使用。例如,导入后,您的编辑器将包含带有 src: "https://my-cdn.com/uploads/unique-image-name.png" 的图像节点。之后任何导出或查看该内容的人都会尝试加载该 URL。确保图像在这些 URL 上保持可用(不要立即删除它们)
服务器实现示例
此示例显示了一个简单的服务器实现,该实现接受图像上传并将其上传到由环境变量配置的 S3 存储桶。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { Upload } from '@aws-sdk/lib-storage'
import { S3Client } from '@aws-sdk/client-s3'
const {
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_REGION,
AWS_S3_BUCKET,
PORT = '3011',
AWS_ENDPOINT,
AWS_FORCE_STYLE,
} = process.env
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY || !AWS_S3_BUCKET) {
console.error('请提供 AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY 和 AWS_S3_BUCKET')
process.exit(1)
}
const s3 = new S3Client({
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
region: AWS_REGION,
endpoint: AWS_ENDPOINT,
forcePathStyle: AWS_FORCE_STYLE === 'true',
})
const app = new Hono() as Hono<any>
app.post('/upload', async (c) => {
// 如果您使用的是 v2 导入,需要这段代码
const file = await c.req.blob()
const filename = c.req.header('File-Name')
const fileType = c.req.header('Content-Type')
// 结束
// 如果您使用的是 v1 导入,需要这段代码
const body = await c.req.parseBody()
const file = body['file']
const filename = file.name
const fileType = file.type
// 结束
if (!file) {
return c.json({ error: '未上传文件' }, 400)
}
try {
const data = await new Upload({
client: s3,
params: {
Bucket: AWS_S3_BUCKET,
Key: filename,
Body: file,
ContentType: fileType,
},
}).done()
return c.json({ url: data.Location })
} catch (error) {
console.error(error)
return c.json({ error: '文件上传失败' }, 500)
}
})
serve({
fetch: app.fetch,
port: Number(PORT) || 3000,
})这是另一个使用 bun 的实现,没有任何依赖项:
const s3Client = new Bun.S3Client({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION,
bucket: process.env.AWS_BUCKET,
endpoint: process.env.AWS_ENDPOINT,
})
Bun.serve({
port: 8081,
async fetch(req) {
const url = new URL(req.url)
// 处理 /upload 端点上的文件上传
if (url.pathname === '/upload') {
// 如果您使用的是 v2 导入,需要这段代码
const file = await req.blob()
const filename = req.headers.get('File-Name')!
const fileType = req.headers.get('Content-Type')!
// 结束
// 如果您使用的是 v1 导入,需要这段代码
const body = await req.formData()
const file = body.get('file')
const filename = file.name
const fileType = file.type
// 结束
const file = await req.blob()
if (!file) {
return new Response(JSON.stringify({ error: '未上传文件' }), {
status: 400,
headers: {
'content-type': 'application/json',
},
})
}
try {
// 文件已包含名称和类型,因此我们可以直接使用它
const s3File = s3Client.file(filename, { type: fileType })
// 将文件写入 S3
await s3File.write(file)
return new Response(
JSON.stringify({
// 将上传文件的 URL 返回给客户端,以便插入到编辑器中
url: new Response(s3File).headers.get('location'),
}),
{
headers: {
'content-type': 'application/json',
},
},
)
} catch (error) {
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : '文件上传失败',
}),
{
status: 500,
headers: {
'content-type': 'application/json',
},
},
)
}
}
return new Response(JSON.stringify({ error: '未找到' }), {
status: 404,
})
},
})