Ecshop分类页相册图片3步调用完整方案

Ecshop商品分类页默认只显示主图,本文实战记录修改category.php函数与goods_list.lbi模板把goods_gallery表的相册缩略图渲染到分类页商品卡片,并给出N+1查询优化、缓存清理、移动端适配的完整改造记录。

更新 21 分钟阅读 2,501 阅读

Ecshop 默认的商品分类页只显示商品主图,对服装、家居、配饰这种依赖多角度展示的类目而言,单图列表的转化率会比多图列表低 15%-25%。我在 2014 年给一家做女装的客户接 Ecshop 二开时,第一个被指派的需求就是把分类页商品卡片从单图改成多图缩略图,让用户在列表层就能预览正面、背面、细节图。这篇笔记记录的是当时改完上线、又陆续解决了缓存、性能、SEO 几个坑之后的最终方案。

整个改动只涉及两个文件:根目录的 category.php 和模板库 library/goods_list.lbi。前者负责在 SQL 层把相册数据查出来挂到商品数组上,后者负责在前台模板里循环渲染缩略图。理论上 30 分钟就能改完,实际上线后会遇到几个隐蔽的坑——SQL 数量爆炸、缓存失效、模板缩略图重复加载——下面会一一展开。

需求拆解:分类页要展示哪些图

动手改代码之前,先把需求里的图细分清楚。Ecshop 的商品图体系一共有三层。

第一层是商品主图(goods_img / goods_thumb),存在 ecs_goods 表的 goods_img、goods_thumb 字段,是上传商品时设置的封面图。这是分类页默认渲染的图,所有 Ecshop 模板都基于这一字段。

第二层是商品相册(goods_gallery),存在 ecs_goods_gallery 表,每个商品可以挂多张图,每张图有 img_url(原图)和 thumb_url(缩略图)两个字段。详情页的相册轮播就是从这张表读的。

第三层是规格图(goods_attr 关联 attr_pic),是给颜色规格挂的图,主要用在详情页的颜色选择器上。这次需求不涉及第三层,只动前两层。

明确需求是:在分类页的商品卡片下方,紧跟着主图显示 3-4 张相册缩略图,鼠标 hover 时大图区域切换到对应缩略图。这个交互需要前端 JS 配合,本文聚焦在数据查询和模板输出,前端切换的代码会在文末贴一份精简版。

第一步:修改 category.php 把相册数据挂到商品数组

找到根目录的 category.php,定位 category_get_goods 函数。Ecshop 2.7.3 这个函数大约在 486 行,不同版本行号会偏移,按函数签名找最准确。函数签名长这样:

/**
 * 获得分类下的商品
 *
 * @access  public
 * @param   string  $children
 * @return  array
 */
function category_get_goods($children, $brand, $min, $max, $ext, $size, $page, $sort, $order)

函数体里有一段循环组装 $arr 数组的代码,主要负责把 SQL 查询结果整理成模板能用的格式。我们要在这个循环里插入一段新的查询,把当前商品的相册数据读出来。找到 watermark_img 这一段:

if ($watermark_img != '')
{
    $arr[$row['goods_id']]['watermark_img'] =  $watermark_img;
}

$arr[$row['goods_id']]['goods_id']         = $row['goods_id'];

在 $arr[$row['goods_id']]['goods_id'] 这一行下面追加这段代码:

// 调用商品相册缩略图列表
$gid  = $row['goods_id'];
$sqlg = "SELECT img_url, thumb_url FROM " . $GLOBALS['ecs']->table('goods_gallery') . " WHERE goods_id = " . $gid . " ORDER BY img_id ASC LIMIT 4";
$ggres2 = $GLOBALS['db']->getAll($sqlg);

这里我和原版社区代码做了三处优化。第一,加了 ORDER BY img_id ASC 让顺序稳定,否则不同 MySQL 版本下顺序可能不一致,前端展示会跳动。第二,加了 LIMIT 4 限制每个商品最多查 4 张,避免有的商品挂了 20 张图把整个分类页的内存撑爆。第三,删除了原版的 print_r 调试语句——这种调试代码上线后忘记删,会污染前台 HTML,被搜索引擎抓到甚至会影响 SEO。

再往下找到这段代码:

$arr[$row['goods_id']]['goods_img']  = get_image_path($row['goods_id'], $row['goods_img']);
$arr[$row['goods_id']]['url']        = build_uri('goods', array('gid'=>$row['goods_id']), $row['goods_name']);

紧跟着加上一行,把刚才查出来的相册数组挂到商品上:

$arr[$row['goods_id']]['gpic'] = $ggres2;

到这里 PHP 这一头改完了。整个修改只增加了 6 行有效代码,没有改动任何已有逻辑,回滚也只要把这 6 行删掉即可。

第二步:修改 goods_list.lbi 模板循环渲染

找到 library/goods_list.lbi 文件,定位到商品卡片的渲染区域,通常在 li 或 div 块里有 {$goods.goods_img} 这种 Smarty 变量。在主图渲染代码之后插入这段相册循环:

<div class="goods-gallery-thumbs">
<!-- {foreach from=$goods.gpic item=picture name=no} -->
<!-- {if $smarty.foreach.no.iteration < 5} -->
<img class="gallery-thumb"
     src="{if $picture.thumb_url}{$picture.thumb_url}{else}{$picture.img_url}{/if}"
     width="30" height="30"
     alt="{$goods.goods_name}"
     data-large="{if $picture.thumb_url}{$picture.thumb_url}{else}{$picture.img_url}{/if}" />
<!-- {/if} -->
<!-- {/foreach} -->
</div>

这段模板有几个细节值得说一下。其一,{if $smarty.foreach.no.iteration < 5} 限定每个商品最多渲染 4 张缩略图,与 PHP 那一头的 LIMIT 4 对齐。即便 PHP 那一头出 bug 多查了,模板这一层也兜底拦了。其二,{if $picture.thumb_url}{else}{$picture.img_url}{/if} 是缩略图回退逻辑——有些老商品 goods_gallery 表里只有 img_url 没有 thumb_url,此时就用原图,避免显示空白。其三,data-large 属性是给前端 JS 用的,hover 时把 src 替换成 data-large 即可实现切换。

另外一个容易被忽视的点是 from = $goods.gpic 这种点号写法。Smarty 默认 from 后面跟一个数组变量,但实际上 from 也支持点号引用嵌套数组。$goods 是 foreach $goods_list 里的 item,$goods.gpic 就是 $goods 里的 gpic 字段——也就是我们刚才在 category.php 里挂上去的相册数组。这种写法在 Smarty 文档里没有显著标注,第一次用容易卡住。

性能坑:N+1 查询的代价

上线后第二天,客户抱怨分类页加载从 800 毫秒涨到 3 秒。我打开 Ecshop 后台开启 SQL 调试,发现每个商品都会触发一次 goods_gallery 查询。如果分类页一页 24 个商品,就是 24 次额外 SQL,外加分页相关的 count 查询,总查询数翻了一倍。这就是经典的 N+1 查询问题。

解决方法是把循环里的单条 SQL 合并成一条 IN 查询。改进后的 category.php 代码:

// 在 $arr 组装循环之前先批量查相册
$goods_ids = array();
foreach ($res as $row) {
    $goods_ids[] = (int)$row['goods_id'];
}
$gpic_map = array();
if (!empty($goods_ids)) {
    $ids_str = implode(',', $goods_ids);
    $sqlg = "SELECT goods_id, img_url, thumb_url FROM " . $GLOBALS['ecs']->table('goods_gallery') . " WHERE goods_id IN ($ids_str) ORDER BY goods_id ASC, img_id ASC";
    $rows = $GLOBALS['db']->getAll($sqlg);
    foreach ($rows as $r) {
        if (!isset($gpic_map[$r['goods_id']])) {
            $gpic_map[$r['goods_id']] = array();
        }
        if (count($gpic_map[$r['goods_id']]) < 4) {
            $gpic_map[$r['goods_id']][] = $r;
        }
    }
}

然后在原来挂 gpic 的地方改成:

$arr[$row['goods_id']]['gpic'] = isset($gpic_map[$row['goods_id']]) ? $gpic_map[$row['goods_id']] : array();

这样把 24 次单条 SQL 合并成 1 次 IN 查询,分类页加载时间从 3 秒降回 850 毫秒,几乎没有性能损耗。这个优化是 Ecshop 二开里最常被忽视的点——很多教程为了简化只给单条循环 SQL,实际生产环境用必须改成批量查询。

缓存配合:清掉 ec_listcache 否则改动不生效

Ecshop 默认开启了商品列表缓存(ec_listcache),缓存 key 包含商品 ID 列表但不包含 gpic 字段。改完代码刷新页面后,如果发现新挂的相册图没出来,多半是缓存还在。手工清缓存有两种方式。

第一种是后台路径:管理中心 → 系统设置 → 清除缓存,全选所有缓存类型点击清除。这种方式简单但每次改完代码都要点几下。

第二种是命令行直接删 temp 目录的缓存文件:

cd /www/wwwroot/your-ecshop/temp/
rm -rf compiled/* caches/*

开发期间我建议直接在 includes/init.php 里临时把 $smarty->caching = 0 设为关闭,避免每次改完都要清缓存。开发完毕上线时再改回 1。

另外要注意 OPcache。如果服务器开启了 OPcache,改完 category.php 之后 PHP 还是会用旧的字节码,需要 sudo systemctl reload php-fpm 重新加载。生产环境建议用 opcache_invalidate 函数针对单个文件清理,避免影响其它在跑的请求。

SEO 坑:缩略图 alt 与重复 src

这次改动会让分类页 HTML 体积明显增大,每个商品多输出 4 张图的 img 标签。如果不做 SEO 处理,会带来两个问题。

第一个问题是缩略图 alt 全部用了 $goods.goods_name,导致同一个页面里出现 4 个相同 alt 的图片。Google 抓取时会把这视为「关键词堆砌」,影响商品页的相关性评分。建议把模板里 alt 的写法改成:

alt="{$goods.goods_name}-第{$smarty.foreach.no.iteration}张图"

这样每张缩略图的 alt 都带序号,避免重复。如果想做得更细致,可以在 goods_gallery 表里加一个 img_desc 字段存每张图的描述,模板里优先用 img_desc。

第二个问题是缩略图 src 与原图 src 在某些列表页会重复。如果首页推荐区也调用了同一个 goods_list.lbi,就会在一个 HTML 页里出现同一张图多次。Google 的 Lighthouse 会判定为「未使用 srcset 的多次下载」,扣 LCP 分数。解决方法是对相册第一张图做特殊处理:如果 thumb_url 与 goods_thumb 相同,就跳过不渲染缩略图。模板里加一层判断即可。

移动端适配:缩略图尺寸与触屏交互

原版模板的 width="30" 在桌面端没问题,但移动端 30px 太小,手指根本点不到。我后来改成响应式:

.goods-gallery-thumbs .gallery-thumb {
    width: 30px;
    height: 30px;
    margin-right: 4px;
    cursor: pointer;
}
@media (max-width: 768px) {
    .goods-gallery-thumbs .gallery-thumb {
        width: 44px;
        height: 44px;
        margin-right: 8px;
    }
}

44px 是 iOS HIG 推荐的最小可点击区域,符合移动端可用性标准。另外移动端不支持 hover,可以把缩略图 click 事件改成切换大图,用 JS 监听:

document.querySelectorAll('.goods-gallery-thumbs').forEach(function(box) {
    var mainImg = box.parentElement.querySelector('.goods-main-img');
    box.querySelectorAll('.gallery-thumb').forEach(function(thumb) {
        thumb.addEventListener('click', function() {
            mainImg.src = this.dataset.large;
        });
    });
});

这段 JS 用原生 API 写,没有依赖 jQuery。如果你的 Ecshop 主题已经引入了 jQuery,可以用更短的版本,但原生写法兼容性更好。

常见问题解答

Q1:改完 category.php 上线后,分类页报错 Undefined index gpic 怎么办?

这个报错通常是模板被缓存了旧版本,但 PHP 代码已经更新。手工清掉 temp/compiled 目录下的缓存文件就行。如果清完还报错,检查是不是哪个商品没有相册数据导致 $goods.gpic 为空。在模板循环外加一层 {if $goods.gpic} 判断即可解决。

Q2:相册图片顺序为什么和后台拖拽的顺序不一致?

Ecshop 的 goods_gallery 表没有专门的排序字段,默认按 img_id 升序,也就是上传先后顺序。如果客户在后台拖拽过排序,需要在 goods_gallery 表上加一个 sort_order 字段,并在 SQL 里按 sort_order 排序。修改 SQL 为 ORDER BY sort_order ASC, img_id ASC 即可。后台拖拽功能需要单独开发,这是另一个工作量。

Q3:分类页加载变慢,怎么定位是不是这次改动引起的?

开 Ecshop 后台的 SQL 调试。管理中心 → 系统设置 → 商店设置 → 显示 SQL 错误信息和查询数。打开后访问分类页,页面底部会输出本次请求执行的 SQL 数和总耗时。如果 SQL 数和商品数成正比,说明触发了 N+1 查询,要按本文「性能坑」一节的方法改成 IN 批量查询。

Q4:goods_gallery 表有大量历史数据,IN 查询会不会很慢?

goods_gallery 表通常只有几万到几十万行,且 goods_id 字段有索引,IN 查询配合 LIMIT 性能很好。即便 100 万行规模,IN 24 个 goods_id 的查询也能在 5 毫秒内返回。如果发现慢,先 EXPLAIN 一下确认 goods_id 字段确实走了索引,没有的话执行 ALTER TABLE ecs_goods_gallery ADD INDEX idx_goods_id (goods_id) 加上。

Q5:相册图都是 https,但首页混了 http 链接被浏览器拦截怎么处理?

Ecshop 老站点经常遇到这个问题,goods_gallery 表里历史 URL 是 http,新上传的是 https,混排会触发浏览器的 mixed content 警告。彻底解决要写一个 SQL 把表里所有 http 替换为 https:UPDATE ecs_goods_gallery SET img_url = REPLACE(img_url, 'http://', 'https://'), thumb_url = REPLACE(thumb_url, 'http://', 'https://')。批量更新前先备份这张表。如果图片本身没有 https 版本(CDN 不支持),就要另外迁移图床。

Q6:怎么让相册支持懒加载,提升首屏速度?

原生 lazy loading 最简单:把 img 标签加上 loading="lazy" 属性,现代浏览器会自动延迟加载视口外的图片。模板改成 <img loading="lazy" src="..." />。如果要兼容 IE 或老 Safari,建议引入 lozad.js 这类轻量懒加载库,体积只有 1KB。我的实测数据是首页 LCP 从 2.4 秒降到 1.6 秒,性能提升明显。

Q7:这个改动会影响 Ecshop 后台的商品列表吗?

不会。category.php 是前台分类页路由,与后台 admin/goods.php 完全独立。后台的商品列表用的是 admin/templates/goods_list.htm 模板,不调用 goods_list.lbi。两套完全隔离,前台改动不会污染后台。

Q8:如果想给详情页也用这个相册数据,要在哪里改?

详情页(goods.php)默认就会读 goods_gallery,不需要改 PHP。详情页模板是 themes/[当前主题]/goods.dwt,里面有现成的相册区域。如果你想自定义相册区域的样式,直接改 goods.dwt 的相关 div 即可,PHP 数据已经备好了。

分享到
标签
版权声明

本文标题:《Ecshop分类页相册图片3步调用完整方案》

本文链接:https://zhangwenbao.com/ecshop-photo-album-product-list.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交