上个月遇到了云图片处理服务的使用体验和一些痛点,今天咱们换个思路,采用 Nginx + PHP 搭一套动态图片处理服务。
用 Nginx + Lua + libvips 搭建图片动态处理服务
建站相关 , 生活日记
162

为什么要做这个?

  说实话,用云服务确实省心,但有几个问题一直困扰着我:

  • 成本问题:按量付费虽然灵活,但被打的时候就头大。
  • 依赖性:过于依赖某个服务商,文件资源长时间存储在平台。
  • 灵活性:自定义处理逻辑受限于平台。
     
    所以我就想,能不能自己搞一套?前面就尝试了纯 Nginx + Lua + libvips 搭建方案,但是有些容器化部署的服务器不方便本地装 libvips,只能想办法用原生 PHP 尝试。

整体架构思路

 这套方案的核心思路:

  1. Nginx 负责路由和静态文件处理
  2. PHP 负责动态生成缩略图
  3. Imagick 作为底层图片处理引擎
     
    整个流程是这样的:用户请求图片 → Nginx 判断是否需要处理 → 需处理就交给 PHP → PHP 调用 Imagick 生成 WebP 格式缩略图 → 返回给用户
# ---------------- 图片处理 ----------------
location /usr/img/ {
    root /www/test/; 
    access_log off; 
    try_files $uri /go.php?$args; 
}

配置说明:

  1. root 指定了网站的根目录,所有图片都在这个目录下
  2. access_log off 关闭了访问日志,减少 I/O 消耗,毕竟图片访问量大
  3. try_files 是关键指令,它会先尝试直接访问 $uri 对应的文件,如果不存在就交给 /go.php

如果原图已经存在,Nginx 会直接返回,不会经过 PHP 处理,性能最优。只有当需要生成变体时,才会触发 PHP 处理逻辑。这种"懒加载"的方式既节省资源,又能保证响应速度。

PHP 处理脚本

<?php
$uri = $_SERVER['REQUEST_URI'];
$uri_path = parse_url($uri, PHP_URL_PATH);

// 匹配原始图片路径和变体参数
if (preg_match('#^(/usr/img/.+\.(jpg|jpeg|png|webp))([!_%-].*)?$#', $uri_path, $matches)) {
    $orig_path = $matches[1];
    $variant   = $matches[3] ?? '';
} else {
    $orig_path = $uri_path;
    $variant   = '';
}

// 模板定义
$templates = [
    'blog'  => ['width' => 300, 'quality' => 80],
    'small' => ['width' => 150, 'quality' => 70],
    'large' => ['width' => 800, 'quality' => 90],
];

// 默认参数
$width   = 300;
$quality = 80;

// 如果 URI 带模板名,例如 !blog !large !small
if (preg_match('#!([a-zA-Z0-9_]+)#', $variant, $m)) {
    $key = $m[1];
    if (isset($templates[$key])) {
        $width   = $templates[$key]['width'];
        $quality = $templates[$key]['quality'];
    }
}

// 如果 URI 带 fw 参数,如 !fw/500
if (preg_match('#fw/(\d+)#', $variant, $m)) {
    $width = max(20, min(5000, (int)$m[1]));
}

// 原图绝对路径
$root_dir = __DIR__ . '/../';
$src = realpath($root_dir . $orig_path);
if (!$src || strpos($src, realpath($root_dir . '/usr/img/')) !== 0) {
    header("HTTP/1.0 404 Not Found");
    exit;
}

// 使用 Imagick 处理图片
$im = new Imagick($src);
$im->setImageFormat('webp');

// 保持透明
if ($im->getImageAlphaChannel()) {
    $im->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
}

// 按宽度缩放,高度自适应
$im->resizeImage($width, 0, Imagick::FILTER_LANCZOS, 1);

// 设置质量
$im->setImageCompressionQuality($quality);

// 输出图片
header("Content-Type: image/webp");
// 浏览器缓存 30 天
header("Cache-Control: public, max-age=2592000");

echo $im;
$im->destroy();

php 代码逐段解析

1. URI 解析与参数提取


if (preg_match('#^(/usr/img/.+\.(jpg|jpeg|png|webp))([!_%-].*)?$#', $uri_path, $matches)) {

    $orig_path = $matches[1];

    $variant   = $matches[3] ?? '';

}

这段正则表达式负责从 URL 中提取两个关键信息:

  • 原始图片路径:比如 /usr/img/test.jpg
  • 变体参数:比如 !blog!fw/500

举个例子:

  • /usr/img/test.jpg!blog → 原图路径是 /usr/img/test.jpg,参数是 !blog
  • /usr/img/test.png!fw/500 → 原图路径是 /usr/img/test.png,参数是 !fw/500

2. 预设模板系统


$templates = [

    'blog'  => ['width' => 300, 'quality' => 80],

    'small' => ['width' => 150, 'quality' => 70],

    'large' => ['width' => 800, 'quality' => 90],

];

我预设了三种常用的图片尺寸模板,这样在使用时就不用每次都手动指定参数了:

  • blog:300px 宽,质量 80(适合博客列表缩略图)
  • small:150px 宽,质量 70(适合小图标、头像)
  • large:800px 宽,质量 90(适合大图展示)

3. 参数解析


// 如果 URI 带模板名,例如 !blog !large !small

if (preg_match('#!([a-zA-Z0-9_]+)#', $variant, $m)) {

    $key = $m[1];

    if (isset($templates[$key])) {

        $width   = $templates[$key]['width'];

        $quality = $templates[$key]['quality'];

    }

}

// 如果 URI 带 fw 参数,如 !fw/500

if (preg_match('#fw/(\d+)#', $variant, $m)) {

    $width = max(20, min(5000, (int)$m[1]));

}

这里有两个参数解析逻辑:

1. 模板名匹配:如果参数是 !blog 这样的,就从预设模板中读取配置

2. 自定义宽度:如果参数是 !fw/500 这样的,就直接使用指定的宽度


URL处理情况说明
/usr/img/xxx.png不处理直接返回原图
/usr/img/xxx.png!不处理参数为空,返回原图
/usr/img/xxx.png!blog处理用 blog 模板,生成300宽缩略图
/usr/img/xxx.png!fw/400处理自定义宽度400px缩略图
/usr/img/xxx.png_fw/500处理用下划线触发,宽度500px缩略图

总结

这套 Nginx + PHP 动态图片处理方案,经过一个月的实际使用,效果还不错:

有啥问题或者想一起聊聊技术细节,随时留言!