上次撈到了商品資料
這次嘗試把加購的商品,連同主商品一起送出
先不實作動態撈加購 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 試試看相容性
老話一句 我覺得這流程開發起來 門檻很高 非常非常高
😂