以Shopify的默认方式向商店中添加商品时,整个页面会重新加载。购物者正在查看你的商品,他们点击“加入购物车”按钮,此时浏览器会丢弃当前页面并重新生成它。

在网络连接速度较慢的情况下,这会导致两三秒钟的空白屏幕显示时间。有时,用户甚至会被重定向到“/cart”页面,而这个页面与他们原本想要购买的商品完全无关,这样一来,购物的流程就会被打断。

购物车抽屉功能可以解决这个问题。当有人添加商品时,会弹出一个滑出式面板:购物车信息会实时更新,“结账”按钮也会直接显示在面板上,这样购物者就无需离开当前页面了。

几乎所有转化率较高的Shopify商店都使用了这种功能,而且你不需要任何第三方应用程序来构建它。只需要两个Ajax接口以及不到一百行的JavaScript代码即可实现。

问题在于,大多数教程和AI编程工具都是用一种脆弱的方式来实现这个功能的。它们会使用JavaScript变量来记录商品数量的变化,并手动更新DOM结构。在演示中这种方式看起来没什么问题,但一旦有两行代码被合并成一行,或者出现了折扣优惠,又或者是某种商品在用户点击“加入购物车”和完成结账操作之间售罄了,这种实现方式就会出问题。

本指南会按照资深开发者的方法来构建这个功能,确保服务器端的数据始终是唯一权威的信息来源。同时,它还会介绍2026年版本中的改进措施,让购物车抽屉功能能够与应用程序和AI购物助手使用相同的技术标准进行交互。

如果你想跟着步骤一起编写代码,可以打开一个可编辑的开发主题,然后逐步构建每个部分。这里展示的所有示例都是在使用标准的Online Store 2.0主题(Horizon或Dawn)进行的测试,而且没有安装任何第三方应用程序。

完成的购物车抽屉功能:用户点击“加入购物车”后,一个面板会滑出显示购物车内容,而页面本身不会重新加载

完成的购物车抽屉功能:用户点击“加入购物车”后,面板会滑出显示购物车内容,页面不会重新加载。

目录

你将构建什么

完成制作后,你将会得到这样一个购物抽屉:

  • 能够通过Ajax技术将商品添加到购物车中,而无需重新加载页面。

  • 每次用户对购物车内容进行修改后,都会重新读取购物车信息,并将返回的结果视为最终数据。

  • 能够自行渲染其内部内容,并以滑动的方式展示出来。

  • 仅通过一个监听器就能处理商品数量的变化。

  • 用户可以删除购物车中的某项商品,从而清空整个购物车。

  • 作为最后的优化措施,该购物抽屉还能通过Shopify提供的新标准接口被其他应用程序或人工智能系统所调用。

先决条件

  • 你需要一个可以编辑的Shopify主题(示例中使用的主题为Online Store 2.0系列,如Horizon或Dawn)。

  • 你需要熟悉`fetch`函数以及Promise对象的相关用法。

  • 你需要掌握Liquid模板语言的基础知识。

  • 不需要使用任何第三方应用程序、框架或构建步骤。

第一步:创建购物抽屉的标记代码

你需要将这个购物抽屉设计成一个可以在所有页面上显示的模块,然后在使用JavaScript之前,先通过Liquid模板语言在服务器端生成当前的购物车内容。

这一点非常重要:如果顾客已经有一些商品在购物车中,那么在页面首次加载时,购物抽屉的内容就会正确显示;只有当用户对购物车内容进行修改后,JavaScript才需要更新这些信息。这种设计方式属于“渐进式增强”,但很多自行开发的系统往往会忽略这一步骤。

下面这些`data-*`属性起到了标记代码与JavaScript脚本之间的连接作用。JavaScript想要访问页面的任何内容,都必须通过这些属性来获取信息,而绝不能通过类名或标签的位置来获取数据。


{%- comment -%}
sections/cart-drawer.liquid
{%- endcomment -%}




</div>

这里有两条值得注意的细节:该条目使用的是item.final_line_price,而不是item.line_price。虽然这两种字段都存在于购物车中,但final_line_price反映了针对具体商品的折扣信息,因此消费者实际支付的金额就是这个数值。此外,每个

  • 标签都包含了data-line-keydata-quantity这两个字段,后续的数量统计功能会读取这些数据。

    添加按钮位于产品卡片或产品页面上,它直接从Liquid模板中获取相应变体的ID,因此每次点击都会添加正确的变量:

    {%- comment -%} 在产品卡片/详细产品页面中 {%- endcomment -%}
    
    

    步骤2:无需重新加载页面即可加入购物车

    整个流程可以用一句话来概括:先修改购物车的数据,然后重新读取这些数据,最后将其呈现出来并打开购物车抽屉。具体来说,就是先发送POST /cart/add.js请求来添加新的商品变量,然后再通过GET /cart.js获取完整的购物车信息,最后根据获取到的数据来显示购物车抽屉。

    // assets/cart-drawer.js
    document.querySelectorAll("[data-add"]').forEach(function (btn) {
      btn.addEventListener("click", function () {
        var id = Number(btn.getAttribute("data-variant-id"));
        fetch("/cart/add.js", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ id: id, quantity: 1 }),
        })
          .then(function (r) { return r.json(); })
          .then(function () { return refresh(); })  // 重新读取购物车数据
          .then(openDrawer);                         // 然后打开购物车抽屉
      });
    });
    

    既然/cart/add.js已经返回了相应的购物车数据,为什么还需要再次读取呢?因为add.js返回的只是新增商品的信息,并不是整个购物车的内容,而购物车抽屉显示的正是整个购物车的信息。

    更重要的是,最终决定购物车内容的其实是Shopify。它可能会将新添加的商品合并到现有的购物车中,或者应用自动折扣优惠,也可能会拒绝销售已售罄的商品。要想确保购物车抽屉显示的内容与实际情况一致,唯一的方法就是直接从Shopify服务器获取最新的数据。而refresh()函数正是用来实现这一点的,也是这个文件中唯一一个能够修改购物车UI的函数:

    function refresh() {
      return fetch("/cart.js").then(function (r) { return r.json(); }).then(render);
    }
    

    这里提到的两个接口/cart/add.js/cart.js都属于Shopify的Ajax API,任何店铺都可以直接使用这些接口,而无需进行任何额外的配置。

    三步流程:消费者点击“加入购物车”,脚本会发送请求到/cart/add.js,然后重新读取/cart.js中的数据,最后显示购物车抽屉

    先进行修改,然后重新读取数据,最后再进行渲染。对于“添加”、“数量变更”以及“删除”这些操作,都是使用相同的处理流程。

    步骤3:根据服务器返回的数据来渲染抽屉界面

    render(cart)函数会接收来自/cart.js的响应数据,并据此渲染抽屉界面。需要注意的是,这个函数并不会计算总数或更新计数器;它直接从Shopify返回的对象中获取item_counttotal_price这些数值。

    function money(cents) {
      return "$" + (cents / 100).toFixed(2);
    }
    
    function render(cart) {
      document.querySelector("[data-cart-count]").textContent = cart.item_count;
      document.querySelector("[data-cart-subtotal"]').textContent = money/cart.total_price);
    
      var itemsEl = document.querySelector("[data-drawer-items"];
      var emptyEl = document.querySelector("[data-drawer-empty"];
      itemsEl.innerHTML = "";
      if (!cart.items.length) { emptyEl.style.display = "block"; return; }
      emptyEl.style.display = "none";
    
      cart.items.forEach(function (line) {
        var li = document.createElement("li");
        li.className = "drawer__item";
        li.setAttribute("data-line", "");
        li.setAttribute("data-line-key", line.key);
        li.setAttribute("data-quantity", line.quantity);
        li.innerHTML =
          '<img class="drawer__item-img" src="' + (line.image || "") + '" alt="">' +
          '<span class="drawer__item-title">' + line.title + "</span>" +
          '<span class="drawer__item-price">' + money(line.final_line_price) + "</span>";
        itemsEl.appendChild(li);
      });
    }
    

    让人容易犯错的地方就是money()这个函数。Ajax API返回的价格是以为单位的,因此价格为1899时,实际上代表的是$18.99;如果忘记将这个数值除以100,就可能会错误地显示为$1,899。而money()函数正好负责完成这种单位转换。

    关于生产环境需要注意的一点是:final_line_price这个字段包含了商品的单价以及折扣信息,因此顾客实际支付的金额就是这个数值(而line_price代表的是打折前的价格)。在支持多种货币的商店中,应该使用Shopify自带的货币格式化功能,以确保货币符号和小数位数符合当前市场的习惯。

    在咖啡店网站上,抽屉界面已打开,其中显示了一项商品的信息,包括图片、标题、价格、小计以及“结账”按钮。

    整个抽屉界面的渲染都是基于 /cart.js中返回的数据进行的。商品数量、详细信息以及小计数值全都来自服务器。

    步骤4:利用事件委托实现数量的增减操作

    每个抽屉都允许购物者调整商品的数量。为“增加”和“减少”按钮设置连接线路的明显方法是:将导线绕过这些按钮,然后分别为它们绑定点击事件处理程序。但这样做的话,在用户进行第一次数量调整之后,这些控制功能就会失效了。

    原因如下:每次购物车的内容发生变化时,列表都会被重新渲染。这样,那些

  • 元素就会被替换掉,而你之前为这些旧元素绑定的处理函数也会随之失效。新的按钮根本没有监听器。

    解决这个问题的方法是使用事件委托机制。在永远不会被替换的父元素(即

  • Comments are closed.