如何替换 Ecwid checkout button 并避免自动恢复?
结论
Ecwid.OnPageLoaded 只能说明 Ecwid 已切换到某个逻辑页面,并不代表购物车 DOM 已完成最后一次渲染。Ecwid 后续的异步更新可能会重新创建 Checkout button,因此直接调用 .hide() 或替换节点,效果往往无法持续。
建议采用以下方式:
- 用 CSS 持续隐藏原 Checkout button;
- 以幂等方式插入自定义 button,避免重复添加;
- 通过事件委托处理点击;
- 收集信息后,先通过 Ecwid API 写入订单备注,再进入正常的 checkout;
- 如果自定义 button 所在的容器也会被重建,再使用
MutationObserver,不要使用高频 timer。
为什么 button 会自动恢复
下面的代码只会修改执行时已经存在的 DOM 节点:
$button.hide();
.hide() 会给当前节点添加内联样式。当 Ecwid 因购物车校验、运费计算、库存更新或其他异步操作重新渲染购物车区域时,旧节点会被删除。新生成的 Checkout button 没有原来的内联样式,所以看起来像是被“恢复”了。
通常并不是其他脚本撤销了 .hide(),而是 Ecwid 替换了整个节点。Ecwid.OnPageLoaded 也不是 Checkout button 专用的“渲染完成”事件,不适合用来执行一次性的 DOM 替换。
推荐实现
先用 CSS 隐藏 Ecwid 原 button。即使节点被重新创建,这条 CSS 规则仍然有效:
.ecwid-productBrowser-CART
.ecwid-productBrowser-cart-checkoutButton {
display: none !important;
}
然后以幂等方式插入自定义 button:
(function () {
var customButtonId = 'delivery-options-button';
function ensureCustomButton() {
var checkoutButton = document.querySelector(
'.ecwid-productBrowser-cart-checkoutButton'
);
if (!checkoutButton) {
return;
}
var parent = checkoutButton.parentNode;
if (!parent || document.getElementById(customButtonId)) {
return;
}
var button = document.createElement('button');
button.id = customButtonId;
button.type = 'button';
button.textContent = 'Delivery Options';
parent.appendChild(button);
}
Ecwid.OnPageLoaded.add(function (page) {
if (page.type === 'CART') {
ensureCustomButton();
}
});
document.addEventListener('click', function (event) {
var button = event.target.closest('#' + customButtonId);
if (!button) {
return;
}
event.preventDefault();
openDeliveryOptions();
});
function openDeliveryOptions() {
// 显示表单或弹窗,在这里收集额外信息。
}
})();
点击事件通过事件委托绑定在 document 上。即使自定义 button 随购物车区域一起被删除并重新插入,也不需要重新绑定 click handler。
处理 Ecwid 的后续重绘
如果实际测试发现,OnPageLoaded 触发后 Checkout button 的父容器仍会被重建,可以使用 MutationObserver 监听 DOM 变化:
(function () {
var customButtonId = 'delivery-options-button';
var scheduled = false;
function ensureCustomButton() {
scheduled = false;
var checkoutButton = document.querySelector(
'.ecwid-productBrowser-cart-checkoutButton'
);
if (!checkoutButton) {
return;
}
var parent = checkoutButton.parentNode;
if (!parent || document.getElementById(customButtonId)) {
return;
}
var button = document.createElement('button');
button.id = customButtonId;
button.type = 'button';
button.textContent = 'Delivery Options';
parent.appendChild(button);
}
function scheduleUpdate() {
if (scheduled) {
return;
}
scheduled = true;
window.requestAnimationFrame(ensureCustomButton);
}
var observer = new MutationObserver(scheduleUpdate);
observer.observe(document.body, {
childList: true,
subtree: true
});
Ecwid.OnPageLoaded.add(function (page) {
if (page.type === 'CART') {
scheduleUpdate();
}
});
document.addEventListener('click', function (event) {
if (!event.target.closest('#' + customButtonId)) {
return;
}
event.preventDefault();
openDeliveryOptions();
});
function openDeliveryOptions() {
// 收集额外信息。
}
})();
MutationObserver 只在 DOM 发生变化时执行,比反复运行 setInterval 更稳定。回调仍需保持幂等,并通过 requestAnimationFrame 合并短时间内连续发生的变更。
写入 Notes field 后继续 checkout
用户确认输入后,可以先写入订单备注,再进入 Ecwid checkout。Ecwid JavaScript API 是否可用,取决于当前商店和加载的 API 版本,因此需要先检查 Ecwid.Cart.setOrderComments 是否存在:
function continueToCheckout(extraInformation) {
var notes = [
'Delivery options:',
extraInformation
].join('\n');
if (
Ecwid.Cart &&
typeof Ecwid.Cart.setOrderComments === 'function' &&
typeof Ecwid.Cart.gotoCheckout === 'function'
) {
Ecwid.Cart.setOrderComments(notes);
Ecwid.Cart.gotoCheckout();
return;
}
console.error(
'Required Ecwid Cart API methods are not available on this storefront.'
);
}
如果 setOrderComments 是异步方法,或者当前 Ecwid 版本提供 callback 或 Promise,应按照该版本的实际 API 定义,等写入完成后再调用 Ecwid.Cart.gotoCheckout(),以免页面在备注保存前跳转。
注意事项
- 不要依赖
$button.hide(),它只对当前节点有效。 - 不要反复克隆或替换 Ecwid 的原 button,Ecwid 内部可能保存了该节点的状态或事件。
- 自定义元素必须使用固定
id或其他唯一标记,防止重复插入。 - CSS selector 属于 Ecwid 的页面实现细节。主题或 storefront 版本升级后,可能需要重新检查。
- 如果额外信息属于订单的正式结构化数据,应优先考虑 Ecwid 支持的 checkout custom fields,不要把所有内容都拼入 Notes field。
- 不要假设 Ecwid 提供了 Checkout button 专用的公开重绘事件。在没有官方 API 保证的情况下,CSS 配合
MutationObserver是更稳妥的 DOM 层方案。
备注:内容仅供参考。
