參考 How To Build a Shopping Cart with Vue 3 and Vuex 的手把手教學,一步一步實現購物車功能
$ npm install -g @vue/cli
$ vue create vuex-shopping-cart
建立一個名為 vuex-shopping-cart
的專案,選擇 Manually select features
,記得勾選 Router
和 Vuex
$ 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
編輯 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/
未完待續....
每日一句:
報復性的放風,咖啡廳一位難求