iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Modern Web

別再說我不會框架,網頁 Vue 起來!系列 第 25

[番外] 一步一步實現購物車功能 [序]

  • 分享至 

  • xImage
  •  

前言

參考 How To Build a Shopping Cart with Vue 3 and Vuex 的手把手教學,一步一步實現購物車功能


Vue CLI 設定專案

$ npm install -g @vue/cli

$ vue create vuex-shopping-cart

建立一個名為 vuex-shopping-cart 的專案,選擇 Manually select features,記得勾選 RouterVuex

$ cd vuex-shopping-cart
$ npm install bulma

bulma 為免費開源的 CSS 框架 (flexbox)

進入檔案 src/main.js,載入 bulma

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './../node_modules/bulma/css/bulma.css'  // <------加一行

createApp(App).use(store).use(router).mount('#app')

安裝 axios (HTTP Library,用來發送請求)

$ npm install axios

確認 server 可正常啟動

$ npm run serve

後端自己當

vue-shopping-cart 同層,建一個 cart-backend

$ mkdir cart-backend
$ cd cart-backend

並在 cart-backend 資料夾下,建立三個檔案

  • server.js 負責 Node.js 伺服器的設定
  • server-cart-data.json 產品內容
  • server-product-data.json 購物車內容
// 初始化專案
$ npm init

安裝後端需要使用的套件

$ npm install concurrently express body-parser
  • Express 為 Node 框架,方便處理 API 請求
  • Concurrently 用來同時跑 Express 後端伺服器以及 Vue.js 的開發伺服器
  • body-parser Express 的 middleware,用來解析請求內容

編輯 server.js

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');     // <--- 用來寫入檔案系統
const path = require('path'); // <--- 方便定義檔案路徑

const app = express();
const PRODUCT_DATA_FILE = path.join(__dirname, 'server-product-data.json');
const CART_DATA_FILE = path.join(__dirname, 'server-cart-data.json');

app.set('port', (process.env.PORT || 3000));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
  next();
});

app.listen(app.get('port'), () => {
  console.log(`Find the server at: http://localhost:${app.get('port')}/`);
});

接下來新增前端需要用到的 API endpoint

  • 增加購物車品項 /cart [POST]
  • 刪除購物車品項 /cart/delete [DELETE]
  • 移除所有購物車品項 /cart/delete/all [DELETE]
  • 取得所有商品品項 /products [GET]
  • 取得所有購物車品項 /cart [GET]

增加購物車品項

// 略
app.post('/cart', (req, res) => {
    fs.readFile(CART_DATA_FILE, (err, data) => {
      const cartProducts = JSON.parse(data);
      const newCartProduct = {
        id: req.body.id,
        title: req.body.title,
        description: req.body.description,
        price: req.body.price,
        image_tag: req.body.image_tag,
        quantity: 1
      };
      let cartProductExists = false;
      cartProducts.map((cartProduct) => {
        if (cartProduct.id === newCartProduct.id) {
          cartProduct.quantity++;
          cartProductExists = true;
        }
      });
      if (!cartProductExists) cartProducts.push(newCartProduct);
      fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
        res.setHeader('Cache-Control', 'no-cache');
        res.json(cartProducts);
      });
    });
  });

刪除購物車的品項

// 略
app.delete('/cart/delete', (req, res) => {
  fs.readFile(CART_DATA_FILE, (err, data) => {
    let cartProducts = JSON.parse(data);
    cartProducts.map((cartProduct) => {
      if (cartProduct.id === req.body.id && cartProduct.quantity > 1) {
        cartProduct.quantity--;
      } else if (cartProduct.id === req.body.id && cartProduct.quantity === 1) {
        const cartIndexToRemove = cartProducts.findIndex(cartProduct => cartProduct.id === req.body.id);
        cartProducts.splice(cartIndexToRemove, 1);
      }
    });
    fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
      res.setHeader('Cache-Control', 'no-cache');
      res.json(cartProducts);
    });
  });
});

刪除購物車全部品項

app.delete('/cart/delete/all', (req, res) => {
  fs.readFile(CART_DATA_FILE, () => {
    let emptyCart = [];
    fs.writeFile(CART_DATA_FILE, JSON.stringify(emptyCart, null, 4), () => {
      res.json(emptyCart);
    });
  });
});

取得全部商品品項

app.get('/products', (req, res) => {
  fs.readFile(PRODUCT_DATA_FILE, (err, data) => {
    res.setHeader('Cache-Control', 'no-cache');
    res.json(JSON.parse(data));
  });
});

取的購物車品項

app.get('/cart', (req, res) => {
  fs.readFile(CART_DATA_FILE, (err, data) => {
    res.setHeader('Cache-Control', 'no-cache');
    res.json(JSON.parse(data));
  });
});

接下來產生所使用的假資料 (mock data)

// server-cart-data.json
[
    {
        "id": 2,
        "title": "浴巾組阿",
        "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
        "price": 199,
        "image_tag": "xxxx.png",
        "quantity": 1
    },
    {
        "id": 3,
        "title": "室內拖鞋",
        "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
        "price": 59,
        "image_tag": "xxxx.png",
        "quantity": 1
    }
]
[
    {
      "id": 1,
      "title": "記憶枕頭阿",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
      "product_type": "寢具",
      "image_tag": "xxx.png",
      "created_at": 2020,
      "owner": "xxx",
      "owner_photo": "xx.jpg",
      "email": "xx@gmail.com",
      "price": 1000
    },
    {
      "id": 2,
      "title": "xxxx",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
      "product_type": "xxx",
      "image_tag": "xxx.png",
      "created_at": 2020,
      "owner": "xx",
      "owner_photo": "xxx.jpg",
      "email": "xxx@gmail.com",
      "price": 99
    }
  ]

啟動 server

$ node server

會看到終端機 show 出 Find the server at: http://localhost:3000/
代表伺服器成功啟動,再進到 Vue 專案中設定 proxy

// vuex-shopping-cart/vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000/',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

target 為API的網域路徑,是將請求託給 API Server 代理,告訴 Web Server 任何 /api 請求,代理到 http://localhost:3000/

未完待續....

每日一句:
報復性的放風,咖啡廳一位難求 /images/emoticon/emoticon02.gif


上一篇
中央狀態指揮中心- Vuex [續]
下一篇
[番外] 一步一步實現購物車功能 [續]
系列文
別再說我不會框架,網頁 Vue 起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言