转换过程中保留图像
Available in Start planBeta
您导入的一些文档可能包含您希望保留在转换文档中的图像。
注意
Tiptap 不提供图像上传服务。您需要实现自己的服务器来处理图像上传。
导入图像
如果您导入的 DOCX 文件包含图像,则仅在提供图像上传回调 URL 时,转换服务才可以将这些图像包含在生成的 Tiptap JSON 中。
这是您服务器上的一个 URL 端点,转换服务将在导入过程中使用它来卸载图像。
import { Editor } from '@tiptap/core'
import { Import } from '@tiptap-pro/extension-import-docx'
const editor = new Editor({
// ... 其他编辑器选项,
extensions: [
Import.configure({
appId: '<your-app-id>',
token: '<your-jwt>',
imageUploadCallbackUrl: 'https://your-server.com/upload-image'
})
]
})在此配置中,imageUploadCallbackUrl 设置为一个处理接收图像文件的端点(例如,在您的服务器上)。如果未提供此参数,导入器将从文档中删除图像。
当触发导入时,转换服务将把每个嵌入的图像上传到您提供的 URL。
回调过程
此端点可以使用任何 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 不限制您使用的端点。您可以在 URL 中包含令牌或密钥(例如,https://your-server.com/upload-image?key=123)以控制访问。转换服务只会简单地调用该 URL。请在您的服务器上实现任何必要的身份验证(例如,验证请求头或 URL 中的秘密令牌)。
- 图像的持久性: 您返回的 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,
})
},
})