購物車與結帳這兩個區塊是整個 WooCommerce 區塊化程度最高、也最容易讓有經驗的開發者踩坑的地方,原因是它們表面上還是「一個頁面」,實際上運作方式跟舊版完全不同 — 舊版是 PHP 產生表單、送出後整頁重載;新版是 React 畫的介面,透過 Store API 跟後端溝通。
先看舊版怎麼運作
回憶一下舊版結帳頁的生命週期:
- 使用者打開
/checkout/,PHP 執行短代碼woocommerce_checkout - WooCommerce 跑一連串 hook 組出 HTML 表單(
woocommerce_checkout_fields這時候生效) - 使用者填完按「下單」,表單 POST 到伺服器
- PHP 驗證、建立訂單、呼叫金流的
process_payment() - 回傳導向網址,整頁跳轉
整個流程的重心在伺服器端,前端只是一張表單。所以要改任何東西,改 PHP 就對了。
新版的生命週期
區塊結帳頁完全不同:
- 使用者打開
/checkout/,PHP 只輸出一個容器加上初始資料 - React 接手,打
GET /wc/store/v1/cart拿購物車狀態,畫出表單 - 使用者每改一個東西(地址、運送方式、折價券),前端就打對應的 API,拿回完整的購物車狀態再重繪
- 按下單時前端打
POST /wc/store/v1/checkout,把表單資料一次送出 - 後端建立訂單、跑金流,回傳
payment_result - 前端依
payment_result決定要導向哪裡
差別在於:表單長什麼樣是前端決定的,後端只提供資料與 schema。 你在 PHP 端改 HTML 沒有意義,因為那段 HTML 根本不存在。
購物車端點回傳什麼
實測一下(這是公開端點,不用登入):
$ curl https://your-site.com/wp-json/wc/store/v1/cart
回傳的頂層欄位:
items, items_count, items_weight, totals, fees, coupons,
shipping_rates, shipping_address, billing_address,
needs_payment, needs_shipping, has_calculated_shipping,
payment_methods, payment_requirements, cross_sells, errors, extensions
幾個值得注意的:
totals是後端算好的,前端不做任何金額計算 — 這很重要,代表折扣、稅、運費的邏輯全部留在 PHP,前端只負責顯示shipping_rates是可選的運送方式清單,選了之後打/cart/select-shipping-rate會拿回重算過的totalserrors是結構化的錯誤,前端據此顯示訊息extensions是留給你的空間,等一下會講
結帳端點 POST /wc/store/v1/checkout 接受的欄位則是:
billing_address, shipping_address, customer_note,
payment_method, payment_data, extensions
回傳裡最關鍵的是 payment_result:
{
"payment_result": {
"payment_status": "success",
"redirect_url": "https://your-site.com/checkout/order-received/123/..."
}
}
前端拿到 payment_status 與 redirect_url 之後才決定跳轉。這個結構在之後串金流時會非常關鍵,因為你的金流要把使用者導到哪裡,是透過這個欄位傳回前端的。
為什麼你熟悉的 hook 失效了
看這幾個常見的舊 hook:
| 舊 hook | 在區塊結帳頁 |
|---|---|
woocommerce_checkout_fields | ❌ 無效,表單不是 PHP 產生的 |
woocommerce_after_checkout_billing_form | ❌ 無效,沒有這個插入點 |
woocommerce_checkout_process | ⚠️ 部分情況不觸發 |
woocommerce_checkout_create_order | ✅ 有效,訂單仍由 PHP 建立 |
woocommerce_thankyou | ✅ 有效 |
woocommerce_order_status_* | ✅ 有效 |
規律很清楚:跟「畫面」有關的 hook 失效,跟「訂單」有關的 hook 仍然有效。 因為訂單的建立、金流的處理、狀態的變化都還在 PHP 端跑,只有介面那一層搬到前端去了。
這也是為什麼之後提到金流、發票、物流時,很多做法跟舊版是一樣的,那些都發生在後端,只有當要新增欄位時,因為牽涉到畫面才需要用新的 API。
要放自訂資料,用 extensions
如果你需要在購物車或結帳的 API 回應裡帶自己的資料(例如「這張訂單符合免運資格」、「這個會員有多少點數」),對應的機制是 ExtendSchema:
use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
add_action( 'woocommerce_blocks_loaded', function () {
$extend = StoreApi::container()->get( ExtendSchema::class );
$extend->register_endpoint_data(
array(
'endpoint' => 'cart', // 只能是有效的 Store API 端點.
'namespace' => 'my-plugin', // 必填,你的外掛命名空間.
'schema_callback' => function () {
return array(
'points_balance' => array(
'description' => __( '會員點數餘額', 'my-plugin' ),
'type' => 'integer',
'readonly' => true,
),
);
},
'data_callback' => function () {
return array( 'points_balance' => (int) get_user_meta( get_current_user_id(), 'points', true ) );
},
)
);
} );
註冊之後,你的資料會出現在 /cart 回應的 extensions 底下:
{ "extensions": { "my-plugin": { "points_balance": 350 } } }
三件要注意的事:
namespace是必填的,而且要用你的外掛前綴,這是避免跟其他外掛撞名的唯一保護endpoint必須是有效端點,給錯會直接丟例外,訊息會列出所有合法值- 要掛在
woocommerce_blocks_loaded之後,太早呼叫拿不到容器
交給 AI 時最容易拿到的錯誤答案
這一篇的內容是整個第四部裡 AI 最容易答錯的地方。你問「怎麼在 WooCommerce 結帳頁加一個欄位」,十次有八次會拿到 woocommerce_checkout_fields 的寫法,那是舊版做法,網路上有上千篇文章這樣寫,而區塊結帳頁上它一點反應都沒有。
分辨方法很簡單,看它給的東西碰不碰得到畫面:
- 只碰訂單、狀態、金流 → 舊做法多半仍然正確
- 要改結帳頁的欄位、版面、驗證訊息 → 舊做法幾乎都無效,要用新的 API
還有一個實用的除錯習慣:遇到「改了沒反應」,先確認那個站的結帳頁到底是哪一種。打開結帳頁看原始碼,搜尋 wp-block-woocommerce-checkout 就是區塊版,看到 <form class="checkout woocommerce-checkout"> 就是傳統短代碼版。
明白資料怎麼流之後,下一篇我們用 WooCommerce 提供的 Additional Checkout Fields API,在區塊結帳頁加一個真正會存進訂單的自訂欄位。