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
相关产品推荐
相关产品推荐

