WooCommerce Cat 外掛開發 (四) – 顯示購物車

前情提要:上一篇我們實作了加入購物車的 API,並且認識了 Alpine.js 的基本使用方式,接下來我們要深入購物車表格,讓它可以顯示、編輯以及刪除品項

在把購物車的內容顯示出來之前,我們先來重構與 Store API 最不可或缺的兩個方法:getJSON() 以及 reqJSON(),前者是以 GET 方法請求 API,藉此取得相關資料,像是購物車的明細,後者是修改資料的方法,像是改變購物車商品數量、刪除品項等等,所有與 Store API 的溝通都需要用到這兩個方法。

回到 wp-content/plugins/woocommerce-cat/assets/src/template/cart.js,先前的程式碼如下:

export default (homeUrl,nonce) => ({
    nonce,
    routeCart: homeUrl + '/wp-json/wc/store/v1/cart',
    routeChecout: homeUrl + '/wp-json/wc/store/v1/checkout',

    // 初始化
    init() {
        this.getJSON(this.routeCart);
    },

    // 取得資料請求
    getJSON(route) {
        fetch(route).then(resp => {
            return resp.json()
        }).then(data => {
            return data
        }).catch((error) => {
            console.log(`Error: ${error}`);
        })
    },

    // 修改資料請求
    reqJSON(route, method, body) {
        fetch(route, {
            method,
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
                'Nonce': this.nonce
            },
            body: JSON.stringify(body)
        }).then(resp => {
            return resp.json()
        }).then(data => {
            return data
        }).catch((error) => {
            console.log(`Error: ${error}`);
        })
    }
})

由於我們希望在進入結帳頁的時候,就能先從 Store API 取得目前購物車的內容,因此我們新增一個 cart 屬性,並且在 init() 初始化時使用 getJSON() 方法將取得的結果存到 cart 裡面,直覺上會寫成這樣:

export default (homeUrl,nonce) => ({
    nonce,
    cart:[], // 存放購物車資料

    // 初始化
    init() {
        this.cart = this.getJSON(this.routeCart);
    },

    // 略
})

當這樣寫的時候會發現 cart 完全沒有寫入任何資料,但也沒有顯示任何錯誤訊息,這是怎麼一回事呢?

JavaScript 的非同步處理

原因出在每一次跟 Store API 請求的時候,必須要執行從 HTTP 請求到回傳資料的整個過程,在預設情況下 JS 是用「同步」的方式來執行程式,也就是讀完一行後接著處理下一行,它並不會等待前一行有取得資料的情況下才執行下一行,因此當我們使用 this.cart = this.getJSON(route) 來存放購物車資料時,getJSON() 根本還沒有跑完就完成初始化的動作,造成 cart 還沒有存到資料執行就結束了。

要改善這個問題必須採用「非同步」的作法,也就是要確保前一行的資料已經確實拿到並且存入後,才會繼續執行下一行。在早期的 JavaScript 只能使用回呼函式來確保當 A 函式觸發時再執行 B 函式,B 函示觸發後再執行 C 函示,但這樣的寫法會讓程式碼難以閱讀。

而在新版的 JS 裡面提供了 Promise 物件,可以用 then 串接的方式來確保前一行程式碼已經執行完畢,事實上我們已經在 getJSON() 方法裡面看過它了,JS 內建的 Fetch API 本身就是一種 Promise 物件,所以可以很直覺的寫成這樣:

getJSON(route) {
    fetch(route).then(resp => {
        return resp.json();
    }).then(data => {
        console.log(data)
    }).catch((error) => {
        console.log(`Error: ${error}`);
    })
},

第一個 then 是將 API 的請求結果轉成 JSON 格式後,再交由第二個 then 來回傳,第三個 catch 是當有錯誤發生時的處理,透過 then 的串接可以很直覺的理解為第一件事做完後「然後」做第二件事,這樣就能確保這個流程是非同步的。

除了使用 then 的寫法外,Promise 物件還可以使用關鍵字 async/await 來指定該方法以非同步的方式執行,再拿 getJSON() 為例,我們可以改寫成這樣:

async getJSON(route) {
    let response = await fetch(route);
    let data = await response.json()
    return data;
},

可以看到在方法名稱前面多增加了一個 async 的關鍵字,async 是非同步 asynchronous 的簡稱,多了這個關鍵字之後,我們就可以在這個方法裡面使用 await 來做到跟 then 一樣的效果,首先把 API 的請求結果存到變數 response 裡面,在 fetch 前面多加 await 關鍵字就代表會在取得回傳結果後才存入 response,然後 JSON 化的動作也多加了 await 確保轉換完成後才存入變數 data,最後就能放心的回傳 data

接下來就能在 init() 裡面使用 getJSON() 來取得購物車內容:

init() {
    this.getJSON(this.routeCart).then(data => this.cart = data)
},

這邊需要注意的是加了 asyncgetJSON() 方法變成是 Promise 物件,如果要取得 Promise 物件的執行結果一定要使用 then 來取得,所以這邊會需要再一個 thendata 存到變數 cart 裡面,這樣就能確實在初始化時取得購物車的品項內容。

另外如果你比較少寫 JS 可能會對箭頭函示的寫法有點陌生,因為它有不少的簡化寫法讓我剛接觸的時候總是一頭霧水:

// 以往 function 的寫法
this.getJSON(this.routeCart).then( function( data ){
    this.cart = data
} )

// 改用箭頭函示省略 function
this.getJSON(this.routeCart).then( ( data ) => {
    this.cart = data
} )

// 如果函式只有一個參數,外面的小括弧可以省略
this.getJSON(this.routeCart).then( data => {
    this.cart = data
} )

// 如果函式內容只有一行或是直接 return,外面的大括弧可以省略
this.getJSON(this.routeCart).then( data =>
    this.cart = data
)

// 不換行的寫法
this.getJSON(this.routeCart).then( data => this.cart = data )

由於我希望可以各別處理 getJSONreqJSON 的回傳結果,如果是像一開始直接把 then 寫在裡面就沒辦法自訂了,因此我把 reqJSON 也改成使用 async/await 的寫法,就能針對回傳的 data 做處理:

async reqJSON(route, method, body) {
    let response = await fetch(route, {
        method,
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json",
            'Nonce': this.nonce
        },
        body: JSON.stringify(body)
    });
    return await response.json();
},

稍後會提到的刪除購物車就能這樣用:

removeItem(itemKey) {
    this.reqJSON(`${this.routeCart}/remove-item`, 'POST', {
        'key': itemKey,
    }).then(data => this.cart = data)
},

更完整的 Promise、async/await 以及非同步的知識可以參考以下文章:

購物車物件

從 Store API 拿到購物車的資料後,我們就可以用 log 的方式來查看 cart 的內容,或是使用 Chrome 的 Alpine.js 擴充功能就能直接顯示所有的屬性與方法,並且還可以即時編輯屬性的值來確認顯示結果,個人很推薦使用:https://chrome.google.com/webstore/search/alpine.js?hl=zh-TW

https://oberonlai.blog/wp-content/uploads/woocommerce-cat-4/woocommerce-cat-02.jpg

回到我們的購物車,根據目前所設計的表格,我們會需要的欄位有商品圖片、商品名稱、單價、數量,至於小計則用單價 x 數量來計算,表格內容與對應欄位如下圖:

https://oberonlai.blog/wp-content/uploads/woocommerce-cat-4/woocommerce-cat-01.jpg

一、x-for 迴圈

接下來我們只要使用 Alpine.js 的 x-for 迴圈就可以非常方便的把商品內容顯示出來,在不考慮 CSS 樣式的情況下,基本寫法如下:

<ul>
    <template x-for="item in cart.items" :key="item.id">
        <li>...</li>
    </template>
</ul>

Alpine.js 的迴圈基本公式要用 <template> 標籤把重複的內容包起來,然後屬性 x-for 的值為「元素名稱 in 陣列」,由於我們要的東西在 items 陣列裡面,因此會寫成 item in cart.items,這樣就能用 item.xxx 來取得元素的屬性,至於 item 可以隨意命名。

至於 :key="item.id" 為每個元素的識別符號,需採用不重複的屬性來作為值,這邊使用商品 ID 來作為 key。

二、x-bind 綁定圖片網址

接下來取得商品圖片,圖片在 images 陣列裡面,我們直接拿第一張圖即可:

<ul>
    <template x-for="item in cart.items" :key="item.id">
        <li>
             <!--商品圖片-->
            <img :src="item.images[0].src" />
        </li>
    </template>
</ul>

:src 是 Alpine.js 綁定屬性 x-bind 的寫法,它可以讓原始的 HTML 屬性像是 srcaltstyle 傳入 JS 參數,因此原本要顯示圖片的語法為 <img src="圖片網址">,修改後變成 <img :src="JS參數">,而商品第一張圖片的路徑放在 item.images[0].src 裡面,因此最後寫成 <img :src="item.images[0].src" />

x-bind 可以跟原始的 HTML 屬性並存,如果寫成 <img src="圖片網址" :src="JS參數"> 也是沒問題,但在 src 上就顯得沒必要,實務上會並用的是 CSS class 屬性,這樣就能保有原始的 class 以及動態新增的 class。像這樣:<img class="w:100%" :class=" 5>3 ? 'h:500' : 'h:300' ">,當 5>3 的話圖片高度就會是 500 像素高,後面會有更多實際案例。

三、x-text / x-html 輸出文字

下一個處理商品名稱:

<ul>
    <template x-for="item in cart.items" :key="item.id">
        <li>
             <!--商品圖片-->
            <img :src="item.images[0].src" />

            <!--商品名稱-->
            <h3 x-text="item.name"></h3>
        </li>
    </template>
</ul>

如果要輸出文字使用 x-text 屬性即可,假設 item.name 叫做筆記本的話,那麼這個寫法 <h3 x-text="item.name"></h3> 輸出後就會變成 <h3>筆記本</h3>,另外輸出文字還可以使用 x-html,跟 x-text 的差別在於後者會過濾掉 HTML 的標籤以確保輸出的資料只有純文字,用 x-html 的話則能顯示 HTML 標籤,通常都會用 x-text 輸出以避免資料來源被植入惡意程式碼。

然後商品單價也是使用 x-text

<ul>
    <template x-for="item in cart.items" :key="item.id">
        <li>
             <!--商品圖片-->
            <img :src="item.images[0].src" />

            <!--商品名稱-->
            <h3 x-text="item.name"></h3>

            <!--單價-->
            <p x-text="item.prices.currency_symbol+new Intl.NumberFormat().format(item.prices.price)"></p>
        </li>
    </template>
</ul>

可以看到 x-text 裡面跟平常的 JS 一樣可以做字串組合或是加減乘除,item.prices.currency_symbol 是 Store API 回傳的貨幣符號,這邊我們拿到的是「NT$」,然後 item.prices.price 是商品價格,這邊用了 NumberFormat() 物件來將商品增加三位逗點的格式,因此最後輸出的結果是 <p>NT$2,000</p>

小計的部分跟單價差不多,一樣是用 x-text 來輸出,只有輸出結果是用單價乘以數量:

<ul>
    <template x-for="item in cart.items" :key="item.id">
        <li>
             <!--商品圖片-->
            <img :src="item.images[0].src" />

            <!--商品名稱-->
            <h3 x-text="item.name"></h3>

            <!--單價-->
            <p x-text="item.prices.currency_symbol+new Intl.NumberFormat().format(item.prices.price)"></p>

            <!--數量-->
            <div>...</div>

            <!--小計-->
            <div class="float:right f:24 f:red-50 float:none@sm mt:10 mt:0@sm" x-text="item.prices.currency_symbol+new Intl.NumberFormat().format((item.prices.price*item.quantity))"></div>

            <!--刪除-->
            <div>...</div>

        </li>
    </template>
</ul>

至於數量跟刪除的部分會牽涉到點擊事件,以及 Alpine.js 超方便的表單綁定 x-model 功能,這部分我們留待下一篇繼續!

目錄

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2024 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!