[番外] 一步一步实现购物车功能 [序]

前言

参考 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": "[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/

未完待续....

每日一句:
报复性的放风,咖啡厅一位难求 /images/emoticon/emoticon02.gif


<<:  Day26:今天来聊一下使用资料连接器将资料连接到Azure Sentinel

>>:  第25车厢-让pdf档有翻页效果!pdf.js+turn.js应用篇

[Day 28] - React 前端串後端 - Donate!

我在想,还剩下没几天,该怎麽把永丰的收款串到前端呢... 後端我也没写购物车,前端也只简单写个阳春的...

2021 — 找工作 (上)

原本是计画要2020换工作,结果因为疫情的关系打算延後了一年在开始投履历跟面试~ 时间线 2021 ...

YouTube 转换为 MP3

由於 YouTube 没有提供下载服务,人们如何将影片、音乐资源转换成 MP3 以便离线播放呢?接下...

D22 - 用 Swift 和公开资讯,打造投资理财的 Apps { 台股成交量实作.2 }

上一篇在 TwMarketTradingInfoManager 完成了拿取大盘成交量的 API,接下...

Day 3:747. Largest Number At Least Twice of Others

今日题目 题目连结:747. Largest Number At Least Twice of Ot...