Shopify 集合页改造:增加 Banner 图片 + 点击加载更多功能
在做 Shopify 商城二开的时候,我们常常会遇到集合页(Collection Page)用户体验不够流畅的问题:
- 默认的 分页 需要用户不断点击「下一页」才能看完所有商品,流程比较割裂。
- 集合页顶部有时需要插入 活动 Banner 图,但 Shopify 默认没有直接的插槽。
于是,我改造了 Collection 模板,增加了两个功能:
- 在第一页商品列表前插入 Banner 图片
- 将传统分页替换为「点击加载更多」按钮
在原来的结构中增加banner图和按钮的结构 {% if section.settings.img != blank and paginate.current_page == 1 %}保证了只会加载一次,防止重复加载图
<ul id="product-grid" data-id="{{ section.id }}" class=" grid product-grid grid--{{ section.settings.columns_mobile }}-col-tablet-down grid--{{ section.settings.columns_desktop }}-col-desktop {% if section.settings.quick_add == 'bulk' %} collection-quick-add-bulk{% endif %} ">{%- assign cols = section.settings.columns_desktop | plus: 0 -%}{%- assign gutter = 'var(--grid-desktop-horizontal-spacing)' -%}{% if section.settings.img != blank and paginate.current_page == 1 %}<li class="grid__item grid__item--banner pc" style=" width: calc({{ 200 | divided_by: cols }}% - (({{ cols }} - 2) / {{ cols }}) * {{ gutter }}); max-width: calc({{ 200 | divided_by: cols }}% - (({{ cols }} - 2) / {{ cols }}) * {{ gutter }});"><img src="{{ section.settings.img | image_url: width: '1920x' }}" width="{{ section.settings.img.width }}"height="{{ section.settings.img.height }}" loading="lazy" alt="{{ section.settings.img.alt }}"></li>{% endif %}{% assign skip_card_product_styles = false %}{%- for product in collection.products -%}{% assign lazy_load = false %}{%- if forloop.index > 2 -%}{%- assign lazy_load = true -%}{%- endif -%}<li class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}" {% ifsettings.animations_reveal_on_scroll %} data-cascade style="--animation-order: {{ forloop.index }};" {% endif %}>{% render 'card-product'%} <!--产品卡组件 --></li>{%- assign skip_card_product_styles = true -%}{%- endfor -%}
</ul>
<div class="Load-More-inner"><!--使用 paginate 提供“筛选后”的真实总数与每页条数 --><div class="Load-More-number text-text" id="LoadMoreNumber" data-total="{{ paginate.items }}"data-per-page="{{ paginate.page_size }}"><!-- 左侧显示“当前已加载数”,初始为当前页已渲染的产品数量 --><span id="LoadedCount">{{ collection.products | size }}</span>of<span id="TotalCount">{{ paginate.items }}</span></div>{% if paginate.pages > 1 %}<!-- 由 JS 动态计算下一页 --><div class="Load-More btn_4" id="LoadMoreBtn">Load More</div>{% endif %}</div>
Shopify 默认的集合页分页机制,用户每次都要跳页,体验割裂。JS 逻辑优化:初始化 → 监听 → 点击加载 → 状态更新。
- UI 与数据同步:点击加载、筛选、Section 重渲染,都会自动更新按钮和计数。
- 兼容 Shopify:监听 shopify:section:load / facets:updated / MutationObserver。
- 避免重复加载:通过 Math.ceil(totalLoaded / perPage) 动态计算下一页。
- 用户体验优化:按钮有 Loading 状态,加载完毕后自动隐藏。
Ajax 异步加载 + DOM 解析:点击按钮后,用 fetch
拉取下一页 HTML,再用 DOMParser
抽取 #product-grid
里的 <li>
追加。每次加载后,调用 updateLoadMoreUI(),判断按钮显示隐藏。
fetch(url.href).then(r => r.text()).then(html => {const doc = new DOMParser().parseFromString(html, 'text/html');const nextGrid = doc.querySelector('#ProductGridContainer #product-grid');const nextProducts = nextGrid.querySelectorAll('li.grid__item:not(.grid__item--banner)');nextProducts.forEach(item => productGrid.appendChild(item));updateLoadMoreUI();});
由于 Shopify 的 Facet Filter 筛选会刷新整个产品区块,如果不处理,Load More 状态会错乱。我加了一个 MutationObserver,监听 #product-grid
子元素变化:
const observer = new MutationObserver(() => updateLoadMoreUI());
observer.observe(productGrid, { childList: true, subtree: true });
完整的js代码参考
<script>
/* 封装一个计算&展示状态的方法 */
function updateLoadMoreUI() {const productGrid = document.getElementById('product-grid');const loadMoreNumber = document.getElementById('LoadMoreNumber');const btn = document.getElementById('LoadMoreBtn');if (!productGrid || !loadMoreNumber || !btn) return;// 统计当前已加载(排除 banner)const totalLoaded = productGrid.querySelectorAll('li.grid__item:not(.grid__item--banner)').length;// 读取筛选后的“总数/每页”const totalProducts = parseInt(loadMoreNumber.dataset.total, 10) || 0;const perPage = parseInt(loadMoreNumber.dataset.perPage, 10) || 1;// 更新显示const loadedEl = document.getElementById('LoadedCount');const totalEl = document.getElementById('TotalCount');if (loadedEl) loadedEl.textContent = totalLoaded;if (totalEl) totalEl.textContent = totalProducts;// 如果已加载 >= 总数,则隐藏按钮if (totalLoaded >= totalProducts || perPage <= 0) {btn.style.display = 'none';} else {btn.style.display = 'inline-flex'; }
}/* 页面初次加载/筛选后(区块被替换)也确保状态正确 */
document.addEventListener('DOMContentLoaded', updateLoadMoreUI);
window.addEventListener('shopify:section:load', updateLoadMoreUI);
window.addEventListener('shopify:section:render', updateLoadMoreUI);
document.addEventListener('facets:updated', updateLoadMoreUI);//新增 MutationObserver,监听 product-grid 变化(筛选结果替换时触发)
document.addEventListener('DOMContentLoaded', () => {const productGrid = document.getElementById('product-grid');if (productGrid) {const observer = new MutationObserver(() => {updateLoadMoreUI();});observer.observe(productGrid, { childList: true, subtree: true });updateLoadMoreUI(); // 初始执行一次}
});document.addEventListener('click', function (e) {const btn = e.target.closest('#LoadMoreBtn');if (!btn) return;const productGrid = document.getElementById('product-grid');const loadMoreNumber = document.getElementById('LoadMoreNumber');if (!productGrid || !loadMoreNumber) return;// 基于已加载数量/每页动态算下一页(用 Math.ceil 避免重复加载)const perPage = parseInt(loadMoreNumber.dataset.perPage, 10) || 1;const totalProducts = parseInt(loadMoreNumber.dataset.total, 10) || 0;const totalLoadedBefore = productGrid.querySelectorAll('li.grid__item:not(.grid__item--banner)').length;const currentPage = Math.ceil(totalLoadedBefore / perPage);const nextPage = currentPage + 1;// 从现有 URL 保留所有筛选参数,只改 pageconst url = new URL(window.location.href);url.searchParams.set('page', nextPage);// 可选 loading 态const originalBtnText = btn.textContent;btn.textContent = 'Loading…';btn.disabled = true;fetch(url.href).then(response => response.text()).then(html => {const parser = new DOMParser();const doc = parser.parseFromString(html, 'text/html');// 取下一页对应的商品网格const nextGrid = doc.querySelector('#ProductGridContainer #product-grid');if (!nextGrid) {btn.style.display = 'none';return;}const nextProducts = nextGrid.querySelectorAll('li.grid__item:not(.grid__item--banner)');if (!nextProducts.length) {btn.style.display = 'none';return;}// 追加产品nextProducts.forEach(item => productGrid.appendChild(item));updateLoadMoreUI(); // 追加完产品后立刻更新 UI}).catch(err => {console.error('Load More Error:', err);}).finally(() => {btn.textContent = originalBtnText;btn.disabled = false;updateLoadMoreUI(); //兜底再更新一次});
});
</script>