阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

上次撈到了商品資料

這次嘗試把加購的商品,連同主商品一起送出

先不實作動態撈加購 id 就先放一個實際商品 id 做測試


這功能乍聽之下簡單,實際上做起來非常複雜

以預設的 theme Dawn 來說 購物車有三種模式

drawer, page, popup 然後三種 ux 表現都不相同

有的會用 form submit 送出到 /cart/add

有的會用 ajax 送出 json 到 /cart/add.js

有的會用 ajax 送出 form data 到 /cart/add.js

奇怪的是,有些看起來像 ajax 但還是送到 /cart/add

到底什麼怪設計 還是我看錯?不想管了 我已經眼花了

先不用 route 來決定解法 就用 http request 內容本身來決定解法好了


我估計是 shopify 各種技術債、前後 api 版本相容性考量 最後才變這樣

更難纏的是 有些看起來是 page 但其實用 fetch 送出 ajax 再 redirect

然後不同 theme 的實作 還有跟一些別的 app 的衝突

我只能說非常難實作

我姑且用 chatgpt 做了一個在 Dawn 三種模式都能用的版本

function initAkawaAddonMonitor({ getAddonItems }) {
  const log = (...args) => console.log("[Akawa Addon]", ...args);

  const interceptFormSubmit = () => {
    const form = document.querySelector("form[action='/cart/add']");
    if (!form) return;

    form.addEventListener("submit", async (e) => {
      const addonItems = getAddonItems?.() || [];
      if (!addonItems.length) return;

      e.preventDefault();

      const mainId = form.querySelector("input[name='id']")?.value;
      if (!mainId) return log("❌ Unable to get main product ID");

      const allItems = [{ id: parseInt(mainId), quantity: 1 }, ...addonItems];

      await fetch("/cart/add.js", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Akawa-Addon": "true",
        },
        body: JSON.stringify({ items: allItems }),
      });
    });
  };

  const interceptAjaxAdd = () => {
    const originalFetch = window.fetch;
    window.fetch = async function (url, options = {}) {
      const isAdd = url.includes("/cart/add") && !options.headers?.["X-Akawa-Addon"];
      if (!isAdd || options.method !== "POST") return originalFetch.apply(this, arguments);

      const addonItems = getAddonItems?.() || [];
      if (!addonItems.length) return originalFetch.apply(this, arguments);

      try {
        if (
          options.headers?.["Content-Type"] === "application/json" &&
          typeof options.body === "string"
        ) {
          const body = JSON.parse(options.body);
          const newItems = [...(body.items || []), ...addonItems];

          log("🚀 Intercepted JSON, merged add-on items", newItems);

          return originalFetch(url, {
            ...options,
            headers: {
              ...options.headers,
              "X-Akawa-Addon": "true",
            },
            body: JSON.stringify({ items: newItems }),
          });
        }

        if (options.body instanceof FormData) {
          const newFormData = new FormData();
          for (const [key, value] of options.body.entries()) {
            newFormData.append(key, value);
          }

          addonItems.forEach((item) => {
            newFormData.append("items[][id]", item.id);
            newFormData.append("items[][quantity]", item.quantity);
          });

          log("🚀 Intercepted FormData, injected add-on items", addonItems);

          return originalFetch(url, {
            ...options,
            headers: {
              ...options.headers,
              "X-Akawa-Addon": "true",
            },
            body: newFormData,
          });
        }
      } catch (err) {
        console.warn("[Akawa Addon] Intercept failed, fallback", err);
      }

      return originalFetch.apply(this, arguments);
    };
  };

  interceptFormSubmit();
  interceptAjaxAdd();
  log("✅ Akawa Universal Addon initialized");
}

直接去修改原生 fetch 函數

而且解法哈扣到不行 感覺充滿地雷 會跟一堆 theme 以及 app 功能衝突

這實在是很悲劇 相當難讀 相當難維護 但是目前跑起來可以用


另外 在 UI 方面

上次使用 native js 組裝 DOM 寫了一段

非常的醜 非常難維護

我尋找比較好的解法 發現 preact + htm 可以很好地解決這個問題

// blocks/recommendations.liquid

<div
  id="akawa-recommendations-root"
  data-set-id="{{ block.settings.recommendation_set_id }}"
  ></div>
<script src="{{ 'index.js' | asset_url }}" type="module"></script>

{% schema %}
  {
    "name": "Akawa Recommendations",
    "target": "section",
    "settings": [
      {
        "type": "text",
        "id": "recommendation_set_id",
        "label": "Recommendation Set ID",
        "default": "Click 'Manage app' below to copy the ID from admin panel",
        "info": "Set ID can be found in the app admin panel."
      }
    ]
  }
{% endschema %}

注意有用到 type = module

// assets/index.js

import { h, render } from "https://esm.sh/[email protected]";
import { useEffect, useState } from "https://esm.sh/[email protected]/hooks";
import htm from "https://esm.sh/htm";
const html = htm.bind(h);

function App({ setId }) {
  const [items, setItems] = useState([]);
  const [error, setError] = useState("");

  useEffect(() => {
    fetch(`https://akawa-upsell.turn.tw/api/recommendations?set_id=${setId}`)
      .then(res => {
        if (!res.ok) throw new Error("Failed to fetch recommendations");
        return res.json();
      })
      .then(json => {
        if (json.data.length === 0) {
          setError("No recommendations found.");
        } else {
          setItems(json.data);
        }
      })
      .catch(err => {
        console.error(err);
        setError("Error loading recommendations.");
      });
  }, [setId]);

  if (error) return html`<p>${error}</p>`;

  return html`
    <div style="display: flex; flex-wrap: wrap; gap: 16px;">
      ${items.map(
        (item) => html`
          <div style="border: 1px solid #ddd; padding: 12px; border-radius: 8px; width: 150px;">
            <img src="${item.image}" alt="${item.title}" style="width: 100%; height: auto;" />
            <p style="font-weight: bold; margin: 8px 0 4px;">${item.title}</p>
            <p style="color: gray;">${item.override_price ? `$${item.override_price}` : ""}</p>
            <a href="/products/${item.handle}">View</a>
          </div>
        `
      )}
    </div>
  `;
}

document.addEventListener("DOMContentLoaded", () => {
  debugLog("Akawa recommendations script loaded. version 1.0.3");

  const container = document.getElementById("akawa-recommendations-root");
  if (!container) return;

  const setId = container.dataset.setId;
  if (!setId) {
    container.innerText = "No recommendation set ID found.";
    return;
  }

  render(h(App, { setId }), container);

  initAkawaAddonMonitor({
    getAddonItems: () => [
      {
        id: 42633366306899,
        quantity: 1,
      },
    ],
  });

  hideDynamicButton();
});

成果非常好,我非常滿意,太神奇了


hideDynamicButton 是因為 shopify 有一個 dynamic checkout button 功能

已經夠複雜了 無法支援那個快速結帳功能了 直接把它搞掉,夠瞎吧 但應該只能這樣

function hideDynamicButton() {
  const btn = document.querySelector(".shopify-payment-button");
  if (btn) btn.style.display = "none";
}

感覺最複雜的技術問題,倒是就這樣解決了,再多找一些 theme 試試看相容性

老話一句 我覺得這流程開發起來 門檻很高 非常非常高

😂

按讚的人:

共有 0 則留言


👉 身份:資深全端工程師、指導過無數人半路出家轉職 👉 使命:打造 CodeLove 成為優質新手村,讓非本科也有地方自學&討論

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!