Onlook 服务端图片压缩指南:基于 Sharp 的 @onlook/image-server 实践与源码解析
2026/9/11 4:44:49 网站建设 项目流程

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(compressImageServerbatchCompressImagesServer)的调用方法、全部压缩参数与默认值、格式支持/跳过规则、内置压缩预设,并结合 compress.ts 源码、image.test.ts 测试用例以及 image.ts 中的真实 tRPC 集成示例,说明它在 Onlook 实际项目里的落地方式。读完本文,你将能独立在任意 Node.js 服务(API 路由、服务端函数、批处理脚本)中完成图片压缩、格式转换、缩放与批量处理,并理解其设计边界。

一、包定位与使用红线:只能在服务端使用

@onlook/image-serverpackage.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>
  • inputstring | Buffer—— 文件路径或图片 Buffer(Buffer 输入在服务端接收上传、tRPC 传参等场景下非常实用)
  • outputPath:可选。提供则压缩结果写入该文件;不提供则返回内存 Buffer
  • options:可选,压缩配置(完整参数见第四节)

典型用法一:压缩并保存为 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 }, );

实现细节(可从源码确认):

  1. fs.mkdir(outputDir, { recursive: true })确保输出目录存在;
  2. 过滤掉.ico/.svg路径,并为每个被跳过的文件在结果数组中插入一条success: false的跳过记录,保证返回结果数量与输入数量一一对应;
  3. 剩余文件通过Promise.all并行调用compressImageServer,输出文件名为原名(去扩展名).输出格式,例如photo.jpgphoto.webp(见 compress.ts)。

注意:批量模式下若format缺省或为'auto',统一按'webp'输出(compress.ts),与单图压缩的“自动推导原格式”策略不同,这是批量场景为了统一输出格式而做的取舍。

四、完整参数表与默认值

CompressionOptions定义在 types.ts,compressImageServer的解构默认值位于 compress.ts,两者合并后如下:

参数类型默认值说明
qualitynumber80有损格式质量(JPEG/WebP/AVIF 适用),0–100
widthnumber未设置缩放目标宽度;与height至少提供一个才触发缩放
heightnumber未设置缩放目标高度
formatSupportedFormat \| 'auto''auto'输出格式:jpeg/png/webp/avifauto按输入自动推导
progressivebooleantrue渐进式编码(JPEG/PNG 适用)
mozjpegbooleantrue是否使用 mozjpeg 编码器(JPEG 适用)
effortnumber4编码努力程度(WebP/AVIF 适用,越高压缩率越好、耗时越长)
compressionLevelnumber6PNG 压缩级别(0–9)
keepAspectRatiobooleantrue缩放时是否保持宽高比。truesharp.fit.insidefalsesharp.fit.fill(可能拉伸变形)
withoutEnlargementbooleantrue原图小于目标尺寸时不做放大

SupportedFormat仅包含四种输出格式(types.ts):

export type SupportedFormat = 'jpeg' | 'png' | 'webp' | 'avif';

4.1 缩放逻辑

widthheight存在时,源码会构造 resize 参数:

const resizeOptions = { width, height, fit: keepAspectRatio ? sharp.fit.inside : sharp.fit.fill, withoutEnlargement, }; sharpInstance = sharpInstance.resize(resizeOptions);
  • keepAspectRatio: truefit: inside,等比缩放,结果不会超过目标矩形,适合缩略图场景;
  • keepAspectRatio: falsefit: 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/jpgjpeg保持照片格式
pngpng保留透明通道与无损特性
gifwebp动图转静图,压缩率更好
tiff/tifjpeg高保真源转常见格式
其他 / 未知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):

  1. 扩展名检查:输入为字符串路径时,path.extname(input).toLowerCase()命中.ico/.svg直接返回错误,错误信息形如Skipping .ICO file - format not supported for compression. Use original file instead.
  2. 元数据检查:即使输入是 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.statsize;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 描述一一对应,并可从源码确认精确值):

预设用途具体参数(源码值)
webWeb 交付优化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 ValidationError 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):

  1. 客户端上传base64 编码的图片数据
  2. 服务端Buffer.from(input.imageData, 'base64')还原为 Buffer,以 Buffer 形式调用compressImageServer(buffer, undefined, options),不写磁盘;
  3. 由于未指定outputPath,返回结果中的buffer字段携带压缩后数据;
  4. 服务端将其再转为 base64(bufferData字段)传回客户端——因为 tRPC 结果序列化不直接支持 Buffer。

同时 project.ts 也在项目相关逻辑中导入了compressImageServer,用于服务端图片落盘前的压缩。这两个真实调用点可以印证:Buffer 输入 + 不落盘 + 结果折叠的组合是该包在 API 服务中的主流用法。

十、测试体系与验证方式

包内测试位于packages/image-server/test/image.test.ts,使用bun:test运行,测试样本图片存放在test/images/input(含favicon.icojpg.jpgpng.pngsvg.svgwebp.webp)。测试维度包括:

  • 输入校验:不存在的文件、空输入
  • 真实图片压缩:JPEG、PNG 压缩到 WebP,校验originalSize > 0compressedSize > 0compressionRatio > 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 防御性跳过,并把所有错误折叠进结果对象。在实际项目中使用时,建议遵循以下实践:

  1. 严守服务端边界:只在 Node.js 进程(API 路由、服务端函数、脚本)中导入,勿在浏览器或 Electron preload 中使用;
  2. 优先用预设:Web 交付用COMPRESSION_IMAGE_PRESETS.web,缩略图用thumbnail,追求画质用highQuality,追求体积用lowFileSize
  3. 善用 Buffer 模式:处理上传流或需要返回压缩结果的接口时,省略outputPath,从result.buffer取数据;
  4. 始终检查success:该包不抛异常,跳过与失败都以success: false+error表达,调用方务必显式处理;
  5. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询