ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

如何用 PHP 和 Flexbox 实现响应式四列图片画廊

如何用 PHP 和 Flexbox 实现响应式四列图片画廊 前言图片画廊看起来是最简单的页面之一实际却经常出三类怪毛病。第一类是排版抽风四列布局在 1080p 显示器上正好换到某台高分屏笔记本第四张图莫名其妙掉到下一行看起来像少了一列。第二类是最后一行被拉宽一行本来放四张最后一行只剩两张这两张就被拉成半个屏幕宽的大图。第三类是移动端挤成一条线断点没设对手机上四列每列只剩 80 像素图片全成了模糊的色块。这三个问题的根源不在一处前两个是 Flexbox 的flex-basis与flex-grow语义没吃透第三个是响应式断点与图片资源尺寸没对齐。而 PHP 在这个组合里的职责被很多人搞错——它不该负责布局它负责的是准备数据、生成正确的图片地址与尺寸声明、并且安全地把它们输出成 HTML。本文先讲清 Flexbox 四列是怎么算出来的再讲图片本身如何不破坏布局最后给出一份带缩略图生成、srcset输出和转义处理的完整可运行示例。一、四列是怎么算出来的Flex 容器里放n列、列间距为gap时每个子项的宽度必须满足列宽 (100% - (n - 1) * gap) / n用 CSS 自定义属性把列数抽出来断点里只改一个变量这是最省心的写法:root { --gap: 16px; } .gallery { display: flex; flex-wrap: wrap; /* 允许换行否则会被压缩成一坨 */ gap: var(--gap); --cols: 4; } .gallery__item { /* 0 不放大、0 不缩小、固定基础宽度 */ flex: 0 0 calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols) - 0.01px); margin: 0; } media (max-width: 1024px) { .gallery { --cols: 3; } } media (max-width: 768px) { .gallery { --cols: 2; } } media (max-width: 480px) { .gallery { --cols: 1; } }flex是flex-grow flex-shrink flex-basis的简写三个值的取舍直接决定布局行为写法含义后果flex: 1放大 1、缩小 1、基础尺寸 auto最后一行项数不足时被拉宽flex: 1 1 220px弹性放大 最小 220px列数自适应但最后一行同样被拉宽flex: 0 0 calc(...)不放大、不缩小、宽度精确计算严格 n 列最后一行保持原宽推荐flex: 0 1 25%不放大、可缩小加上gap后总宽超过 100%会挤成三列最后一行如果希望填满可以改用flex-grow: 1并用占位元素把空缺补齐——库里面只要有n * 列数个元素最后一行就不会缺项?php // 需 PHP 8.0用占位元素补齐最后一行避免末行图片被拉伸 function pad_to_full_row(array $items, int $cols): array { $missing (count($items) % $cols) 0 ? 0 : $cols - (count($items) % $cols); for ($i 0; $i $missing; $i) { $items[] [placeholder true]; } return $items; }CSS Grid 也能做同样的事grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))但 Flexbox 在需要最后一行不拉伸子项高度可不等这类场景下更好控。两者对比需求Flexbox 首选写法Grid 首选写法严格四列flex: 0 0 calc(...) 断点变量grid-template-columns: repeat(4, 1fr)列数自适应flex: 1 1 220px 补齐占位repeat(auto-fit, minmax(220px, 1fr))最后一行不拉伸flex-grow: 0天然满足天然满足等高分块align-items: stretchgrid-auto-rows: 1fr卡片高度不齐天然支持瀑布式需要grid-auto-rows配合二、图片本身不能决定布局画廊之所以抽风一半原因是图片在反过来影响容器图片按自身比例撑高卡片行与行高度不一致图片作为行内元素inline在基线处留出几像素缝隙原图尺寸巨大浏览器解码慢导致布局抖动CLS。正确做法是让容器决定尺寸让图片适应容器.gallery__item { aspect-ratio: 4 / 3; /* 卡片比例固定与图片原始比例无关 */ overflow: hidden; /* 裁掉超出部分 */ border-radius: 8px; background: #eaeaea; /* 图片加载前先占住位置 */ } .gallery__item img { display: block; /* 消除行内元素基线空隙 */ width: 100%; height: 100%; object-fit: cover; /* 等比裁切填满不变形 */ }四个属性的分工要记住aspect-ratio管卡片盒子比例object-fit管图片怎么填满盒子display: block管消除缝隙background管加载前的占位。少任何一个都会出现一种可复现的错位。三、PHP 该做的事数据、尺寸声明、转义PHP 侧的核心职责有四条一条都不能省。第一过滤出真正的图片。用getimagesize()过滤而不是靠扩展名因为扩展名可以随便改。第二输出转义后的属性值。文件名里带引号、尖括号、单引号是常态用户上传的图尤其如此不转义就是存储型 XSS。第三声明图片尺寸与响应式资源。让浏览器在图片下载完成之前就知道盒子有多大同时按屏幕宽度选择合适尺寸的图。第四不信任任何来自请求的路径避免目录穿越。?php // gallery.php 的数据层 —— 需 PHP 8.0 declare(strict_types1); /** return listarray{src: string, alt: string, w: int, h: int, srcset: string} */ function scan_gallery(string $dir, string $baseUrl): array { $items []; foreach (glob($dir . /*.{jpg,jpeg,png,webp}, GLOB_BRACE) ?: [] as $path) { $info getimagesize($path); if ($info false) { continue; // 扩展名像图片但内容不是跳过 } [$w, $h] [$info[0], $info[1]]; if ($w 300 || $h 200) { continue; // 太小的图放上来只会糊 } $name pathinfo($path, PATHINFO_FILENAME); $file rawurlencode(basename($path)); // 缩略图按 400 / 800 / 1200 三档预先生成命名规则为 xxx400.jpg $variants []; foreach ([400, 800, 1200] as $size) { $thumb sprintf(%s/%s%d.%s, $dir, $name, $size, pathinfo($path, PATHINFO_EXTENSION)); if (is_file($thumb)) { $variants[] sprintf(%s/%s%d.%s, $baseUrl, rawurlencode($name), $size, pathinfo($path, PATHINFO_EXTENSION)); } } $items[] [ src $baseUrl . / . $file, alt $name, w $w, h $h, srcset build_srcset($variants, [400, 800, 1200]), ]; } usort($items, static fn (array $a, array $b): int strcmp($a[alt], $b[alt])); return $items; } /** param liststring $urls param listint $widths */ function build_srcset(array $urls, array $widths): string { $parts []; foreach ($urls as $i $url) { if (!isset($widths[$i])) { break; } $parts[] sprintf(%s %dw, $url, $widths[$i]); } return implode(, , $parts); }渲染部分用sprintf拼装所有动态值统一走htmlspecialchars()?php // 需 PHP 8.0 /** param listarray{src: string, alt: string, w: int, h: int, srcset: string} $items */ function render_gallery(array $items): string { $sizes (max-width: 480px) 100vw, (max-width: 768px) 50vw, . (max-width: 1024px) 33vw, 25vw; $cards ; foreach ($items as $item) { $cards . sprintf( figure classgallery__item . img src%s alt%s width%d height%d srcset%s sizes%s . loadinglazy decodingasync . /figure, htmlspecialchars($item[src], ENT_QUOTES), htmlspecialchars($item[alt], ENT_QUOTES), (int) $item[w], (int) $item[h], htmlspecialchars($item[srcset], ENT_QUOTES), htmlspecialchars($sizes, ENT_QUOTES), ); } return div classgallery . $cards . /div; }width和height属性写的是图片的原始像素尺寸浏览器会用它们算出宽高比在 CSS 还没生效时先预留出正确比例的空间这是消除 CLS 的标准手段CSS 里的aspect-ratio: 4 / 3会在样式加载后接管最终比例两者并不冲突。代码实战完整可运行的两步第一步用 GD 生成 12 张测试图需要 GD 扩展。没有现成图片也能跑?php // make_samples.php —— 需 PHP 8.0 且启用 GD declare(strict_types1); $dir __DIR__ . /images; if (!is_dir($dir)) { mkdir($dir, 0775, true); } for ($i 1; $i 12; $i) { $im imagecreatetruecolor(800, 600); // PHP 8.0 起返回 GdImage 对象 $bg imagecolorallocate($im, ($i * 37) % 256, ($i * 71) % 256, ($i * 113) % 256); imagefilledrectangle($im, 0, 0, 800, 600, $bg); imagestring($im, 5, 40, 60, sample {$i}, imagecolorallocate($im, 255, 255, 255)); imagejpeg($im, sprintf(%s/sample%d.jpg, $dir, $i), 85); imagedestroy($im); } echo 已生成 12 张测试图片到 {$dir}\n;第二步把数据层、渲染函数和页面模板合成gallery.php?php // gallery.php —— 需 PHP 8.0 declare(strict_types1); // 这里放 scan_gallery()、build_srcset()、render_gallery() 三个函数 $items scan_gallery(__DIR__ . /images, /images); if ($items []) { http_response_code(200); echo 还没有图片先运行 php make_samples.php; exit; } ? !DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title响应式四列图片画廊/title style :root { --gap: 16px; } body { margin: 24px; font-family: system-ui, Microsoft YaHei, sans-serif; } .gallery { display: flex; flex-wrap: wrap; gap: var(--gap); --cols: 4; } .gallery__item { flex: 0 0 calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols) - 0.01px); margin: 0; aspect-ratio: 4 / 3; overflow: hidden; border-radius: 8px; background: #eaeaea; } .gallery__item img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .25s ease; } .gallery__item:hover img { transform: scale(1.04); } media (max-width: 1024px) { .gallery { --cols: 3; } } media (max-width: 768px) { .gallery { --cols: 2; } } media (max-width: 480px) { .gallery { --cols: 1; } } /style /head body ? render_gallery($items) ? /body /html用内置服务器跑起来即可把两个文件放在同一目录先执行生成脚本再启动服务浏览器打开http://127.0.0.1:8000/gallery.php拖动窗口宽度就能看到 4 / 3 / 2 / 1 列的切换。php make_samples.php php -S 127.0.0.1:8000常见坑点❌ 子项只写flex: 1最后一行只有两张图时被拉成半屏宽✅ 用flex: 0 0 精确宽度如果确实想让末行填满就要用占位元素补齐到整行项数❌calc((100% - 3 * var(--gap)) / 4)算得刚刚好✅ 浮点误差会让第 4 张图在某些缩放比例下掉到下一行减去0.01px留出余量即可稳定四列❌ 图片不加display: block✅img默认是行内元素会在基线处留出几像素空隙行高被撑大加display: block或给容器line-height: 0❌ 不设object-fit指望图片自己适应✅ 竖图与横图混排时卡片会被撑成不同高度。容器用aspect-ratio固定比例图片用object-fit: cover裁切填充❌ 直接把文件名拼进 HTML 属性✅ 上传的图片名可能含引号或尖括号必须htmlspecialchars($v, ENT_QUOTES)URL 部分再过rawurlencode()❌ 用$_GET[dir]之类的参数直接拼进glob()扫描路径✅glob(../../ . $userInput . /*.jpg)可以读到站点外文件。目录必须来自白名单或固定根目录并校验真实路径❌ 手机拍摄的 JPEG 竖图在画廊里显示成横的✅ 相机把方向信息写在 EXIF 的 Orientation 里浏览器不一定遵守。生成缩略图时用exif_read_data()读出 Orientation 并相应旋转、再清掉该标记❌ 直接把几 MB 的原图输出到页面✅ 预先生成 400 / 800 / 1200 三档缩略图用srcsetsizes让浏览器按屏幕宽度选择并加loadinglazy延迟加载视口外的图总结环节关键做法少了它会怎样列宽计算flex: 0 0 calc((100% - (n-1)*gap)/n - 0.01px)列数不稳定、末行被拉宽响应式断点只改 CSS 变量--cols每档都要重写一套宽度规则卡片盒子aspect-ratiooverflow: hidden混排图片高度参差行与行错位图片填充display: blockobject-fit: cover基线缝隙、图片变形尺寸声明输出width/height与srcset/sizes加载时页面抖动安全输出htmlspecialchars()rawurlencode()存储型 XSS、路径穿越资源体积预生成多档缩略图 loadinglazy移动端首屏加载数 MB布局的稳定性和 PHP 的关系其实很间接PHP 只要把每张图的路径是安全的、尺寸是准确的、多档资源是齐全的这三件事做对剩下的交给flex: 0 0加一个 CSS 变量就够了。那些第四张图莫名换行的玄学问题九成来自calc()里没留那 0.01px 的余量以及忘了给gap留出计算空间。
返回列表