MonoRepo + Vue3 + Element plus + Node + express 全站架构实现商品管理系统

在这个前端开发的时代,竞争可谓是异常激烈,大家都在追求更高效的开发方式、更新的技术栈,甚至连头发都被"卷"得越来越少。听说如果你的头发还剩下几根,那说明你还不够聪明!今天就让我们一起来看看如何让你的头顶更加光亮聪明,尤其是通过MonoRepo + Vue3 + Element Plus + Node + Express全栈架构实现商品管理系统的方式。

来吧,今天主要分享的内容是MonoRepo + Vue3 + Element plus + Node + express 全栈架构实现商品管理系统。

看完本篇会有哪些收获呢

1. MonoRepo 管理项目的思想和运用。

MonoRepo(单一仓库)是一种将多个项目放在同一个代码仓库中的管理方式。这种方式的优点在于:

  • 版本控制方便:所有项目在同一个仓库中,便于管理和协作。
  • 依赖共享:第三方包只需安装一次,所有子项目均可使用,避免重复安装。
  • 代码共享:公共的工具函数、配置等可以在所有子项目中共享,减少代码冗余

2. Node.js 框架 express 的使用,接口编写,跨域处理

在本项目中,我们使用Node.js的Express框架来搭建后端API。通过Express,我们可以轻松地处理HTTP请求,编写接口,并进行跨域处理。

3. mysql2 的基本使用,

在后端,我们使用MySQL数据库来存储商品信息。通过mysql2库,我们可以轻松地进行数据库操作。

4. 弄清楚MVC 架构思想,控制器,路由model 的关系逻辑

MVC(模型-视图-控制器)是一种常见的软件架构模式。在本项目中,我们将业务逻辑分为以下几部分:

  • Model:负责与数据库交互,处理数据。
  • View:负责用户界面的展示,使用Vue.js构建。
  • Controller:处理请求,协调Model和View之间的关系。

5. Vue3 组合式api的使用

在前端部分,我们使用Vue3的组合式API来构建用户界面。组合式API提供了更灵活的逻辑组织方式,使得代码更加清晰。

6. axios 的使用方式

Axios是一个基于Promise的HTTP客户端,用于发送请求。在本项目中,我们使用Axios与后端API进行交互。以下是一个发送GET请求的示例:

是不是已经迫不及待了呢,先来看看展示。

开发成果展示

商品后台管理

商品列表展示

添加商品

更新商品

商品前台展示

总的代码结构

  • shop-products 总的项目名称
    • apps 存放子项目

      • admin 商品后台管理,可以添加商品,更新商品,删除商品
      • api 用于编写接口的子项目
      • web 前台展示商品的子项目
    • package 存放项目公用的包,比如说公共的工具函数,公共的配置等

      • config 公共配置
      • utils 存放工具函数
    • pmpm-workspace.yaml 描述monoprepo 要管理的包 先来看看 pmpm-workspace.yaml, 其实就写了两个东西 packages:

    • apps/*

    • packages/*

这就是典型的MonoRepo 架构思想。

为什么要用MonoRepo 呢!

  1. 方便版本控制,因为都在一个仓库中
  2. 第三方包,可以共享,只需要安装一次即可,在要在总的这个项目中, 使用 pnpm add -w name(包名) 安装的,在所有子应用中都可以正常使用,不需要多次安装,比如在这次的商品管理项目中,element-plus, axios, vite, vue-router,dayjs 都会采用这种安装方式。
  3. 自己开发的工具函数,公共配置,公共组件等也可以在所有子应用中进行共享。在这次的商品管理系统中,我们就会共享公共的配置,公共的工具函数。只要在各个子应用中执行pnpm add name(packages下的项目名称) --workspace 即可在子应用中正常使用,package.json 文件中name 字段定义的名称。 比如这次的商品管理系统中package.json 文件是这样的
json 复制代码
{
  "name": "@shop-product/utils",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

我们只需要在apps 下的子应用中,执行pnpm add @shop-product/utils --workspace ,在子应用中 导入就可以正常使用了

  1. 在根目录下的package.json 中配置项目的启动命令,可以只启动一个,也可以同时启动所有的子应用。比如在 这次的商品管理系统中。
json 复制代码
{
  "name": "@shop-product/root",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ],
  "scripts": {
    "dev":"pnpm -r run dev",
    "dev:api": "pnpm run -F @shop-product/api dev",
    "dev:admin": "pnpm run -F @shop-product/admin dev",
    "dev:web": "pnpm run -F @shop-product/web dev"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "sass": "^1.83.4",
    "sass-loader": "^16.0.4",
    "vite": "^6.0.7"
  },
  "dependencies": {
    "axios": "^1.7.9",
    "dayjs": "^1.11.13",
    "element-plus": "^2.9.3",
    "vue-router": "^4.5.0"
  }
}
  • dev 命令用于启动所有子应用。
  • dev:api 启动api子项目。
  • dev:admin 启动admin 子项目。
  • dev:web 启动web子项目。

只需要执行pnpm run 加上前面的即可运行你想要运行的项目。

在上面我们介绍了,项目总体的架构,下面我们来介绍下单个项目的结构。先来看看packages/下面的公共包

packages 下的公共包

结构如下

config/index.ts 放所有子应用公用的配置

js 复制代码
const BASE_API_URL = 'http://localhost:9999'
export {
  BASE_API_URL
}

utils/http.ts

index.ts 封装axios 请求,包括超时配置,错误处理

js 复制代码
import { axiosGet, axiosPost } from "./http"

export {
  axiosGet,
  axiosPost
}
js 复制代码
import axios from "axios";
import { BASE_API_URL } from '../config'
import { ElMessage } from "element-plus";


axios.defaults.baseURL = BASE_API_URL;
axios.defaults.timeout = 15000

function axiosGet(url: string, data: any = {}) {
  return axios.get(url, {
    params: data
  }).then(res => {
    return res.data.data
  }).catch(() => {
    ElMessage({
      message: '请求异常',
      type: 'error',
    })
  })
}

function axiosPost (url: string, data: any) {
  return axios.post(url, {
    ...data
  }).then(res => {
    return res.data
  }).catch(() => {
    ElMessage({
      message: '请求异常',
      type: 'error',
    })
  })
}

export {
  axiosGet,
  axiosPost,
}

api 项目结构和代码功能实现

可以看到这个目录结构其实就是一个典型的MVC 架构。

  • api 项目名称
    • config 放置一些项目的配置信息,这次我们就会放置跨域的配置,数据库配置信息
    • controlles 处理前端请求过来的相关逻辑
    • modles 操作数据库相关逻辑
    • routes 路由配置,也就是前端请求过来的api地址相关
    • index.js项目的入库文件,启动项目的关键

来看下具体代码

modles

js 复制代码
const db = require("../config/database");
const dayjs = require("dayjs");
class Product {
  constructor(pro) {
    this.pro = pro;
  }

  static fetchAllProduct() {
    return db.execute("SELECT * FROM products ORDER BY updateTime DESC");
  }

  saveProduct() {
    const { name, price, description, imgUrl } = this.pro;
    const date = dayjs().format("YYYY-MM-DD HH:mm:ss");
    const sql =
      "INSERT INTO products (name, price, description, imgUrl, createTime, updateTime) VALUES (?, ? ,?, ?, ?, ?)";
    return db.execute(sql, [
      name,
      parseFloat(price),
      description,
      imgUrl,
      date,
      date,
    ]);
  }

  updateProduct() {
    const { name, price, description, imgUrl, id } = this.pro;
    const sql = `UPDATE products SET name = ?, price = ?, description = ?, imgUrl = ?, updateTime = ? WHERE id = ?`;
    return db.execute(sql, [
      name,
      price,
      description,
      imgUrl,
      dayjs().format("YYYY-MM-DD HH:mm:ss"),
      id,
    ]);
  }

  static deleteProductItem(id) {
    const sql = "DELETE FROM products WHERE id = ?";
    return db.execute(sql, [id]);
  }
  static deleteMultProduct(ids) {
    const sql = `DELETE FROM products WHERE id IN (?)`;
    return db.execute(sql, [ids.join(",")]);
  }
}

module.exports = Product;

controllers

js 复制代码
const Product = require("../modles/product");

exports.getAllProducts = (req, res) => {
  Product.fetchAllProduct()
    .then(([data, tableinfo]) => {
      res.send({
        code: 200,
        msg: "success",
        data: data,
      });
    })
    .catch((err) => {
      console.log("456");
      res.send({
        code: 0,
        msg: "出错了",
      });
    });
};

exports.findByIdProduct = (req, res) => {
  Product.findByIdProduct(req.params.id)
    .then((detail) => {
      res.send({
        code: 200,
        msg: "success",
        data: detail,
      });
    })
    .catch((err) => {
      res.send({
        code: 0,
        msg: "出错了",
        data: detail,
      });
    });
};

exports.findByNameProduct = (req, res) => {
  Product.findByNameProduct(req.params.name)
    .then((detail) => {
      res.send({
        code: 200,
        msg: "success",
        data: detail,
      });
    })
    .catch((err) => {
      res.send({
        code: 0,
        msg: "出错了",
        data: detail,
      });
    });
};
exports.saveProduct = (req, res) => {
  console.log("save", req.body);
  const product = new Product(req.body);
  product.saveProduct().then((data) => {
    res.send({
      code: 200,
      meg: "成功添加",
      data: data,
    });
  });
};
exports.updateProduct = (req, res) => {
  const product = new Product(req.body);
  product.updateProduct().then((data) => {
    res.send({
      code: 200,
      meg: "商品更新成功",
      data: data,
    });
  });
};
exports.deleteProductItem = (req, res) => {
  Product.deleteProductItem(req.body.id).then((data) => {
    res.send({
      code: 200,
      meg: "成功删除一个商品",
      data: data,
    });
  });
};
exports.deleteMultProduct = (req, res) => {
  Product.deleteMultProduct(req.body.ids).then((data) => {
    res.send({
      code: 200,
      meg: `成功删除${req.body.ids.length}个商品`,
      data: data,
    });
  });
};

routes

js 复制代码
const express = require("express");
const {
  getAllProducts,
  saveProduct,
  updateProduct,
  deleteProductItem,
  deleteMultProduct,
} = require("../controllers/products");

const router = express.Router();

router.get("/get_all_products", getAllProducts);
router.post("/save_product", saveProduct);
router.post("/update_product", updateProduct);
router.post("/delete_product_item", deleteProductItem);
router.post("/delete_mult_product", deleteMultProduct);

module.exports = router;

index.js

js 复制代码
const express = require("express");

const app = express();

const cors = require("cors");

// 自定义 CORS 选项
const corsOptions = require("./config/cors");

const router = require("./routes/products");

app.use(express({ extended: false }));
app.use(cors(corsOptions));
app.use(router);
app.listen(9999, () => {
  console.log("http://localhost:9999");
});

config/database.js

js 复制代码
const mysql = require("mysql2");

const pool = mysql.createPool({
  user: "root",
  password: "123456",
  host: "localhost",
  port: "3306",
  database: "node_shangcheng",
  dateStrings: true,
  timezone: "Z",
});

module.exports = pool.promise();

config/cors.js

js 复制代码
const corsOptions = {
  origin: "*", // 仅允许来自此源的请求, 生产环境换成自己的域名
  methods: ["GET", "POST", "PUT", "DELETE"], // 允许的请求方法
  allowedHeaders: ["Content-Type", "Authorization"], // 允许的请求头
  credentials: true, // 是否允许发送 Cookie 和其他凭据
  optionsSuccessStatus: 204, // 对于旧浏览器的兼容性
};

module.exports = corsOptions;

以上是api 项目情况,看到这里相信你已经可以做出一个node.js 的后端项目了,接着我们来看看后台管理系统

admin 项目结构和代码实现

目录结构如下,这是vite 创建出来的项目,相信大家都已经很熟悉了,就不做过多的介绍。

src/pages/Product.vue

vue 复制代码
<template>
  <div>
  <div class="tools">
    <el-button type="primary" @click="addProduct">添加商品</el-button>
    <el-button type="primary" @click="openMutilDialog">批量删除</el-button>
  </div>
    <el-table :data="list" border style="width: 100%" align="center" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" />
      <el-table-column prop="name" label="商品名称" width="180" align="center" />
      <el-table-column prop="price" label="商品价格" width="180" align="center" />
      <el-table-column prop="description" label="商品描述" align="center" />
      <el-table-column prop="imgUrl" label="封面图" align="center">
        <template #default="scope">
          <el-image
          style="width: 100px; height: 100px"
          :src="scope.row.imgUrl"
          fit="fill"></el-image>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间" align="center" />
      <el-table-column prop="updateTime" label="更新时间" align="center" />
      <el-table-column label="操作" align="center">
        <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row)">
          编辑
        </el-button>
        <el-button
          size="small"
          type="danger"
          @click="handleDelete(scope.row)"
        >
          删除
        </el-button>
      </template>
      </el-table-column>
    </el-table>
    <ProductDialog :showSaveDialog="showSaveDialog" :info="info" @saveProduct="saveProduct"  @closeSaveDialog="closeSaveDialog" />
  </div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ElTable, ElTableColumn,ElImage, ElMessageBox, ElButton, ElMessage } from 'element-plus';

import { axiosGet, axiosPost } from '@shop-product/utils'
import type { IproductInfo } from '../typings/product';
import ProductDialog from '../components/ProductDialog/index.vue'

const list = ref<IproductInfo[]>([])

const info = ref<IproductInfo>({
  name: '',
  description: '',
  price: 0,
  imgUrl: ''
})

/**
 * 点击编辑时弹出编辑的表单弹框
 * @param currinfo 
 */
const handleEdit = (currinfo: IproductInfo) => {
  info.value = currinfo,
  showSaveDialog.value = true
}
const addProduct = () => {
  info.value = {
    name: '',
    description: '',
    price: 0,
    imgUrl: ''
  }
  showSaveDialog.value = true
}

/**
 * 删除单个商品确认弹框
 * @param info 
 */
const handleDelete = (info: IproductInfo) => {
  ElMessageBox.confirm(
    `确定要删除《${info.name}》吗`,
    '删除确认',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'info',
    }
  ).then(() => {
    deleteProductItem(info)
  })
}

/**
 * 删除单个商品 
 * @param 
 */
const deleteProductItem = (info: IproductInfo) => {
  axiosPost('/delete_product_item', {
    id: info.id,
  }).then(res => {
    if (res.code === 200) {
      ElMessage({
        type: 'success',
        message: `成功删除《${info.name}》商品`
      })
      getAllProduct()
    }
  })
}

// 保存选中的商品
let selectedProducts:IproductInfo[] = []
const handleSelectionChange = (data: IproductInfo[]) => {
  console.log('选中事件触发', data)
  selectedProducts = data
}

/**
 * 批量删除的确认弹框
 */
const openMutilDialog = () => {
  const names: string[] = selectedProducts.map(item => `《${item.name }》`)
  const ids: number[] = selectedProducts.map((item) => item.id!)
  ElMessageBox.confirm(
    `确定要删除${names},总共${names.length}个商品吗`,
    '删除确认',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'info',
    }
  ).then(() => {
    deleteMultProduct(ids, names)
  })
}

/**
 * 调用接口进行批量删除
 * @param ids id 列表,传给后端的参数
 * @param names 商品名称提示列表,删除成功后进行提示
 */
const deleteMultProduct = (ids: number[], names: string[]) => {  
  axiosPost('/delete_mult_product', {
    ids
  }).then(res => {
    if (res.code === 200) {
      ElMessage({
        type: 'success',
        message: `成功删除了${names}, 总共${names.length}个商品`
      })
      getAllProduct()
    }
  })
}


// 显示弹框的标识
const showSaveDialog = ref(false)
/**
 * 关闭编辑或者新增商品的弹框
 */
const closeSaveDialog = () => {
  showSaveDialog.value = false
}

// 添加成功后关闭弹框并重新查询
const saveProduct = () => {
  showSaveDialog.value = false
  getAllProduct()
}

/**
 * 获取商品列表
 */
async function getAllProduct() {
  list.value = await axiosGet('/get_all_products')
}


onMounted(() => {
  getAllProduct()
})
</script>
<style lang="scss" scoped>
.tools{
  margin-bottom: 20px;
}
</style>

components/ProductDialog/index.vue

vue 复制代码
<template>
  <el-dialog
    v-model="dialogVisible"
    :title="title"
    width="500"
    :before-close="handleClose"
  >
  <el-form :model="form">
      <el-form-item label="商品名称">
        <el-input v-model="form.name" autocomplete="off" />
      </el-form-item>
      <el-form-item label="商品价格">
        <el-input v-model="form.price" autocomplete="off" />
      </el-form-item>
      <el-form-item label="商品描述">
        <el-input v-model="form.description" type="textarea" />
      </el-form-item>
      <el-form-item label="商品图片">
        <el-input v-model="form.imgUrl" autocomplete="off" />
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="confirmSave">
          保存
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
import { axiosPost } from '@shop-product/utils';
import { ElForm, ElFormItem, ElInput, ElButton, ElDialog, ElMessage } from 'element-plus';
import { ref, watch } from 'vue';
import type { IproductInfo } from '../../typings/product';

const {
  showSaveDialog,
  info
} = defineProps({
  showSaveDialog: {
    type: Boolean,
    default: false
  },
  info: {
    type: Object,
    default() {
      return {
        name: '',
        description: '',
        price: '',
        imgUrl: ''
      }
    }
  }
})

const title = ref('添加商品')
const form = ref<IproductInfo>({
  name: '',
  description: '',
  price: 0,
  imgUrl: ''
})

const dialogVisible = ref(false)
watch(() => showSaveDialog, (newVal, oldVal) => {
  dialogVisible.value = newVal
  if (info.id) {
    title.value = '更新商品'
  } else {
    title.value = '添加商品'
  }
  form.value = info as IproductInfo
}, {
  immediate: true
})

const emit = defineEmits(['closeSaveDialog' , 'saveProduct'])
const confirmSave = () => {
  let api = '/save_product'
  let msg = '添加成功'
  if (info.id) {
    api = '/update_product',
    msg = '商品更新成功'
  }
  axiosPost(api, {...form.value}).then(res => {
    ElMessage({
      type: 'success',
      message: msg
    })
    form.value = {
      name: '',
      description: '',
      price: 0,
      imgUrl: ''
    }
    emit('saveProduct')
  })
}

const handleClose = () => {
  emit('closeSaveDialog', false)
}
</script>

说完了后台管理,现在来看看,前台展示

web 项目结构和代码实现

主要就一个页面的代码

pages/Product.vue

vue 复制代码
<template>
  <div>
    <ul class="list">
      <li v-for="item in list" :key="item.id">
        <img :src="item.imgUrl" />
        <h3>{{ item.name }}</h3>
        <p class="price">{{ item.price }}</p>
        <p class="desc">{{ item.description }}</p>
      </li>
    </ul>
  </div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import type { IproductInfo } from '../typings/product';
import { axiosGet } from '@shop-product/utils';

const list = ref<IproductInfo[]>([])
/**
 * 获取商品列表
 */
 async function getAllProduct() {
  list.value = await axiosGet('/get_all_products')
}


onMounted(() => {
  getAllProduct()
})

</script>
<style lang="scss" scoped>
.list {
  display: flex;
  flex-wrap: wrap;
  padding: 0;

  li {
    width: calc(20% - 10px);
    margin: 5px;
    list-style: none;
    border: 1px solid #ff6200;
    padding: 10px;
    border-radius: 10px;
    box-sizing: border-box;

    img {
      width: 100%;
      border-radius: 10px;
    }
    .price {
      color: #ff6200;
      font-size: 20px;
    }
    .desc {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2; /* 定义显示的行数 */
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
}
</style>

看到这里,恭喜你,你成功卷入了全栈工程师

哈哈哈哈哈, 来总结下吧。

总结

通过这次的项目实践,我们不仅掌握了MonoRepo的管理思想,还学会了如何使用Vue3、Element Plus、Node和Express构建一个完整的商品管理系统。在这个过程中,大家的头发也许会变得更加光亮聪明,因为你们在不断学习和进步!

相关推荐
烂蜻蜓2 小时前
前端已死?什么是前端
开发语言·前端·javascript·vue.js·uni-app
谢尔登3 小时前
Vue 和 React 的异同点
前端·vue.js·react.js
祈澈菇凉7 小时前
Webpack的基本功能有哪些
前端·javascript·vue.js
小纯洁w7 小时前
Webpack 的 require.context 和 Vite 的 import.meta.glob 的详细介绍和使用
前端·webpack·node.js
熬夜不洗澡9 小时前
Node.js中不支持require和import两种导入模块的混用
node.js
bubusa~>_<9 小时前
解决npm install 出现error,比如:ERR_SSL_CIPHER_OPERATION_FAILED
前端·npm·node.js
yanglamei19629 小时前
基于Python+Django+Vue的旅游景区推荐系统系统设计与实现源代码+数据库+使用说明
vue.js·python·django
流烟默9 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序
菲力蒲LY10 小时前
vue 手写分页
前端·javascript·vue.js
天下皆白_唯我独黑10 小时前
npm 安装扩展遇到证书失效解决方案
前端·npm·node.js