如何使用Vue3和Vuex构建购物车

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 开源计划 来接受捐赠。

介绍

Vue.js 是一个高性能的 progressive Javascript 框架。 它是 GitHub 上的一个流行框架,并拥有一个活跃且乐于助人的社区。

为了展示 Vue Web 框架的功能,本教程将引导您构建电子商务应用程序的购物车。 该应用程序将存储产品信息并保存客户想要购买的产品以供以后结帐。 为了存储信息,您将探索一个广泛使用的 Vue.js 状态管理库:Vuex。 这将允许购物车应用程序将数据保存到服务器。 您还将使用 Vuex 处理 异步 任务管理。

完成本教程后,您将拥有一个功能正常的购物车应用程序,如下所示:

先决条件

第 1 步 — 使用 Vue CLI 设置应用程序

从 4.5.0 版本开始,Vue CLI 现在提供了一个内置选项,可以在创建新项目时选择 Vue 3 预设。 最新版本的 Vue CLI 允许您开箱即用地使用 Vue 3,并将现有的 Vue 2 项目更新为 Vue 3。 在这一步中,您将使用 Vue CLI 创建项目,然后安装前端依赖项。

首先,通过从终端执行以下命令来安装最新版本的 Vue CLI:

npm install -g @vue/cli  

这将在您的系统上全局安装 Vue CLI。

注意: 在某些系统上,全局安装 npm 包会导致权限错误,从而中断安装。 由于避免将 sudonpm install 一起使用是一种安全最佳实践,因此您可以通过更改 npm 的默认目录来解决此问题。 如果遇到 EACCES 错误,请按照 npm 官方文档 中的 说明进行操作。


使用以下命令检查您的版本是否正确:

vue --version

您将获得如下输出:

Output@vue/cli 4.5.10

注意: 如果您已经全局安装了旧版本的Vue CLI,请在终端执行以下命令进行升级:

npm update -g @vue/cli

现在,您可以创建一个新项目:

vue create vuex-shopping-cart

这使用 Vue CLI 命令 vue create 来创建一个名为 vuex-shopping-cart 的项目。 有关 Vue CLI 的更多信息,请查看 如何使用 Vue CLI 生成 Vue.js 单页应用程序。

接下来,您将收到以下提示:

OutputVue CLI v4.5.10
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

从此列表中选择 Manually select features 选项。

接下来,您将遇到以下提示来自定义您的 Vue 应用程序:

Output...
 ◉ Choose Vue version
 ◯ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
❯◯ Unit Testing
 ◯ E2E Testing

从此列表中选择 Choose Vue versionRouterVuex。 这将允许您选择您的 Vue 版本并使用 Vuex 和 Vue Router

接下来,为您的 Vue 版本选择 3.x (Preview),对 history mode 回答否 (N),然后选择选项以进行配置 In dedicated config file。 最后,回答 N 以避免为将来的项目保存设置。

此时,Vue 将创建您的应用程序。

创建项目后,使用以下命令移入文件夹:

cd vuex-shopping-cart

首先,您将安装 Bulma,这是一个基于 Flexbox 的免费开源 CSS 框架。 通过运行以下命令将 Bulma 添加到您的项目中:

npm install bulma

要在您的项目中使用 Bulma CSS,请打开应用程序的入口点 main.js 文件:

nano src/main.js

然后添加以下突出显示的导入行:

vuex-shopping-cart/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 模块向您的服务器发出请求。 通过运行以下命令添加 Axios 模块:

npm install axios

现在,运行应用程序以确保它正常工作:

npm run serve

在您选择的浏览器中导航到 http://localhost:8080。 您将找到 Vue 应用欢迎页面:

确认 Vue 正在运行后,使用 CTRL+C 停止服务器。

在这一步中,您在计算机中全局安装了 Vue CLI,创建了一个 Vue 项目,安装了所需的 npm 包 Axios 和 Bulma,并将 Bulma 导入到 main.js 文件中的项目中。 接下来,您将设置一个后端 API 来为您的应用存储数据。

第 2 步 — 设置后端

在这一步中,您将创建一个单独的后端来处理您的 Vue 项目。 这将位于与前端 Vue 应用程序不同的项目文件夹中。

首先,移出 Vue 目录:

cd ..

创建一个名为 cart-backend 的单独目录:

mkdir cart-backend

拥有后端文件夹后,将其设为工作目录:

cd cart-backend

您将开始使用必要的文件初始化项目。 使用以下命令创建应用程序的文件结构:

touch server.js
touch server-cart-data.json
touch server-product-data.json

您在此处使用 touch 命令创建空文件。 server.js 文件将保存您的 Node.js 服务器,而 JSON 将保存商店产品和用户购物车的数据。

现在运行以下命令来创建一个 package.json 文件:

npm init

有关 npm 和 Node 的更多信息,请查看我们的 How To Code in Node.js 系列。

将这些后端依赖项安装到您的 Node 项目中:

npm install concurrently express body-parser

Express 是一个用于 Web 应用程序的 Node 框架,它将为处理 API 请求提供有用的抽象。 同时将用于同时运行Express后端服务器和Vue.js开发服务器。 最后,body-parser 是一个 Express 中间件,它将解析对您的 API 的请求。

接下来,在应用程序的根目录中打开一个 server.js 文件:

nano server.js

然后添加以下代码:

购物车后端/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')}/`);
});

此代码段首先将 Node 模块添加到您的后端,包括用于写入文件系统的 fs 模块和用于简化文件路径定义的 path 模块。 然后初始化 Express app 并将对 JSON 文件的引用保存为 PRODUCT_DATA_FILECART_DATA_FILE。 这些将用作数据存储库。 最后,您创建了一个 Express 服务器,设置了端口,创建了一个中间件来设置响应标头,并将服务器设置为侦听您的端口。 有关 Express 的更多信息,请参阅 官方 Express 文档

setHeader 方法设置 HTTP 响应的标头。 在这种情况下,您使用 Cache-Control 来指导应用程序的缓存。 有关这方面的更多信息,请查看关于 Cache-Control Mozilla 开发者网络文章。

接下来,您将创建一个 API 端点,您的前端将查询该端点以将商品添加到购物车。 为此,您将使用 app.post 来侦听 HTTP POST 请求。

将以下代码添加到 server.js 中最后一个 app.use() 中间件之后:

购物车后端/server.js

...
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.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.listen(app.get('port'), () => {
  console.log(`Find the server at: http://localhost:${app.get('port')}/`);
});

此代码从前端接收包含购物车项目的请求对象,并将它们存储在项目根目录的 server-cart-data.json 文件中。 这里的产品是 JavaScript objectsidtitledescriptionpriceimage_tag 和 [X110X ] 属性。 该代码还检查购物车是否已经存在,以确保对重复产品的请求只会增加 quantity

现在,添加代码以创建 API 端点以从购物车中删除商品。 这一次,您将使用 app.delete 来侦听 HTTP DELETE 请求。

将以下代码添加到 server.js 上一个端点之后:

购物车后端/server.js

...
      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.listen(app.get('port'), () => {
  console.log(`Find the server at: http://localhost:${app.get('port')}/`); // eslint-disable-line no-console
});

此代码接收包含要从购物车中删除的商品的请求对象,并通过其 id 检查该商品的 server-cart-data.json 文件。 如果存在且数量大于一,则扣除购物车中商品的数量。 否则,如果商品的数量小于 1,则会从购物车中删除,剩余的商品将存储在 server-cart-data.json 文件中。

为了给您的用户提供额外的功能,您现在可以创建一个 API 端点来从购物车中删除所有商品。 这还将侦听 DELETE 请求。

将以下突出显示的代码添加到上一个端点之后的 server.js

购物车后端/server.js

...
    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.listen(app.get('port'), () => {
  console.log(`Find the server at: http://localhost:${app.get('port')}/`); // eslint-disable-line no-console
});

此代码负责通过返回一个空的 array 从购物车中删除所有商品。

接下来,您将创建一个 API 端点以从产品存储中检索所有产品。 这将使用 app.get 来监听 GET 请求。

在上一个端点之后的 server.js 中添加以下代码:

购物车后端/server.js

...
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));
  });
});
...

此代码使用文件系统的原生 readFile 方法获取 server-product-data.json 文件中的所有数据并以 JSON 格式返回。

最后,您将创建一个 API 端点以从购物车存储中检索所有商品:

购物车后端/server.js

...
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));
  });
});
...

同样,此代码使用文件系统的原生 readFile 方法获取 server-cart-data.json 文件中的所有数据并以 JSON 格式返回。

保存并关闭 server.js 文件。

接下来,您将向 JSON 文件添加一些模拟数据以进行测试。

打开您之前创建的 server-cart-data.json 文件:

nano server-cart-data.json

添加以下产品对象数组:

购物车后端/服务器购物车数据.json

[
    {
        "id": 2,
        "title": "MIKANO Engine",
        "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
        "price": 650.9,
        "image_tag": "diesel-engine.png",
        "quantity": 1
    },
    {
        "id": 3,
        "title": "SEFANG Engine",
        "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
        "price": 619.9,
        "image_tag": "sefang-engine.png",
        "quantity": 1
    }
]

这显示了将在用户的购物车中启动的两个引擎。

保存并关闭文件。

现在打开 server-product-data.json 文件:

nano server-product-data.json

server-product-data.json 文件中添加以下数据:

购物车后端/服务器产品数据.json

[
    {
      "id": 1,
      "title": "CAT Engine",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
      "product_type": "power set/diesel engine",
      "image_tag": "CAT-engine.png",
      "created_at": 2020,
      "owner": "Colton",
      "owner_photo": "image-colton.jpg",
      "email": "colt@gmail.com",
      "price": 719.9
    },
    {
      "id": 2,
      "title": "MIKANO Engine",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
      "product_type": "power set/diesel engine",
      "image_tag": "diesel-engine.png",
      "created_at": 2020,
      "owner": "Colton",
      "owner_photo": "image-colton.jpg",
      "email": "colt@gmail.com",
      "price": 650.9
    },
    {
      "id": 3,
      "title": "SEFANG Engine",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
      "product_type": "power set/diesel engine",
      "image_tag": "sefang-engine.png",
      "created_at": 2017,
      "owner": "Anne",
      "owner_photo": "image-anne.jpg",
      "email": "anne@gmail.com",
      "price": 619.9
    },
    {
      "id": 4,
      "title": "CAT Engine",
      "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
      "product_type": "power set/diesel engine",
      "image_tag": "lawn-mower.png",
      "created_at": 2017,
      "owner": "Irene",
      "owner_photo": "image-irene.jpg",
      "email": "irene@gmail.com",
      "price": 319.9
    }
    
  ]

这将保存用户可以放入购物车的所有可能产品。

保存并关闭文件。

最后,执行这个命令来运行服务器:

node server

您将在终端上收到类似这样的信息:

OutputFind the server at: http://localhost:3000/

让此服务器在此窗口中运行。

最后,您将在 Vue 应用程序中设置代理服务器。 这将启用前端和后端之间的连接。

转到 Vue 应用程序的根目录:

cd ../vuex-shopping-cart

在终端中,运行以下命令来创建 Vue 配置文件:

nano vue.config.js

然后,添加以下代码:

vuex-shopping-cart/vue.config.js

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000/',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

这会将请求从您的前端发送到位于 http://localhost:3000/ 的后端服务器。 有关代理配置的更多信息,请查看 Vue devServer.proxy 文档

保存并关闭文件。

在这一步中,您编写了将处理购物车的 API 端点的服务器端代码。 您从创建文件结构开始,最后在 server.js 文件中添加必要的代码并在 JSON 文件中添加数据。 接下来,您将为前端设置状态存储。

第 3 步 — 使用 Vuex 设置状态管理

在 Vuex 中,store 是保存应用程序状态的地方。 应用程序状态只能通过在组件中调度操作来更新,然后在存储中触发突变。 Vuex store 由 state、mutations、actions 和 getter 组成。

在这一步中,您将构建每个部分,然后将所有内容耦合到一个 Vuex 存储中。

状态

现在,您将为应用程序创建一个存储状态的地方。

项目根目录 src 中的 store 文件夹是在项目设置时自动创建的。 在项目的 src 目录中找到 store 文件夹,然后创建一个名为 modules 的新文件夹:

mkdir src/store/modules

在此文件夹中,创建 productcart 文件夹:

mkdir src/store/modules/product
mkdir src/store/modules/cart

这些将保存您的产品库存和用户购物车的所有状态文件。 您将同时构建这两个文件,每个文件都在单独的终端中打开。 这样,您将能够并排比较您的突变、getter 和操作。

最后,在product文件夹中打开一个index.js文件:

nano src/store/modules/product/index.js

添加以下代码以创建包含您的 productItems 的状态对象:

vuex-shopping-cart/src/store/modules/product/index.js

import axios from 'axios';
const state = {
  productItems: [] 
}

保存文件并保持打开状态。

同样,在新终端中,将 index.js 文件添加到 cart 目录,内容如下:

nano src/store/modules/cart/index.js

然后为 cartItems 添加代码:

vuex-shopping-cart/src/store/modules/cart/index.js

import axios from 'axios';
const state = {
  cartItems: []
}

保存此文件,但保持打开状态。

在这些代码片段中,您导入了 Axios 模块并设置了状态。 state 是一个存储对象,它保存需要在组件之间共享的应用程序级数据。

现在您已经设置了状态,请转到突变。

突变

Mutations 是修改存储状态的方法。 它们通常由字符串类型和接受状态和有效负载作为参数的处理程序组成。

您现在将为您的应用程序创建所有突变。

product/index.js 文件的 state 部分之后添加以下代码:

vuex-shopping-cart/src/store/modules/product/index.js

...
const mutations = {
  UPDATE_PRODUCT_ITEMS (state, payload) {
    state.productItems = payload;
  }
}

这将创建一个 mutations 对象,该对象包含一个 UPDATE_PRODUCT_ITEMS 方法,该方法将 productItems 数组设置为 payload 值。

同样,在 cart/index.js 文件的状态部分之后添加以下代码:

vuex-shopping-cart/src/store/modules/cart/index.js

...
const mutations = {
  UPDATE_CART_ITEMS (state, payload) {
    state.cartItems = payload;
  }
}

这会为您用户的购物车创建一个类似的 UPDATE_CART_ITEMS。 请注意,这遵循 Flux 架构 风格,即引用大写字母的突变。

行动

Actions 是处理突变的方法,因此突变与应用程序代码的其余部分隔离。

product/index.js 中,创建一个包含应用程序所有操作的 actions 对象:

vuex-shopping-cart/src/store/modules/product/index.js

...
const actions = {
  getProductItems ({ commit }) {
    axios.get(`/api/products`).then((response) => {
      commit('UPDATE_PRODUCT_ITEMS', response.data)
    });
  }
}

这里的 getProductItems 方法使用您之前安装的 Axios 包向服务器发送异步 GET 请求。 当请求成功时,将调用 UPDATE_PRODUCT_ITEMS 突变,并将响应数据作为有效负载。

接下来,将以下 actions 对象添加到 cart/index.js

vuex-shopping-cart/src/store/modules/cart/index.js

...
const actions = {
  getCartItems ({ commit }) {
    axios.get('/api/cart').then((response) => {
      commit('UPDATE_CART_ITEMS', response.data)
    });
  },
  addCartItem ({ commit }, cartItem) {
    axios.post('/api/cart', cartItem).then((response) => {
      commit('UPDATE_CART_ITEMS', response.data)
    });
  },
  removeCartItem ({ commit }, cartItem) {
    axios.delete('/api/cart/delete', cartItem).then((response) => {
      commit('UPDATE_CART_ITEMS', response.data)
    });
  },
  removeAllCartItems ({ commit }) {
    axios.delete('/api/cart/delete/all').then((response) => {
      commit('UPDATE_CART_ITEMS', response.data)
    });
  }
}

在此文件中,您创建 getCartItems 方法,该方法向服务器发送异步 GET 请求。 当请求成功时,将调用 UPDATE_CART_ITEMS 突变,并将响应数据作为有效负载。 removeAllCartItems 方法也是如此,尽管它向服务器发出 DELETE 请求。 removeCartItemaddCartItem 方法接收 cartItem 对象作为发出 DELETEPOST 请求的参数。 成功请求后,将调用 UPDATE_CART_ITEMS 突变,并将响应数据作为有效负载。

您使用 ES6 destructuringcommit 方法与 Vuex context 对象解耦。 这类似于使用 context.commit

吸气剂

Getters 对应用程序商店来说就像计算属性对组件一样。 它们从涉及接收计算状态数据的存储状态方法返回计算信息。

接下来,创建一个 getters 对象以获取 product 模块的所有信息:

vuex-shopping-cart/src/store/modules/product/index.js

...
const getters = {
  productItems: state => state.productItems,
  productItemById: (state) => (id) => {
    return state.productItems.find(productItem => productItem.id === id)
  }
}

在这里,您创建了一个方法 productItems,它返回状态中的产品项目列表,然后是 productItemById,这是一个通过 id 返回单个产品的高阶函数。

接下来,在 cart/index.js 中创建一个 getters 对象:

vuex-shopping-cart/src/store/modules/cart/index.js

...
const getters = {
  cartItems: state => state.cartItems,
  cartTotal: state => {
    return state.cartItems.reduce((acc, cartItem) => {
      return (cartItem.quantity * cartItem.price) + acc;
    }, 0).toFixed(2);
  },
  cartQuantity: state => {
    return state.cartItems.reduce((acc, cartItem) => {
      return cartItem.quantity + acc;
    }, 0);
  }
}

在此代码段中,您创建了 cartItems 方法,它返回状态中的购物车项目列表,然后是 cartTotal,它返回可用于结帐的购物车项目总数的计算值. 最后,您创建了 cartQuantity 方法,它返回购物车中的商品数量。

导出模块

productcart 模块的最后部分将导出 statemutationsactionsgetters对象,以便应用程序的其他部分可以访问它们。

product/index.js 中,在文件末尾添加以下代码:

vuex-shopping-cart/src/store/modules/product/index.js

...
const productModule = {
  state,
  mutations,
  actions,
  getters
}

export default productModule;

这会将所有状态对象收集到 productModule 对象中,然后将其作为模块导出。

保存 product/index.js 并关闭文件。

接下来,在 cart/index.js 中添加类似的代码:

vuex-shopping-cart/src/store/modules/product/index.js

...
    const cartModule = {
  state,
  mutations,
  actions,
  getters
}
export default cartModule;

这会将模块导出为 cartModule

设置商店

设置好 state、mutations、action 和 getter 后,将 Vuex 集成到应用程序的最后一部分是创建 store。 在这里,您将利用 Vuex 模块将您的应用程序商店分成两个可管理的片段。

要创建您的商店,请打开 store 文件夹中的 index.js 文件:

nano src/store/index.js

添加以下突出显示的行:

vuex-shopping-cart/src/store/index.js

import { createStore } from 'vuex'
import product from'./modules/product';
import cart from './modules/cart';

export default createStore({
  modules: {
    product,
    cart
  }
})

保存文件,然后退出文本编辑器。

您现在已经创建了状态管理所需的方法并为您的购物车创建了商店。 接下来,您将创建用户界面 (UI) 组件来使用数据。

第 4 步 - 创建接口组件

现在您已经为您的购物车设置了商店,您可以继续为用户界面 (UI) 制作组件。 这将包括对路由器进行一些更改,并为您的导航栏以及产品和购物车的列表和项目视图制作前端组件。

首先,您将更新您的 vue-router 设置。 请记住,当您使用 Vue CLI 工具构建应用程序时,您选择了路由器选项,它允许 Vue 自动为您设置路由器。 现在您可以重新配置路由器以提供 Cart_List.vueProduct_List.vue 的路径,它们是您稍后将制作的 Vue 组件。

使用以下命令打开路由器文件:

nano vuex-shopping-cart/src/router/index.js

添加以下突出显示的行:

vuex-shopping-cart/src/router/index.js

import { createRouter, createWebHashHistory } from 'vue-router'
import CartList from '../components/cart/Cart_List.vue';
import ProductList from '../components/product/Product_List.vue';

const routes = [
  {
    path: '/inventory',
    component: ProductList
  },
  {
    path: '/cart',
    component: CartList
  },
  {
    path: '/',
    redirect: '/inventory'
  },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

这将为您的产品创建 /inventory 路线,并为您购物车中的商品创建 /cart 路线。 它还将您的根路径 / 重定向到产品视图。

添加此代码后,保存并关闭文件。

现在您可以设置您的 UI 组件目录。 在终端上运行此命令以移动到组件的目录:

cd src/components

运行此命令在组件目录下创建三个新的子文件夹:

mkdir core cart product

core 将包含应用程序的基本部分,例如导航栏。 cartproduct 将保存购物车的项目和列表视图以及总库存。

core 目录下,通过运行以下命令创建 Navbar.vue 文件:

touch core/Navbar.vue

cart目录下,创建文件Cart_List_Item.vueCart_List.vue

touch cart/Cart_List_Item.vue cart/Cart_List.vue

最后,在product目录下,创建这两个文件:

touch product/Product_List_Item.vue product/Product_List.vue

现在已经概述了文件结构,您可以继续创建前端应用程序的各个组件。

Navbar 组件

在导航栏中,购物车导航链接将显示您购物车中的商品数量。 您将使用 Vuex mapGetters 辅助方法直接将 store getter 与组件计算属性进行映射,从而允许您的应用程序将这些数据从 store 的 getter 获取到 Navbar 组件。

打开导航栏文件:

nano core/Navbar.vue

将代码替换为以下内容:

vuex-shopping-cart/src/components/core/Navbar.vue

<template>
    <nav class="navbar" role="navigation" aria-label="main navigation">
      <div class="navbar-brand">
        <a
          role="button"
          class="navbar-burger burger"
          aria-label="menu"
          aria-expanded="false"
          data-target="navbarBasicExample"
        >
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
        </a>
      </div>
      <div id="navbarBasicExample" class="navbar-menu">
        <div class="navbar-end">
          <div class="navbar-item">
            <div class="buttons">
              <router-link to="/inventory" class="button is-primary">
               <strong> Inventory</strong>
              </router-link>
              <router-link to="/cart"  class="button is-warning">   <p>
    Total cart items:
    <span> {{cartQuantity}}</span> </p>
              </router-link>
            </div>
          </div>
        </div>
      </div>
    </nav>
</template>
<script>
import {mapGetters} from "vuex"
export default {
    name: "Navbar",
    computed: {
    ...mapGetters([
      'cartQuantity'
    ])
  },
  created() {
    this.$store.dispatch("getCartItems");
  }
}
</script>

作为 Vue 组件,此文件以 template 元素开始,该元素包含组件的 HTML。 该片段包含多个 navbar 类,这些类使用 Bulma CSS 框架中的预制样式。 有关更多信息,请查看 Bulma 文档

这也使用 router-link 元素将应用程序连接到您的产品和购物车,并使用 cartQuantity 作为 计算属性 来动态跟踪您的商品数量大车。

JavaScript 保存在 script 元素中,该元素还处理状态管理和导出组件。 getCartItems 操作在创建导航栏组件时被调度,使用从服务器接收的响应数据中的所有购物车项目更新商店状态。 在此之后,存储 getter 重新计算它们的返回值,并且 cartQuantity 在模板中呈现。 在创建的生命周期钩子上不调度 getCartItems 动作,cartQuantity 的值将是 0,直到存储状态被修改。

保存并关闭文件。

Product_List 组件

该组件是 Product_List_Item 组件的父组件。 它将负责将产品项作为道具传递给 Product_List_Item(子)组件。

首先,打开文件:

nano product/Product_List.vue

更新 Product_List.vue 如下:

vuex-shopping-cart/src/components/product/Product_List.vue

<template>
  <div class="container is-fluid">
    <div class="tile is-ancestor">
      <div class="tile is-parent" v-for="productItem in productItems" :key="productItem.id">
      <ProductListItem :productItem="productItem"/>
      </div>
    </div>
  </div>
</template>
<script>
import { mapGetters } from 'vuex';
import Product_List_Item from './Product_List_Item'
export default {
  name: "ProductList",
  components: {
    ProductListItem:Product_List_Item
  },
  computed: {
    ...mapGetters([
      'productItems'
    ])
  },
  created() {
    this.$store.dispatch('getProductItems');
  }
};
</script>

与前面讨论的 Navbar 组件逻辑类似,这里的 Vuex mapGetters 辅助方法直接将存储 getter 与组件计算属性进行映射,以从存储中获取 productItems 数据。 getProductItems 操作在 ProductList 组件创建时被调度,使用从服务器接收到的响应数据中的所有产品项目更新商店状态。 在此之后,存储 getter 重新计算它们的返回值,并且 productItems 在模板中呈现。 如果没有在 created 生命周期钩子上调度 getProductItems 动作,则模板中不会显示任何产品项目,直到商店状态被修改。

Product_List_Item 组件

该组件将是 Product_List 组件的直接子组件。 它将从其父级接收 productItem 数据作为道具并将它们呈现在模板中。

打开Product_List_Item.vue

nano product/Product_List_Item.vue

然后添加以下代码:

vuex-shopping-cart/src/components/product/Product_List_Item.vue

<template>
    <div class="card">
      <div class="card-content">
        <div class="content">
          <h4>{{ productItem.title }}</h4>
          <a
            class="button is-rounded is-pulled-left"
            @click="addCartItem(productItem)"
          >
            <strong>Add to Cart</strong>
          </a>
          <br />
          <p class="mt-4">
            {{ productItem.description }}
          </p>
        </div>
        <div class="media">
          <div class="media-content">
            <p class="title is-6">{{ productItem.owner }}</p>
            <p class="subtitle is-7">{{ productItem.email }}</p>
          </div>
          <div class="media-right">
            <a class="button is-primary is-light">
              <strong>$ {{ productItem.price }}</strong>
            </a>
          </div>
        </div>
      </div>
    </div>
</template>
<script>
import {mapActions} from 'vuex'
export default {
  name: "ProductListItem",
  props: ["productItem"],
  methods: {
    ...mapActions(["addCartItem"]),
  },
};
</script>

除了前面组件中使用的 mapGetters 辅助函数外,Vuex 还为您提供了 mapActions 辅助函数,可以直接将组件方法映射到商店的动作。 在这种情况下,您使用 mapAction 辅助函数将组件方法映射到商店中的 addCartItem 操作。 现在您可以将商品添加到购物车。

保存并关闭文件。

Cart_List 组件

该组件负责显示添加到购物车的所有产品项目,以及从购物车中删除所有项目。

要创建这个组件,首先打开文件:

nano cart/Cart_List.vue

接下来,更新 Cart_List.vue 如下:

vuex-shopping-cart/src/components/cart/Cart_List.vue

<template>
  <div id="cart">
    <div class="cart--header has-text-centered">
      <i class="fa fa-2x fa-shopping-cart"></i>
    </div>
    <p v-if="!cartItems.length" class="cart-empty-text has-text-centered">
      Add some items to the cart!
    </p>
    <ul>
      <li class="cart-item" v-for="cartItem in cartItems" :key="cartItem.id">
          <CartListItem :cartItem="cartItem"/>
      </li>
      <div class="notification is-success">
        <button class="delete"></button>
        <p>
          Total Quantity:
          <span class="has-text-weight-bold">{{ cartQuantity }}</span>
        </p>
      </div>
      <br>
    </ul>
    <div class="buttons">
    <button :disabled="!cartItems.length" class="button is-info">
      Checkout (<span class="has-text-weight-bold">${{ cartTotal }}</span>)
    </button>
     
 <button class="button is-danger is-outlined" @click="removeAllCartItems">
    <span>Delete All items</span>
    <span class="icon is-small">
      <i class="fas fa-times"></i>
    </span>
  </button>
       </div>
  </div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import CartListItem from "./Cart_List_Item";
export default {
  name: "CartList",
  components: {
    CartListItem
  },
  computed: {
    ...mapGetters(["cartItems", "cartTotal", "cartQuantity"]),
  },
  created() {
    this.$store.dispatch("getCartItems");
  },
  methods: {
    ...mapActions(["removeAllCartItems"]),
  }
};
</script>

此代码使用模板中的 v-if 语句 如果购物车为空,则有条件地呈现消息。 否则,它会遍历购物车项目的存储并将它们呈现到页面。 您还加载了 cartItemscartTotalcartQuantity getter 来计算数据属性,并引入了 removeAllCartItems 动作。

保存并关闭文件。

Cart_List_Item 组件

该组件是 Cart_List 组件的直接子组件。 它从其父级接收 cartItem 数据作为道具并将它们呈现在模板中。 它还负责增加和减少购物车中物品的数量。

打开文件:

nano cart/Cart_List_Item.vue

更新 Cart_List_Item.vue 如下:

vuex-shopping-cart/src/components/cart/Cart_List_Item.vue

<template>
  <div class="box">
    <div class="cart-item__details">
      <p class="is-inline">{{cartItem.title}}</p>
      <div>
        <span class="cart-item--price has-text-info has-text-weight-bold">
          ${{cartItem.price}} X {{cartItem.quantity}}
        </span>
        
        <span>
          <i class="fa fa-arrow-circle-up cart-item__modify" @click="addCartItem(cartItem)"></i>
          <i class="fa fa-arrow-circle-down cart-item__modify" @click="removeCartItem(cartItem)"></i>
        </span>
      </div>
      
    </div>
  </div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
  name: 'CartListItem',
  props: ['cartItem'],
  methods: {
    ...mapActions([
      'addCartItem',
      'removeCartItem'
    ])
  }
}
</script>

在这里,您使用 mapAction 辅助函数将组件方法映射到商店中的 addCartItemremoveCartItem 操作。

保存并关闭文件。

最后,您将更新 App.vue 文件以将这些组件引入您的应用程序。 首先,移回项目的根文件夹:

cd ../..

现在打开文件:

nano src/App.vue

将内容替换为以下代码:

vuex-shopping-cart/src/App.vue

<template>
  <div>
    <Navbar/>
    <div class="container mt-6">
      <div class="columns">
        <div class="column is-12 column--align-center">
          <router-view></router-view>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import Navbar from './components/core/Navbar'
export default {
  name: 'App',
  components: {
    Navbar
  }
}
</script>
<style>
html,
body {
  height: 100%;
  background: #f2f6fa;
}
</style>

App.vue 是以 Vue 组件文件格式定义的应用程序的根。 进行更改后,保存并关闭文件。

在此步骤中,您通过为导航栏、产品库存和购物车创建组件来设置购物车应用程序的前端。 您还使用了在上一步中创建的存储操作和获取器。 接下来,您将启动并运行您的应用程序。

第 5 步 — 运行应用程序

现在您的应用程序已准备就绪,您可以启动开发服务器并试用最终产品。

在前端项目的根目录中运行以下命令:

npm run serve

这将启动一个开发服务器,允许您在 http://localhost:8080 上查看您的应用程序。 另外,请确保您的后端在单独的终端中运行; 您可以通过在 cart-backend 项目中运行以下命令来执行此操作:

node server

后端和前端运行后,在浏览器中导航到 http://localhost:8080。 您会发现正在运行的购物车应用程序:

结论

在本教程中,您使用 Vue.js 和 Vuex 构建了一个在线购物车应用程序进行数据管理。 这些技术可以重复使用以形成电子商务购物应用程序的基础。 如果您想了解有关 Vue.js 的更多信息,请查看我们的 Vue.js 主题页面