自动图像压缩:使用锋利的Vite插件
#javascript #网络开发人员 #vite #sharp

用Vite和Sharp Library优化图像

我想讨论一种利用尖锐库来创建简单的Vite插件的方法。该插件的目的是:

  1. 压缩原始图像。
  2. .webp.avif格式生成轻量级版本。
  3. 在开发过程中提供最少的手动工作。

tl; dr

  • github存储库:Link

压缩详细信息:原始图像(〜3.1 mb) - >压缩后(〜746 kb),webp(〜92 kb),.avif(〜66 kb)。
< /图>

为什么选择这个?

  • .webp.avif这样的现代图像格式在保持透明度的同时,大小可显着降低。
  • 通常,提供项目中的图像没有压缩,导致不必要的膨胀。
  • 使用诸如TinyPNG之类的工具的手动图像压缩是乏味的。自动化此过程是前进的道路。

使用sharp库将与此努力保持关键。

  • 尖锐的文档:Link

概念
当我们将图像导入import Image from './images/hero.png?optimized';时,插件不仅应返回图像链接。相反,它应返回包含多个链接的对象。然后可以将此对象馈入<picture> HTML标签以进行相应的渲染。

这是一个苗条的组件(Picture.svelte)来证明这一点:

<script lang="ts">
 type OptimizedSrc = {
  avif?: string;
  webp?: string;
  fallback: string;
 };

 export let src: OptimizedSrc | string;

 function getSource(src: OptimizedSrc | string): OptimizedSrc {
  if (typeof src === 'string') return { fallback: src };
  return src;
 }

 $: sources = getSource(src);
</script>

<picture>
 {#if sources.avif}
  <source srcset={sources.avif} type="image/avif" />
 {/if}
 {#if sources.webp}
  <source srcset={sources.webp} type="image/webp" />
 {/if}
 <img src={sources.fallback} alt="" />
</picture>

用法:

<script lang="ts">
 import Image from '../images/source.png?optimized';
 import Picture from './Picture.svelte';
</script>

<div>
 <Picture src={Image} />
</div>

制作插件
我建议参考Vite和Follup提供的详尽文档:

设置应用程序后,请确保您在Vite配置中注册该插件。它应该先优先考虑。

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { imageOptimizerPlugin } from './imageOptimizerPlugin';

export default defineConfig({
 plugins: [imageOptimizerPlugin(), sveltekit()]
});

实际上,插件本身的整个代码。

// imageOptimizerPlugin.ts

import path, { basename, extname } from 'node:path';

import { Plugin } from 'vite';
import sharp from 'sharp';

const pattern = '?optimized';

const isProd = process.env.NODE_ENV === 'production';

function isIdForOptimization(id: string | undefined) {
 return id?.includes(pattern);
}

const forSharpPattern = /\?(webp|avif|fallback)/;

function isIdForSharp(id: string | undefined) {
 return forSharpPattern.test(id || '');
}

function resolveId(id: string, importer: string) {
 return path.resolve(path.dirname(importer), id);
}

export const imageOptimizerPlugin = (): Plugin[] => {

 return [
  {
   name: '?sharp-handler',
   enforce: 'pre',
   async resolveId(id, importer) {
    if (!isIdForSharp(id)) return;

    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return resolveId(id, importer!);
   },
   async load(id) {
    if (!isIdForSharp(id)) return;

    const unwrappedId = id.replace(forSharpPattern, '');
    let [, extension] = id.split('?');

    let buffer: Uint8Array;

    if (extension === 'fallback') {
     buffer = await sharp(unwrappedId)
      .png({ quality: 70, effort: 7, compressionLevel: 6 })
      .toBuffer();
    } else if (extension === 'webp') {
     buffer = await sharp(unwrappedId).webp({ quality: 80 }).toBuffer();
    } else {
     buffer = await sharp(unwrappedId).avif({ quality: 60 }).toBuffer();
    }

    if (extension === 'fallback') extension = extname(unwrappedId).replace('.', '');

    const name = basename(unwrappedId, extname(unwrappedId)) + `.${extension}`;

    const referenceId = this.emitFile({
     type: 'asset',
     name: name,
     needsCodeReference: true,
     source: buffer
    });

    return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
   }
  },
  {
   name: '?optimized-handler',
   enforce: 'pre',
   async resolveId(id, importer) {
    if (!isIdForOptimization(id)) return;

    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return resolveId(id, importer!);
   },
   async load(id) {
    if (!isIdForOptimization(id)) return;

    const unwrappedId = id.replace(pattern, '');

    if (!isProd) {
     return {
      code: `import fallback from "${unwrappedId}";` + `export default { fallback };`,
      map: null
     };
    }

    const webp = JSON.stringify(unwrappedId + '?webp');
    const avif = JSON.stringify(unwrappedId + '?avif');
    const fallback = JSON.stringify(unwrappedId + '?fallback');

    return (
     `import webp from ${webp};` +
     `import avif from ${avif};` +
     `import fallback from ${fallback};` +
     `export default {webp, avif, fallback};`
    );
   }
  }
 ];
};

让我们讨论它的工作原理
您的主要Vite插件实际上由两个子插曲组成:

  • 尖锐的处理程序(?sharp-handler

    • 职责:处理实际图像格式。
    • 解决逻辑:解决图像ID时,它会滤除不需要通过夏普处理的图像。
    • 加载逻辑:基于要生成的特定图像格式,调整了尖锐的参数。每种图像格式(.webp.avif或压缩原始)都是使用其各自的设置生成的。然后将处理后的图像缓冲区发射为具有参考ID的资产。
  • 优化的处理程序(?optimized-handler

    • 职责:处理自定义图像导入。
    • 分辨率逻辑:仅针对以优化标记的图像。
    • 加载逻辑:在开发模式下,它返回原始图像以更快的性能。在生产模式下,它处理图像以生成优化版本并使用所有可用格式构造导出语句。

开发与产品模式:
该插件的行为不同:

  • 开发模式中

  • 生产模式中,图像已完全处理,同时生成.webp.avif格式以及压缩原始。

最后,为防止由于我们的自定义导入而导致的绒毛错误,请从我们量身定制的模块中通知有关预期返回格式的打字稿:

// optimized-module.d.ts
declare module '*?optimized' {
 const value: {
  webp?: string;
  avif?: string;
  fallback: string;
 };
 export default value;
}

总而言之,使用此插件,您的图像将自动优化和转换。例如,在我的测试回购中,有效地将3.1 MB图像降低到746 kb,而.webp.avif版本的重量分别仅为92 kb和66 kb。

最后的样子:
Image description

感谢您的阅读!我相信您会找到这种方法。 ð