参考 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": "[email protected]",
"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": "[email protected]",
"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/
未完待续....
每日一句:
报复性的放风,咖啡厅一位难求
<<: Day26:今天来聊一下使用资料连接器将资料连接到Azure Sentinel
>>: 第25车厢-让pdf档有翻页效果!pdf.js+turn.js应用篇
我在想,还剩下没几天,该怎麽把永丰的收款串到前端呢... 後端我也没写购物车,前端也只简单写个阳春的...
原本是计画要2020换工作,结果因为疫情的关系打算延後了一年在开始投履历跟面试~ 时间线 2021 ...
由於 YouTube 没有提供下载服务,人们如何将影片、音乐资源转换成 MP3 以便离线播放呢?接下...
上一篇在 TwMarketTradingInfoManager 完成了拿取大盘成交量的 API,接下...
今日题目 题目连结:747. Largest Number At Least Twice of Ot...