Onlook 服务端图片压缩指南:基于 Sharp 的 @onlook/image-server 实践与源码解析
【免费下载链接】onlookThe Cursor for Designers • An Open-Source AI-First Design tool • Visually build, style, and edit your React App with AI项目地址: https://gitcode.com/GitHub_Trending/on/onlook
本篇技术指南围绕 Onlook 开源仓库中的packages/image-server包展开,它是一套仅限服务端运行的图片压缩工具集,底层基于 Sharp(^0.33.5)实现。文章会完整覆盖该包的安装方式、两个核心 API(compressImageServer与batchCompressImagesServer)的调用方法、全部压缩参数与默认值、格式支持/跳过规则、内置压缩预设,并结合 compress.ts 源码、image.test.ts 测试用例以及 image.ts 中的真实 tRPC 集成示例,说明它在 Onlook 实际项目里的落地方式。读完本文,你将能独立在任意 Node.js 服务(API 路由、服务端函数、批处理脚本)中完成图片压缩、格式转换、缩放与批量处理,并理解其设计边界。
一、包定位与使用红线:只能在服务端使用
@onlook/image-server在package.json中的描述是 "Server-side image processing utilities for Onlook",其唯一运行时依赖是sharp(^0.33.5),并要求node >= 18.0.0。
Sharp 是典型的 Node.js 原生模块(依赖 libvips 原生库),因此该包绝对不能在浏览器或 Electron preload 脚本中导入:
- ✅安全使用场景:Node.js 服务器、API 路由(如 Next.js Route Handlers / Server Actions)、服务端函数、批处理脚本
- ❌禁止使用场景:浏览器端代码、Electron preload 脚本、客户端组件
这一约束不仅写在 README 开头,也以注释形式固化在包的入口文件packages/image-server/src/index.ts第一行:
// ⚠️ WARNING: This package contains Node.js-only dependencies (Sharp). Do not use in a browser environment. export * from './compress'; export * from './types';从源码结构看,入口只做两件事:导出压缩实现(compress.ts)与共享类型(types.ts)。这意味着你在任何import { compressImageServer } from '@onlook/image-server'的地方,都应先确认该模块运行在服务端进程内。
二、安装方式
该包是 Onlook monorepo(使用 Bun 管理)内部包,需要服务端图片处理能力的其他包,直接在dependencies中声明即可:
{ "dependencies": { "@onlook/image-server": "*" } }安装后可从包入口获得以下导出:
compressImageServer:单张图片压缩batchCompressImagesServer:批量压缩CompressionOptions/CompressionResult/SupportedFormat:类型定义(详见后文)
三、快速上手:两个核心 API
3.1 单图压缩compressImageServer(input, outputPath?, options?)
签名(compress.ts):
export async function compressImageServer( input: string | Buffer, outputPath?: string, options: CompressionOptions = {}, ): Promise<CompressionResult>input:string | Buffer—— 文件路径或图片 Buffer(Buffer 输入在服务端接收上传、tRPC 传参等场景下非常实用)outputPath:可选。提供则压缩结果写入该文件;不提供则返回内存 Bufferoptions:可选,压缩配置(完整参数见第四节)
典型用法一:压缩并保存为 WebP 文件。
import { compressImageServer } from '@onlook/image-server'; // Compress and save to file const result = await compressImageServer('input.jpg', 'output.webp', { quality: 80, format: 'webp', });典型用法二:压缩到内存 Buffer(适合后续上传、入库或经接口返回)。
// Compress to buffer const result = await compressImageServer('input.jpg', undefined, { quality: 70 });当outputPath缺省时,实现走toBuffer({ resolveWithObject: true })分支,result.buffer即为压缩后的数据(见 compress.ts)。
3.2 批量压缩batchCompressImagesServer(inputPaths, outputDir, options?)
签名(compress.ts):
export async function batchCompressImagesServer( inputPaths: string[], outputDir: string, options: CompressionOptions = {}, ): Promise<CompressionResult[]>import { batchCompressImagesServer } from '@onlook/image-server'; const results = await batchCompressImagesServer( ['image1.jpg', 'image2.png'], './output-directory', { format: 'webp', quality: 85 }, );实现细节(可从源码确认):
- 先
fs.mkdir(outputDir, { recursive: true })确保输出目录存在; - 过滤掉
.ico/.svg路径,并为每个被跳过的文件在结果数组中插入一条success: false的跳过记录,保证返回结果数量与输入数量一一对应; - 剩余文件通过
Promise.all并行调用compressImageServer,输出文件名为原名(去扩展名).输出格式,例如photo.jpg→photo.webp(见 compress.ts)。
注意:批量模式下若format缺省或为'auto',统一按'webp'输出(compress.ts),与单图压缩的“自动推导原格式”策略不同,这是批量场景为了统一输出格式而做的取舍。
四、完整参数表与默认值
CompressionOptions定义在 types.ts,compressImageServer的解构默认值位于 compress.ts,两者合并后如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
quality | number | 80 | 有损格式质量(JPEG/WebP/AVIF 适用),0–100 |
width | number | 未设置 | 缩放目标宽度;与height至少提供一个才触发缩放 |
height | number | 未设置 | 缩放目标高度 |
format | SupportedFormat \| 'auto' | 'auto' | 输出格式:jpeg/png/webp/avif,auto按输入自动推导 |
progressive | boolean | true | 渐进式编码(JPEG/PNG 适用) |
mozjpeg | boolean | true | 是否使用 mozjpeg 编码器(JPEG 适用) |
effort | number | 4 | 编码努力程度(WebP/AVIF 适用,越高压缩率越好、耗时越长) |
compressionLevel | number | 6 | PNG 压缩级别(0–9) |
keepAspectRatio | boolean | true | 缩放时是否保持宽高比。true用sharp.fit.inside,false用sharp.fit.fill(可能拉伸变形) |
withoutEnlargement | boolean | true | 原图小于目标尺寸时不做放大 |
SupportedFormat仅包含四种输出格式(types.ts):
export type SupportedFormat = 'jpeg' | 'png' | 'webp' | 'avif';4.1 缩放逻辑
当width或height存在时,源码会构造 resize 参数:
const resizeOptions = { width, height, fit: keepAspectRatio ? sharp.fit.inside : sharp.fit.fill, withoutEnlargement, }; sharpInstance = sharpInstance.resize(resizeOptions);keepAspectRatio: true:fit: inside,等比缩放,结果不会超过目标矩形,适合缩略图场景;keepAspectRatio: false:fit: fill,强制填满目标尺寸,可能改变宽高比。
4.2 按格式分派的压缩参数
applyFormatCompression(compress.ts)把统一选项映射到各格式的 Sharp 编码参数:
| 输出格式 | 使用的 Sharp 选项 | 生效参数 |
|---|---|---|
jpeg | .jpeg({ quality, progressive, mozjpeg }) | 质量、渐进式、mozjpeg |
png | .png({ compressionLevel, progressive }) | 压缩级别、渐进式 |
webp | .webp({ quality, effort }) | 质量、努力程度 |
avif | .avif({ quality, effort }) | 质量、努力程度 |
| 兜底 | .webp({ quality, effort }) | 同 WebP |
4.3auto格式的推导规则
determineOptimalFormat(compress.ts)根据输入图片的元数据格式(而非仅凭扩展名)决定输出格式:
| 输入格式 | 输出格式 | 理由 |
|---|---|---|
jpeg/jpg | jpeg | 保持照片格式 |
png | png | 保留透明通道与无损特性 |
gif | webp | 动图转静图,压缩率更好 |
tiff/tif | jpeg | 高保真源转常见格式 |
| 其他 / 未知 | webp | 默认选择现代高压缩格式 |
五、支持与跳过的格式
5.1 支持的输入格式(✅)
- JPEG/JPG:有损压缩,适合照片
- PNG:无损压缩,支持透明
- WebP:现代格式,压缩率与画质均衡
- TIFF/TIF:高质量图像
- GIF:动图/静态图(会被转为静态帧)
- BMP:位图
5.2 自动跳过的格式(⏭️)
以下格式会被自动跳过并返回失败结果,而不是尝试压缩:
- ICO:图标文件本身已针对 favicon、应用图标场景优化,直接使用原文件即可
- SVG:矢量图形应保持可缩放,不应栅格化
// These will return { success: false, error: "Skipping ICO/SVG file..." } await compressImageServer('favicon.ico', 'output.webp'); // ❌ Skipped await compressImageServer('logo.svg', 'output.png'); // ❌ Skipped源码中这一判断有两道防线(compress.ts 与 compress.ts):
- 扩展名检查:输入为字符串路径时,
path.extname(input).toLowerCase()命中.ico/.svg直接返回错误,错误信息形如Skipping .ICO file - format not supported for compression. Use original file instead.; - 元数据检查:即使输入是 Buffer(无扩展名),也会调用
sharpInstance.metadata()检查真实格式,若元数据为svg同样返回Skipping SVG format - not supported for compression。这一设计保证“伪装成其他扩展名/以 Buffer 传入的 SVG”也不会被误压缩,测试用例SVG buffer input专门验证了这条路径。
为什么跳过?ICO 已针对 favicon/应用图标场景优化;SVG 是矢量图,压缩会破坏可缩放性。正确做法是直接使用原文件。
六、返回结果结构
CompressionResult(types.ts):
export interface CompressionResult { success: boolean; originalSize?: number; // 原始大小(字节) compressedSize?: number; // 压缩后大小(字节) compressionRatio?: number; // 压缩率百分比:(originalSize - compressedSize) / originalSize * 100 outputPath?: string; // 写入文件时的输出路径 buffer?: Buffer; // 未指定 outputPath 时的压缩结果数据 error?: string; // 失败时的错误信息 }- 原始大小:文件路径输入时取
fs.stat的size;Buffer 输入时取input.length(compress.ts)。 compressionRatio为负数说明压缩后反而更大(例如 PNG 转 PNG 无损场景),这是正常现象,需要调用方按业务判断。
七、使用内置压缩预设
Onlook 在packages/constants/src/files.ts中预置了 4 组常用压缩配置,可直接与compressImageServer组合使用,避免每次手写参数:
import { compressImageServer } from '@onlook/image-server'; import { COMPRESSION_IMAGE_PRESETS } from '@onlook/constants'; // Use predefined presets const result = await compressImageServer('input.jpg', 'output.webp', COMPRESSION_IMAGE_PRESETS.web);各预设的完整定义(与 README 描述一一对应,并可从源码确认精确值):
| 预设 | 用途 | 具体参数(源码值) |
|---|---|---|
web | Web 交付优化 | WebP,quality 80,progressive,effort 4 |
thumbnail | 小缩略图 | 300×300,WebP,quality 70,keepAspectRatio |
highQuality | 高质量输出 | JPEG,quality 95,progressive,mozjpeg |
lowFileSize | 极致压缩体积 | WebP,quality 60,effort 6 |
注意thumbnail预设的 300×300 配合keepAspectRatio: true意味着“等比缩放到 300×300 矩形内”,不会拉伸变形。
八、错误处理与健壮性设计
该包采取“函数永不抛出,错误一律折叠进结果对象”的策略(compress.ts):
} catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred', }; }调用方只需检查result.success:
const result = await compressImageServer('input.jpg'); if (!result.success) { console.error('Compression failed:', result.error); // Common error cases: // - "Skipping .ICO file - format not supported for compression. Use original file instead." // - "Skipping SVG format - not supported for compression. Use original file instead." // - File not found, permission errors, corrupted files, etc. }批量接口batchCompressImagesServer同样把整体异常收敛为单个失败结果数组(compress.ts)。
测试覆盖了哪些错误场景?image.test.ts 中的Input Validation与Error Handling两组用例验证了:
- 不存在的文件 →
success: false - 空字符串输入 →
success: false - 损坏的图片文件(写入非图片内容的假
.jpg)→success: false - 权限错误(输出到
/root/impossible-path.jpg)→success: false - 伪造的
.ico/.svg文件 →success: false - 真实 ICO / SVG 文件 → 正确跳过且不产生输出文件(测试断言输出文件不存在)
九、Onlook 中的真实集成:tRPC 压缩接口
在 Onlook 的 Web 应用中,该包被 image.ts 包装为受保护的 tRPC mutation(image.compress),是“服务端压缩”的教科书式用法:
import { compressImageServer, type CompressionOptions, type CompressionResult } from '@onlook/image-server'; import { z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '../trpc';核心流程(image.ts):
- 客户端上传base64 编码的图片数据;
- 服务端
Buffer.from(input.imageData, 'base64')还原为 Buffer,以 Buffer 形式调用compressImageServer(buffer, undefined, options),不写磁盘; - 由于未指定
outputPath,返回结果中的buffer字段携带压缩后数据; - 服务端将其再转为 base64(
bufferData字段)传回客户端——因为 tRPC 结果序列化不直接支持 Buffer。
同时 project.ts 也在项目相关逻辑中导入了compressImageServer,用于服务端图片落盘前的压缩。这两个真实调用点可以印证:Buffer 输入 + 不落盘 + 结果折叠的组合是该包在 API 服务中的主流用法。
十、测试体系与验证方式
包内测试位于packages/image-server/test/image.test.ts,使用bun:test运行,测试样本图片存放在test/images/input(含favicon.ico、jpg.jpg、png.png、svg.svg、webp.webp)。测试维度包括:
- 输入校验:不存在的文件、空输入
- 真实图片压缩:JPEG、PNG 压缩到 WebP,校验
originalSize > 0、compressedSize > 0、compressionRatio > 0且输出文件真实存在 - auto 格式推导:不指定格式时的自动输出
- 缩放:指定
width/height后正确输出 - Buffer 输出:
result.buffer为合法 Buffer 且长度与compressedSize一致 - ICO / SVG 跳过:单图与批量两条路径均验证
success: false、错误信息包含Skipping .ICO file/Skipping .SVG file、且输出文件未被创建;Buffer 形式的 SVG 也正确跳过 - 混合格式批量:结果数量与输入一致,成功项有输出文件,被跳过项错误信息正确
- 质量对比:
[95, 80, 65, 50]多档质量压缩全部成功 - 异常处理:损坏文件、权限错误、伪造的 ICO/SVG
十一、小结与最佳实践
@onlook/image-server是一个聚焦且健壮的服务端图片压缩模块,围绕 Sharp 封装了格式推导、格式专属压缩参数、缩放、批量处理与 ICO/SVG 防御性跳过,并把所有错误折叠进结果对象。在实际项目中使用时,建议遵循以下实践:
- 严守服务端边界:只在 Node.js 进程(API 路由、服务端函数、脚本)中导入,勿在浏览器或 Electron preload 中使用;
- 优先用预设:Web 交付用
COMPRESSION_IMAGE_PRESETS.web,缩略图用thumbnail,追求画质用highQuality,追求体积用lowFileSize; - 善用 Buffer 模式:处理上传流或需要返回压缩结果的接口时,省略
outputPath,从result.buffer取数据; - 始终检查
success:该包不抛异常,跳过与失败都以success: false+error表达,调用方务必显式处理; - ICO/SVG 直接透传:它们会被跳过是设计行为,请直接使用原文件。
许可证:Apache-2.0。
【免费下载链接】onlookThe Cursor for Designers • An Open-Source AI-First Design tool • Visually build, style, and edit your React App with AI项目地址: https://gitcode.com/GitHub_Trending/on/onlook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考