😎vite自动打包压缩图片和转webp的插件

前言

最近写页面的时候,用webp优化图片大小,就想着有没有相关插件可以开发和打包的时候自动帮我转化和压缩。因为用vite打包工具,就去社区找相关插件,可没找到一个比较符合我要求的,就打算自己工作摸鱼写一个吧👀。(也算是第一次写vite插件吧😂)

仓库

github: github.com/illusionGD/...

需求

  • 能压缩图片,压缩质量能配置
  • 能自动转webp格式,并且打包后能把图片引用路径的后缀改成.webp
  • 支持开发环境和生产环境
  • 不影响原项目图片资源,开发要无感,使用简单

技术栈

  • sharp:图片压缩、格式转换
  • @vitejs/plugin-vue:vite插件开发

实现思路

生产环境

生产环境要考虑两个功能:

1、压缩图片:这个比较简单,在generateBundle钩子函数里面处理图片的chunk中的buffer就可以了

ts 复制代码
export default function ImageTools() {
    return {
        // hook
      async  generateBundle(_options, bundle) {
          for (const key in bundle){
              // 过滤图片key
              const { ext } = parse(key)
              if (!/(png|jpg|jpeg|webp)$/.test(ext)) {
                  continue
              }
              
              // 处理图片buffer
              if (chunk.source && chunk.source instanceof Buffer) {
                   // 压缩图片,这里就省略逻辑了,可以去看sharp文档
                  const pressBuffer = await pressBufferToImage(chunk.source) 
                  // 替换处理后的buffer
                  chunk.source = pressBuffer
                }
          }
      }
    }
}

2、转webp格式: 还是在generateBundle中,直接copy一份图片的chunk,替换chunk的source和fileName,再添加到bundle中输出

ts 复制代码
export default function ImageTools() {
    return {
        // hook
      async  generateBundle(_options, bundle) {
          for (const key in bundle){
              // 过滤图片key
              ...
              // 处理图片buffer
              ...
              
              /*webp相关逻辑*/
              // 克隆原本的chunk
              const webpChunk = structuredClone(chunk)
              // 生成webp的buffer, 逻辑省略
              const webpBuffer = await toWebpBuffer(chunk.source)
              
              // 更改新chunk的source和fileName
              webpChunk.source = webpBuffer
              
              const ext = extname(path)
              const webpName = key.replace(ext, '.wep')
              webpChunk.fileName = webpName
              
              // 添加到bundle中
              bundle[webpName] = webpChunk
          }
      }
    }
}

3、替换路径后缀为.webp :这里就有点麻烦,需要考虑图片的引入方式和打包的产物,解析产物去替换了

引入方式:

  • css:backgroundbackground-image
  • 组件、html文件中的标签:imgsource<div style="background-image: url('')"></div><div style="background: url('')"></div>
  • import:import 'xxx/xxx/xx.png'

产物, 以vue为例:

css中引入的,打包后还是在css中 组件中的标签引入,打包后是在js中

html文件中的标签:就在html中

知道产物后就比较好替换了,我这里采用一种比较巧妙的方法,不需要转ast就能精准替换路径后缀:

先在generateBundle中收集打包后图片的名称和对应的webp名称:

再替换上述产物文件中的图片后缀:

ts 复制代码
function handleReplaceWebp(str: string) {
  let temp = str
  for (const key in map) { // 这里的map就是上述图片中的对象
    temp = temp.replace(new RegExp(key, 'g'), map[key])
  }
  return temp
}
export default function ImageTools() {
    return {
        // hook
      async  generateBundle(_options, bundle) {
          for (const key in bundle){
              // 过滤图片key
              ...
              // 处理图片buffer
              ...
              // 替换js和css中的图片后缀
             if (/(js|css)$/.test(key) && enableWebp) {
              if (/(js)$/.test(key)) {
                chunk.code = handleReplaceWebp(chunk.code)
              } else if (/(css)$/.test(key)) {
                chunk.source = handleReplaceWebp(chunk.source)
              }
            }
          }
      },
      
      // 替换html中的图片后缀
      async writeBundle(opt, bundle) {
          for (const key in bundle) {
            const chunk = bundle[key] as any
            if (/(html)$/.test(key)) {
              const htmlCode = handleReplaceWebp(chunk.source)
              writeFileSync(join(opt.dir!, chunk.fileName), htmlCode)
            }
          }
        }
    }
}

好了,这就是生产环境大概实现思路了,接下来看开发环境中如何转webp

开发环境

有人可能认为,开发环境并不需要压缩和转webp功能,其实不然,开发环境主要是为了看图片处理后的效果,是否符合预期效果,不然每次都要打包才能看,就有点麻烦了.

开发环境主要考虑以下两点:

  1. 和生产环境一样,需要做压缩和转webp处理
  2. 需要加入缓存,避免每次热更都进行压缩和转webp

压缩和转webp处理

这里就比较简单了,不需要处理bunlde,在请求本地服务器资源hook中(configureServer) 处理并返回图片资源就行:

ts 复制代码
export default function ImageTools() {
    return {
        // hook
     configureServer(server) {
         server.middlewares.use(async (req, res, next) => {
            if (!filterImage(req.url || '')) return next()

            try {
              const filePath = decodeURIComponent(
                path.resolve(process.cwd(), req.url?.split('?')[0].slice(1) || '')
              )
                
              // 过滤图片请求
              ...
              
              const buffer = readFileSync(filePath)
              // 处理图片压缩和转webp,返回新的buffer,逻辑省略
              const newBuffer = await pressBufferToImage(buffer)
             
              if (!newBuffer) {
                next()
              }

              res.setHeader('Content-Type', `image/webp`)
              res.end(newBuffer)
            } catch (e) {
              next()
            }
          })
    }
}

缓存图片

这里的思路:

  • 第一次请求图片时,缓存对应图片的文件,并带上hash值
  • 每次请求时都对比缓存文件的hash,有就返回,没有就继续走图片处理逻辑

详细代码就不贴了,这里只写大概逻辑

ts 复制代码
export function getCacheKey({ name, ext, content}: any, factor: AnyObject) {
  const hash = crypto
    .createHash('md5')
    .update(content)
    .update(JSON.stringify(factor))
    .digest('hex')
  return `${name}_${hash.slice(0, 8)}${ext}`
}

export default function ImageTools() {
    return {
        // hook
     configureServer(server) {
         server.middlewares.use(async (req, res, next) => {
            if (!filterImage(req.url || '')) return next()

            try {
              const filePath = decodeURIComponent(
                path.resolve(process.cwd(), req.url?.split('?')[0].slice(1) || '')
              )
                
              // 过滤图片请求
              ...
              const { ext, name } = parse(filePath)
              const file = readFileSync(filePath)
              // 获取图片缓存的key,就是图片hash的名称
              const cacheKey = getCacheKey(
                {
                  name,
                  ext,
                  content: file
                },
                { quality, enableWebp, sharpConfig, enableDevWebp, ext } // 这里传生成hash的因子,方便后续改配置重新缓存图片
              )
              const cachePath = join('node_modules/.cache/vite-plugin-image', cacheKey)

              // 读缓存
              if (existsSync(cachePath)) {
                return readFileSync(cachePath)
              }
              
              // 处理图片压缩和转webp,返回新的buffer
              ...
          })
    }
}

总结

  • 以上就是大致思路了,代码仅供参考
  • GitHub: vite-plugin-image-tools
  • 后续打算继续维护这个仓库并更新更多图片相关功能的,有问题欢迎提issue呀~
相关推荐
小kian2 天前
vite安全漏洞deny解决方案
前端·vite
DevinJohw4 天前
为什么我选择[email protected]
react.js·vite
页面魔术5 天前
[译]专访尤雨溪: 2025年有什么计划?
前端·vue.js·vite
用户74054639943096 天前
Vite开发服务器遇到大量依赖需要优化导致重启的问题
vite
前端与小赵6 天前
webpack和vite之间的区别
前端·webpack·vite
给钱,谢谢!6 天前
记录vite引入sass预编译报错error during build: [vite:css] [sass] Undefined variable.问题
前端·css·sass·vite
小遁哥7 天前
vue3接入tailwindcss3
css·vue.js·vite
xiaoyan20159 天前
vue3.5+deepseek+arco+markdown搭建web版流式输出AI模板
vue.js·vite·deepseek
wordbaby9 天前
Vite 中的 Import 打包机制详解
前端·vite