在这个前端开发的时代,竞争可谓是异常激烈,大家都在追求更高效的开发方式、更新的技术栈,甚至连头发都被"卷"得越来越少。听说如果你的头发还剩下几根,那说明你还不够聪明!今天就让我们一起来看看如何让你的头顶更加光亮聪明,尤其是通过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 呢!
- 方便版本控制,因为都在一个仓库中
- 第三方包,可以共享,只需要安装一次即可,在要在总的这个项目中, 使用
pnpm add -w name(包名)
安装的,在所有子应用中都可以正常使用,不需要多次安装,比如在这次的商品管理项目中,element-plus, axios, vite, vue-router,dayjs
都会采用这种安装方式。 - 自己开发的工具函数,公共配置,公共组件等也可以在所有子应用中进行共享。在这次的商品管理系统中,我们就会共享公共的配置,公共的工具函数。只要在各个子应用中执行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
,在子应用中 导入就可以正常使用了
- 在根目录下的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构建一个完整的商品管理系统。在这个过程中,大家的头发也许会变得更加光亮聪明,因为你们在不断学习和进步!