使用Cloudflare Images + proxy-go 免费实现单域名多源自适应浏览器优化图片到avif webp

proxy-go的作用是缩短链接, 完全隐藏原图url, 并且可以再套国内CDN or Cloudfront

如果不需要上面三点, 直接用长链接也行

不用proxy-go, 使用cloudflare worker代理也行, 方法写评论里了

Cloudflare Images设置

5000次/月的免费转换

  1. 订阅: Simplify and scale your image pipeline with Cloudflare Images , 登录后左侧"Images-概述", 选择自存储
  2. 左侧"Images-转换"启用需要使用的域, 这里代表图片原域名在线转换链接的根域名, 域名是需要托管在cloudflare上的

这里就可以使用拼接链接进行图片优化了.

比如:

你的启用域名是 https://test.com(需要启用了cloudflare CDN, 如果根域名没有使用Cloudflare的CDN, 用任意二级域名也可以, 比如https://anyone.test.com)

图片的访问域名是 https://s3.test.com/666.jpg

那么可以通过以下链接访问优化后的图片

https://test.com/cdn-cgi/image/format=auto,metadata=none/https://s3.test.com/666.jpg

部署proxy-go

如果只需要最简单代理, nginx应该也可以, 需要注意透传accept并选择合适格式, 添加CF-Image-Format: auto头部和删除CSP, 可以找AI写一下, 我没用就没研究了

proxy-go部署比较适合的是大陆路由直连的国外服务器, 这样速度好点. 阿里腾讯的日本香港也可以

按这个教程 proxy-go部署方法 部署项目

  1. config.json的一部分这样写

     "MAP":{
        "/s3": {
          "DefaultTarget": "https://s3.test.com",
          "ExtensionMap": {
            "jpg,png,jpeg,webp": "https://test.com/cdn-cgi/image/format=auto,metadata=none/https://s3.test.com"
          }
        }
    }...
    
  2. proxy-go 绑定 cdn.test.com 域名

然后图片就可以这样访问:

https://cdn.test.com/s3/666.jpg

会自动根据用户浏览环境, 还有压缩转换后图片大小, 来提供 avif, webp, jpeg等

效果

原图:
3.2M

优化后:
538kb

为什么不是avif?

可以看到截图里有一句: "warning:cf-images 299 “image too large for AVIF”

因为avif更大, 所以使用了最佳文件大小, 也就是webp

如果avif或者webp都比原图大, 那么会使用原图, 比如有些jpeg再转换反而文件变大了, 这时候就用原图

如果avif更小, 那么会使用avif, 比如:

并且, Cloudflare Images"的转换并发非常高, 同时转换几十上百张图片, 几乎没什么延迟, 就非常棒. 比我二次开发的webp-server-go 好用多了, 而且也不占用服务器资源.

唯一缺点:

每个月免费5000次, 超过了需要付费. 个人或者小公司用, 应该不会超过. 因为单个图片独特转换成功后, 一个月内只算作一次, 也就是说, 5000次就代表着5000张图片, 而不是访问5000次.

超过的价格也是可以接受的:

Cloudflare大善人

没错, 腾讯云Edgeone也有这个功能

但是,第一次访问, 慢了2秒, 第二次, 报错…, 第三次, 卡了三四秒, 第四次, 报错…

谁爱用谁用, 我不爱用

Cloudflare Worker写法示例

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Fetch and log a request
 * @param {Request} request
 */
async function handleRequest(request) {
  // Parse request URL to get access to query string
  let url = new URL(request.url)

  // 用 worker 處理 image 時,options 會放在 cf object 裡
  let options = { cf: { image: {} } }

  // 這裡可以用來處理這個 worker 希望支援的 options
  if (url.searchParams.has("fit")) options.cf.image.fit = url.searchParams.get("fit")
  if (url.searchParams.has("width")) options.cf.image.width = url.searchParams.get("width")
  if (url.searchParams.has("height")) options.cf.image.height = url.searchParams.get("height")
  if (url.searchParams.has("quality")) options.cf.image.quality = url.searchParams.get("quality")

  // 用 worker 作 transformation 的話,format option 就不支援 auto 了
  // 需要再額外判斷 reqeust 的 accept header
  const accept = request.headers.get("Accept");
  if (/image\\/avif/.test(accept)) {
    options.cf.image.format = 'avif';
  } else if (/image\\/webp/.test(accept)) {
    options.cf.image.format = 'webp';
  }

  // 用來取得 image 的 source url
  // 如果你的 image url 是有規律的(ex: 放在 S3 的某個 bucket)
  // 那也可以只讓 client 帶上 object key 就好,這樣就能避免 client 拿到 image 的 source url
  const imageURL = url.searchParams.get("image")
  if (!imageURL) return new Response('Missing "image" value', { status: 400 })

  try {
    // TODO: Customize validation logic
    const { hostname, pathname } = new URL(imageURL)

    // Cloudflare images 只支援這些檔案格式,因此如果 request 的檔案格式不在其中的話,直接回傳 400 Error
    // @see <https://developers.cloudflare.com/images/url-format#supported-formats-and-limitations>
    if (!/\\.(jpe?g|png|gif|webp)$/i.test(pathname)) {
      return new Response('Disallowed file extension', { status: 400 })
    }

    // 只處理特定 domain 的 requests
    if (hostname !== 's3.test.com') {
      return new Response('Must use "s3.test.com" source images', { status: 403 })
    }
  } catch (err) {
    return new Response('Invalid "image" value', { status: 400 })
  }

  // Build a request that passes through request headers
  const imageRequest = new Request(imageURL, {
    headers: request.headers
  })

  // Returning fetch() with resizing options will pass through response with the resized image.
  return fetch(imageRequest, options)
}

记得修改上面的s3.test.com为你自己的原图片域名

然后这个worker绑定启用了转换的域名, 比如worker.test.com

那么, 可以通过
https://worker.test.com?image=https://s3.test.com/666.jpg
访问优化后的图片

如果要完全隐藏源图, 可以参考这个修改

export default {
  async fetch(request, env, ctx) {
    const resizingOptions = {
      /* resizing options will be demonstrated in the next example */
    };

    const hiddenImageOrigin = "https://secret.example.com/hidden-directory";
    const requestURL = new URL(request.url);
    // Append the request path such as "/assets/image1.jpg" to the hiddenImageOrigin.
    // You could also process the path to add or remove directories, modify filenames, etc.
    const imageURL = hiddenImageOrigin + requestURL.path;
    // This will fetch image from the given URL, but to the website's visitors this
    // will appear as a response to the original request. Visitor’s browser will
    // not see this URL.
    return fetch(imageURL, { cf: { image: resizingOptions } });
  },
};

使用cloudfront的个人建议设置

缓存策略

源请求策略

响应标头策略

同样图片第二次访问后就会缓存了, 并且应该能根据用户设备判断图片格式.

如果转换有gif图片, 建议加上onerror=redirect

可以在图片超过转换限制时使用原图