本文约 5,955 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:三千张图传上去,媒体库卡到打不开
有人用 WordPress 做摄影素材站,第一版操作是:把本地三千张照片打包,用后台的”添加媒体”一次性上传。上传到一半超时,重试几次后库里多了一堆重复的附件;好不容易传完,打开媒体库要等十几秒;前台列表页因为没有分页,一次加载几百张原图,页面大小 80MB。
图片站和普通博客的技术特征完全不同:数据量大、单文件体积大、元数据(EXIF)丰富、展示方式以网格为主。用管理文章的方式管理图片,必然踩坑。核心要解决四件事:怎么高效入库、怎么保存和展示图片参数、怎么生成合适的缩略图、怎么在不卡死的前提下展示大量图片。
原理分析:图片在 WordPress 里的三层数据
第一层是附件记录(wp_posts 中 post_type='attachment'),代表媒体库里的一张图;第二层是元数据(wp_postmeta 中 _wp_attachment_metadata),由 wp_generate_attachment_metadata() 生成,包含各尺寸缩略图的文件名、宽高,以及从 EXIF 读出的相机参数;第三层是文件系统,即 uploads 目录下的真实文件与其派生尺寸。
批量入库必须同时写这三层,缺一不可。只插入 wp_posts 记录会出现”媒体库里有图但前台不显示”;只拷贝文件不建记录则媒体库看不到。
| 层级 | 关键函数 | 遗漏后果 |
|---|---|---|
| 附件记录 | wp_insert_attachment() |
媒体库不显示 |
| 元数据 | wp_generate_attachment_metadata() |
缩略图失效 |
| 文件系统 | copy() / wp_handle_upload() |
文件 404 |
关于 EXIF,需要知道两件事:一是 PHP 的 exif_read_data() 依赖 exif 扩展,部分精简环境未启用;二是 EXIF 里包含 GPS 坐标,直接展示会泄露拍摄位置,隐私敏感型站点应当剥离。
保姆级实操
步骤 1:准备待导入目录。 把图片按分类放到服务器上的子目录,目录名即分类名。
mkdir -p /tmp/import/{风景,人像,建筑}
# 确认扩展支持
php -m | grep -i exif
php -r 'var_dump(function_exists("exif_read_data"));'
步骤 2:扫描目录批量入库。 用 WP-CLI 脚本,一次性完成三层写入。
<?php
/**
* 用法:wp eval-file import-images.php /tmp/import --path=/var/www/mimangfei
*/
$base = $args[0];
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
$dirs = glob( $base . '/*', GLOB_ONLYDIR );
foreach ( $dirs as $dir ) {
$cat_name = basename( $dir );
$term = term_exists( $cat_name, 'gallery_cat' );
if ( ! $term ) {
$term = wp_insert_term( $cat_name, 'gallery_cat' );
}
foreach ( glob( $dir . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE ) as $file ) {
$filename = basename( $file );
$upload = wp_upload_bits( $filename, null, file_get_contents( $file ) );
if ( ! empty( $upload['error'] ) ) {
WP_CLI::warning( $upload['error'] );
continue;
}
$attach_id = wp_insert_attachment( array(
'guid' => $upload['url'],
'post_mime_type' => $upload['type'],
'post_title' => preg_replace( '/\.[^.]+$/', '', $filename ),
'post_status' => 'inherit',
), $upload['file'] );
$meta = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
wp_update_attachment_metadata( $attach_id, $meta );
wp_set_object_terms( $attach_id, (int) $term['term_id'], 'gallery_cat' );
// 读取 EXIF 并落到自定义字段
if ( function_exists( 'exif_read_data' ) && 'image/jpeg' === $upload['type'] ) {
$exif = @exif_read_data( $upload['file'], 'EXIF', true );
if ( ! empty( $exif['EXIF'] ) ) {
update_post_meta( $attach_id, '_camera_model', sanitize_text_field( $exif['IFD0']['Model'] ?? '' ) );
update_post_meta( $attach_id, '_focal_length', sanitize_text_field( $exif['EXIF']['FocalLength'] ?? '' ) );
update_post_meta( $attach_id, '_aperture', sanitize_text_field( $exif['EXIF']['FNumber'] ?? '' ) );
update_post_meta( $attach_id, '_iso', sanitize_text_field( $exif['EXIF']['ISOSpeedRatings'] ?? '' ) );
// 不保存 GPS,避免泄露拍摄位置
}
}
}
}
WP_CLI::success( '图片导入完成' );
步骤 3:规划缩略图尺寸。 只注册真正用到的尺寸,避免每个附件生成七八个文件。
add_action( 'after_setup_theme', function () {
add_theme_support( 'post-thumbnails' );
add_image_size( 'gallery_grid', 400, 0, false ); // 网格列表
add_image_size( 'gallery_large', 1600, 0, false ); // 详情页大图
} );
add_filter( 'intermediate_image_sizes_advanced', function ( $sizes ) {
unset( $sizes['medium_large'] );
unset( $sizes['1536x1536'] );
unset( $sizes['2048x2048'] );
return $sizes;
} );
步骤 4:前端瀑布流展示。 用 CSS columns 实现,无需 JS 库,配合 srcset 自动选图。
.gallery-masonry {
column-count: 4;
column-gap: 16px;
}
.gallery-masonry__item {
break-inside: avoid;
margin: 0 0 16px;
border-radius: 8px;
overflow: hidden;
background: #f1f5f9;
}
.gallery-masonry__item img {
display: block;
width: 100%;
height: auto;
}
@media (max-width: 1024px) { .gallery-masonry { column-count: 3; } }
@media (max-width: 640px) { .gallery-masonry { column-count: 2; } }
步骤 5:输出带 srcset 的图片。 让浏览器按屏幕大小自动选择,节省移动端流量。
<?php
$ids = get_posts( array(
'post_type' => 'attachment',
'post_mime_type' => 'image',
'posts_per_page' => 24,
'post_status' => 'inherit',
'tax_query' => array( array(
'taxonomy' => 'gallery_cat',
'field' => 'slug',
'terms' => '风景',
) ),
) );
echo '<div class="gallery-masonry">';
foreach ( $ids as $img ) {
echo '<div class="gallery-masonry__item">';
echo wp_get_attachment_image( $img->ID, 'gallery_grid', false, array(
'loading' => 'lazy',
'sizes' => '(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw',
) );
echo '</div>';
}
echo '</div>';
步骤 6:媒体库性能优化。 附件超过五千条后,媒体库列表会明显变慢。用钩子限制列表查询字段,或按分类筛选使用。
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() && $query->is_main_query() && 'attachment' === $query->get( 'post_type' ) ) {
$query->set( 'posts_per_page', 40 );
$query->set( 'no_found_rows', true ); // 跳过总数统计,显著提升速度
}
} );
常见踩坑
- 一次性上传几千张,PHP 报 “Maximum execution time of 30 seconds exceeded”。
- 上传大图提示 “HTTP error.”,实为
post_max_size或 nginxclient_max_body_size太小。 - 媒体库慢到打不开,因为默认查询做了
SQL_CALC_FOUND_ROWS总数统计。 - 每个附件生成七八种尺寸,磁盘占用是原图的三倍。
- 直接展示原图,页面单张 5MB,移动端直接卡死。
- 案例:某摄影站把原图直接放进文章,未生成缩略图。首页一次加载 30 张 4MB 的原图,LCP 超过 12 秒,移动端跳出率 95%。改造后统一输出
gallery_grid尺寸并加loading="lazy",首屏图片总体积从 120MB 降到 2.3MB,LCP 降到 1.8 秒。 - EXIF 里的 GPS 坐标被完整展示,摄影师拍摄地泄露。
- 图片文件名含中文,部分 CDN 回源时出现 404。
总结
图片站是否做对,看三个数字:单张展示图体积 <200KB、列表页首屏图片总体积 <3MB、媒体库打开时间 <2 秒。行动清单:① 批量入库同时写三层数据;② 只注册必要尺寸;③ 用 wp_get_attachment_image() 输出 srcset;④ 剥离 GPS 等敏感 EXIF;⑤ 开启懒加载;⑥ 附件量大时限制媒体库查询。图片站还涉及版权问题,可继续看 图片水印与版权保护、WordPress 图片优化 与 WordPress 媒体库管理。
