探索 Tiptap V3 的最新功能

转换过程中保留图像

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 框架或云函数实现。您需要集成的关键步骤是:

  1. 接收文件: 请求将包含图像文件数据,您需要在服务器上解析这些数据。
  2. 存储图像: 将图像保存到可通过 URL 访问的位置。这可以是 AWS S3 存储桶、Cloudinary 等存储服务,或您服务器上的公共文件夹。为保存的文件生成公共 URL 或路径。
  3. 返回 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,
     })
   },
 })