You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix.run中如何将AsyncIterable Uint8Array格式图片缩至500px后上传Cloudinary?

解决方案:用Sharp处理AsyncIterable格式图片并上传Cloudinary

要实现上传前将图片缩放到500×500px,你需要先把AsyncIterable<Uint8Array>格式的数据转换为Sharp兼容的流,再通过Sharp处理后传给Cloudinary的上传流。以下是具体实现步骤:

1. 安装依赖

首先安装Sharp包:

npm install sharp

2. 修改uploadImageToCloudinary函数

核心思路是将异步迭代器转换为Node.js可读流,通过Sharp流式处理缩放,再直接管道到Cloudinary的上传流(避免全量加载到内存,更高效):

import { Readable } from "stream"; // 导入Node.js流模块
import sharp from "sharp"; // 导入Sharp

async function uploadImageToCloudinary(data: AsyncIterable<Uint8Array>) {
  let randomFolder = (Math.random() + 1).toString(36).substring(5);

  // 工具函数:将AsyncIterable<Uint8Array>转换为可读流
  const asyncIterableToStream = (iterable: AsyncIterable<Uint8Array>) => 
    Readable.from(iterable);

  return new Promise<UploadApiResponse>((resolve, reject) => {
    const uploadStream = cloudinary.uploader.upload_stream(
      {
        folder: `file_uploaded/${randomFolder}`, // 修正原拼写错误:uplaoded → uploaded
      },
      (error, result) => {
        if (error) return reject(error);
        resolve(result!);
      }
    );

    // 流式处理:异步迭代器→可读流→Sharp缩放→Cloudinary上传流
    asyncIterableToStream(data)
      .pipe(
        sharp().resize(500, 500, {
          fit: "cover", // 裁剪模式:保持比例,填充到500×500(可选值:contain/cover/fill等)
          withoutEnlargement: true, // 禁止放大小于500px的图片
        })
      )
      .pipe(uploadStream)
      .on("error", reject); // 捕获流处理中的错误
  });
}

3. 关键细节说明

  • 流转换:用Readable.from()直接将AsyncIterable<Uint8Array>转为可读流,无需手动收集Buffer,适合大文件场景。
  • Sharp缩放参数:
    • fit: "cover":保持图片比例,裁剪超出部分以适配500×500尺寸;若需要完整显示图片(留空白),可改用fit: "contain"。
    • withoutEnlargement: true:防止原本小于500px的图片被强制放大,避免模糊。
  • 错误处理:给流添加error事件监听,确保处理过程中的错误能被捕获并传递给Promise。

完整修改后的代码

import { redirect, unstable_composeUploadHandlers, unstable_createMemoryUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";
import { writeAsyncIterableToWritable } from "@remix-run/node";
import type { UploadApiResponse } from "cloudinary";
import { v2 as cloudinary } from "cloudinary";
import cuid from "cuid";
import { Readable } from "stream"; // 新增
import sharp from "sharp"; // 新增

import { createInstagram } from "~/models/instagram.server";
import { createNewOrder } from "~/models/order.server";

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

async function uploadImageToCloudinary(data: AsyncIterable<Uint8Array>) {
  let randomFolder = (Math.random() + 1).toString(36).substring(5);

  const asyncIterableToStream = (iterable: AsyncIterable<Uint8Array>) => 
    Readable.from(iterable);

  return new Promise<UploadApiResponse>((resolve, reject) => {
    const uploadStream = cloudinary.uploader.upload_stream(
      {
        folder: `file_uploaded/${randomFolder}`,
      },
      (error, result) => {
        if (error) return reject(error);
        resolve(result!);
      }
    );

    asyncIterableToStream(data)
      .pipe(sharp().resize(500, 500, {
        fit: "cover",
        withoutEnlargement: true,
      }))
      .pipe(uploadStream)
      .on("error", reject);
  });
}

export const action = async ({ request }) => {
  let selectedLargePhotosURLArray: any = [];

  const uploadHandler = unstable_composeUploadHandlers(
    async ({ name, contentType, data, filename }) => {
      const regexFileExtension = /\.[0-9a-z]+$/gm;
      const fileExtension = filename?.match(regexFileExtension);
      if (fileExtension != undefined && fileExtension?.length > 0) {
        const lowCaseFileExt = fileExtension[0].toLowerCase();
        const allowedFileExtension = [".png", ".jpg", ".jpeg", ".heic", ".gif"].includes(lowCaseFileExt);
        
        if (name === "petImages" && filename !== undefined && allowedFileExtension) {
          const uploadedImage = await uploadImageToCloudinary(data);
          console.log(uploadedImage.secure_url);
          selectedLargePhotosURLArray.push(uploadedImage.secure_url);
          return uploadedImage.secure_url;
        }
      }
    },
    unstable_createMemoryUploadHandler({
      maxPartSize: 300000000,
    })
  );

  const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );

  return null;
};

(注:原代码中文件扩展名判断逻辑已简化为数组includes,更简洁易维护)

内容的提问来源于stack exchange,提问作者mrWiga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 16:50:22