ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Node.js+Vue打造个人物品管理系统:前后端分离实战

用Node.js+Vue打造个人物品管理系统:前后端分离实战 你有没有过这种经历周末想用那台微单翻箱倒柜半天找不到备用电池书架上的某本书突然要翻死活想不起来借给了谁上个月买的螺丝钉、扎带、充电线散落在三个抽屉和两个收纳箱里。我反正是受够了这种“找东西半小时、用东西五分钟”的日子于是用 Node.js 加 Vue 框架花了一个周末给自己做了一套个人物品管理系统。这个系统既能当家庭物品登记簿也可以改造成小型工作室、仓库的库存管理工具核心就解决三件事东西放在哪里、数量还有多少、状态是借出还是在库。项目难度不高但链路非常完整从环境配置到接口开发再到页面交互都要走一遍特别适合刚学完 JavaScript 基础、想练一个真实前后端项目的同学参考。1. 项目方案设计为什么是 Node.js 加 Vue1.1 技术选型背后的实际考量先聊选型。为什么不是 Spring Boot 加 React也不是 Python Django 配模板说实话没有绝对优劣但对“个人物品管理系统”这个场景Node.js 加 Vue 有几个非常实际的理由。第一是语言统一。前后端全都用 JavaScript写完接口写页面不用切换上下文。我见过不少朋友先学 Java 后端再啃 JavaScript 前端光是在脑子里维护两套语言规范就很累而全栈 JS 的心智负担小很多特别适合一个人开发的小项目。第二是生态成熟。npm 上有 Express、better-sqlite3、multer 这些现成的库几行代码就能搞定接口、数据库和图片上传。数据库选 SQLite 而不是 MySQL原因也很简单个人项目没有并发压力一个文件就是整个数据库备份直接复制文件走人零配置、零维护。等以后真需要多人同时用了再迁到 MySQL 也不迟逻辑层用参数化查询写的话迁移成本并不高。第三是部署省事。个人项目不像公司项目有专门的运维环境一份 Node 进程加上前端打包出来的静态文件扔到一台小主机上就能跑。Vue 这边我选 Vite 作为构建工具冷启动快、热更新流畅写代码的体验比老一代 Webpack 舒服太多了。1.2 功能模块拆解控制边界很重要个人物品管理系统听起来挺大但真要动手做之前我建议先划个边界。刚开始如果什么都想做既做扫码又做报表大概率一个周末交付不了。我这次刻意控制了功能范围只保留使用频率最高的几个模块。物品台账记录名称、分类、存放位置、数量、状态、备注和图片。分类与位置两个独立的筛选维度方便后续统计“电子设备在哪里”“书柜里有什么”。关键词检索按名称或备注模糊搜索这是日常用最多的功能。状态管理区分在库、借出、维修等状态物品借给谁了一目了然。统计卡片显示物品总数、分类数、在库数量和借出数量打开页面心里就有数。不做库存流水不做权限系统不做审批流。个人用这些花哨的功能反而碍事。等基础版本跑通了再按需往上加这是做小项目最舒服的节奏。1.3 数据库表设计一张表就够个人物品管理在数据层面并不复杂一张表就能撑起所有核心功能。我用 SQLite 建表时是这样的CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT DEFAULT 未分类, location TEXT DEFAULT , quantity INTEGER DEFAULT 1, status TEXT DEFAULT 在库, image TEXT DEFAULT , remark TEXT DEFAULT , created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) );字段含义很直白name 是物品名称必填category 和 location 分别对应分类和存放位置quantity 记录数量默认是 1status 标记状态image 存图片访问路径remark 写备注。时间字段我用 TEXT 类型存本地时间SQLite 的 datetime(now, localtime) 会直接生成可读性很强的字符串个人项目里比时间戳更直观排查数据时不用在脑子里换算时区。这里有个小经验字段宁可预留宽一点也不要一开始定太死。比如 location 我直接用 TEXT 而不是外键关联位置表这样用户想填“客厅书架第三层”还是“卧室床头柜抽屉”都可以自由发挥。等数据量大了再考虑规范化拆分初期灵活更重要。1.4 工程目录结构前后端分离但心中有数项目采用前后端分离的结构开发时两个服务独立运行最终部署时可以合并。目录划分如下item-manager/ ├─ server/ # Node.js 后端 │ ├─ server.js # 入口文件Express 应用 │ ├─ db.js # SQLite 连接与建表逻辑 │ ├─ routes/ # 路由文件 │ │ ├─ items.js # 物品 CRUD 路由 │ │ └─ upload.js # 图片上传路由 │ ├─ uploads/ # 上传的图片存放目录 │ └─ package.json ├─ web/ # Vue 前端 │ ├─ src/ │ │ ├─ api/ # axios 封装 │ │ ├─ router/ # vue-router 配置 │ │ ├─ views/ # 页面组件 │ │ ├─ components/ # 公共组件 │ │ ├─ App.vue │ │ └─ main.js │ ├─ index.html │ └─ package.json └─ README.md后端把路由拆成 items 和 upload 两个文件前端把 api 请求单独封装一层都是为了让项目结构一眼就能看懂。我见过很多初学者把一百行接口逻辑全塞进 server.js前端页面里到处写 axios一报错就要翻半天结构化一点能省掉大量排查时间。2. 环境准备与工程初始化从零到跑通2.1 Node.js 安装与版本管理建议开篇先解决环境问题。个人项目建议直接上 LTS 版本别追新。我习惯用 nvm-windows 管理 Node 版本因为不同项目依赖对 Node 版本要求不一样用 nvm 随时切换非常方便。nvm install 18.20.4 nvm use 18.20.4 node -v npm -v装完记得看一下 node -v 和 npm -v 是否正常输出。很多依赖装不上、启动报语法错误的问题根子都在 Node 版本不对。比如 Vite 5 要求 Node 18 以上如果你机器上还是 Node 16跑 npm create vite 大概率直接报错。遇到这种情况先查版本比 debug 代码快得多。npm 在国内下载速度慢的话可以顺手配一下镜像源但注意是改 registry 而不是装什么奇怪的工具。npm config set registry https://registry.npmmirror.com这个镜像源对大文件包支持得很好实测下来能省不少时间。如果你在公司内网有私有 npm 源用自己的源也行总之别让网络问题卡在第一步。2.2 高频报错npm 脚本无法加载怎么破.NET 环境下 Windows 用户经常会遇到一个红色报错大意是npm.ps1 无法加载因为在此系统上禁止运行脚本。这个我之前也踩过原因很简单PowerShell 默认的执行策略是 Restricted不允许运行 .ps1 脚本而 npm 会调用 npm.ps1自然就被拦下来了。解法有两种按场景选第一种直接用 cmd 或 Git Bash 跑命令完全绕过 PowerShell。对于只是偶尔跑一下 npm 的人来说这最省事没有任何副作用。第二种以管理员身份打开 PowerShell执行一次Set-ExecutionPolicy RemoteSigned执行后本机下载的脚本可以运行但需要签名的远程脚本仍然会被拦截安全性比完全放开高很多。我不建议直接设成 Unrestricted为了跑 npm 把整个 PowerShell 安全策略关掉属实有点得不偿失。如果你是 Windows 7 或老旧环境连 cmd 都提示找不到 node那大概率是安装时没把 Node.js 加进 PATH重新安装时勾选“Add to PATH”选项即可。这个问题现在比较少见但偶尔还能在帮朋友排查时碰到。2.3 用 Vite 初始化前端项目前端部分我是用 Vite 命令行脚手架创建的比手动配置 Webpack 轻松太多。npm create vitelatest web -- --template vue cd web npm install创建过程中会让你选择模板选 vue 即可。如果需要 TypeScript 就选 vue-ts不过这个项目用 JavaScript 就够跑了。基础依赖装完再补上路由、状态管理、UI 组件库和 HTTP 请求库npm install vue-router4 pinia element-plus axiosElement Plus 是 Vue 3 生态里很成熟的组件库表格、表单、弹窗、上传组件都是现成的个人管理系统这种管理后台风格页面用起来非常顺手。pinia 负责状态管理这个项目其实用不上太多全局状态但装上有利于后续扩展。axios 是请求库后面封装 API 全靠它。装完依赖跑一下 npm run dev浏览器打开提示的本地地址能出现 Vite 默认页面就说明前端工程没问题。2.4 初始化后端工程后端我建在 server 目录下初始化命令很常规mkdir server cd server npm init -y npm install express cors better-sqlite3 multer这四个库各司其职expressWeb 框架负责路由和中间件。cors解决开发时前后端不同源的跨域请求问题。better-sqlite3SQLite 驱动特点是同步 API个人项目用起来非常干脆。multer处理文件上传的中间件。装完在 package.json 里加一个启动脚本scripts: { start: node server.js }然后创建 server.js先写一个最简单的接口测试链路const express require(express); const cors require(cors); const app express(); app.use(cors()); app.use(express.json()); app.get(/api/health, (req, res) { res.json({ code: 0, message: ok }); }); app.listen(3000, () { console.log(server running at http://localhost:3000); });跑 npm start 后访问 http://localhost:3000/api/health能看到 JSON 返回就代表后端环境全部打通。到这里前后端两个工程都跑起来了接下来开始填业务逻辑。3. 后端 API 设计与核心实现3.1 数据库初始化建好地基再盖楼后端首先要保证数据库文件存在、表结构正确。我在 db.js 里封装了一个连接模块const Database require(better-sqlite3); const path require(path); const db new Database(path.join(__dirname, inventory.db)); db.exec( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT DEFAULT 未分类, location TEXT DEFAULT , quantity INTEGER DEFAULT 1, status TEXT DEFAULT 在库, image TEXT DEFAULT , remark TEXT DEFAULT , created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) ) ); module.exports db;better-sqlite3 是同步 API说实话第一次看到它的时候我还挺怀疑的都用 Node 了居然不用异步但用下来发现对于个人项目这种低并发的场景同步代码简单可控不会出现回调嵌套地狱排查问题也更直接。真到需要异步的场景换用其他驱动也不难。如果你希望程序启动时自动建好数据库文件直接在 server.js 里 require(./db) 就行这个模块加载的时候就会完成连接和建表动作非常省心。3.2 物品 CRUD 接口设计接口层面的 JSON 结构我统一用{ code: 0, data: ... }包裹方便前端统一处理。接口清单如下方法路径功能GET/api/items查询列表支持关键词、分类、位置、分页GET/api/items/:id查询单个物品详情POST/api/items新增物品PUT/api/items/:id更新物品DELETE/api/items/:id删除物品POST/api/upload上传图片列表接口是核心我把它放在 routes/items.js 里实现。查询条件需要组合处理支持名称模糊搜索、分类精确匹配、位置模糊匹配还要分页const express require(express); const router express.Router(); const db require(../db); router.get(/, (req, res) { const { keyword , category , location , page 1, pageSize 10 } req.query; const conditions []; const params {}; if (keyword) { conditions.push((name LIKE keyword OR remark LIKE keyword)); params.keyword %${keyword}%; } if (category) { conditions.push(category category); params.category category; } if (location) { conditions.push(location LIKE location); params.location %${location}%; } const where conditions.length ? WHERE ${conditions.join( AND )} : ; const total db.prepare(SELECT COUNT(*) AS count FROM items${where}).get(params).count; const offset (Number(page) - 1) * Number(pageSize); const limit Number(pageSize); const list db.prepare(SELECT * FROM items${where} ORDER BY id DESC LIMIT ${limit} OFFSET ${offset}).all(params); res.json({ code: 0, data: { total, list } }); });注意这里用参数化查询而不是字符串拼接category、keyword 这些用户输入全部走绑定参数能有效避免 SQL 注入。LIMIT 和 OFFSET 我做了 Number 转换后直接拼接因为这两个值已经强转成数字不存在注入风险用起来也省心。新增和更新的写法也类似router.post(/, (req, res) { const { name, category 未分类, location , quantity 1, status 在库, image , remark } req.body; if (!name) { return res.status(400).json({ code: 1, message: 物品名称不能为空 }); } const result db.prepare( INSERT INTO items (name, category, location, quantity, status, image, remark) VALUES (name, category, location, quantity, status, image, remark) ).run({ name, category, location, quantity, status, image, remark }); res.json({ code: 0, data: { id: result.lastInsertRowid } }); }); router.put(/:id, (req, res) { const id Number(req.params.id); const { name, category, location, quantity, status, image, remark } req.body; db.prepare( UPDATE items SET name name, category category, location location, quantity quantity, status status, image image, remark remark, updated_at datetime(now, localtime) WHERE id id ).run({ ...req.body, id }); res.json({ code: 0, message: 更新成功 }); });新增接口里必填校验必须要做名称空的物品存进去毫无意义后面前端虽然也会校验但后端校验是最后一道防线不能省。3.3 图片上传与静态资源托管物品带图片对使用体验提升很大找东西的时候一眼能看到外观。我用 multer 实现上传存储策略是磁盘存储const multer require(multer); const path require(path); const fs require(fs); const uploadDir path.join(__dirname, ../uploads); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const storage multer.diskStorage({ destination: uploadDir, filename: (req, file, cb) { const unique Date.now() - Math.round(Math.random() * 1e9); cb(null, unique path.extname(file.originalname)); } }); const upload multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) { if (/^image\//.test(file.mimetype)) { cb(null, true); } else { cb(new Error(只支持图片文件)); } } }); router.post(/upload, upload.single(file), (req, res) { if (!req.file) { return res.status(400).json({ code: 1, message: 没有收到文件 }); } res.json({ code: 0, data: { url: /uploads/ req.file.filename } }); });文件名的生成我用了时间戳加随机数避免重名互相覆盖也避免中文文件名在部分浏览器或服务器上出现编码问题。这是我在实际开发中踩过的坑直接用原始文件名保存结果 Linux 服务器上和浏览器里的编码不一致图片加载不出来。静态资源托管放在 server.js 里注册app.use(/uploads, express.static(path.join(__dirname, uploads)));这样浏览器访问http://localhost:3000/uploads/xxx.jpg就能直接看到图片前端拿到上传接口返回的路径后拼接地址即可。这里要注意如果你改了 uploads 目录位置或文件名规则记得检查静态托管路径是否同步更新否则前端会一直 404。3.4 跨域处理与局域网访问开发时前端跑在 5173 端口后端在 3000 端口跨域问题必须处理。最简单的方式就是后端启用 cors 中间件app.use(cors());不过更推荐的方式是在 Vite 里配置代理让前端把接口请求转发给后端这样浏览器看到的还是同源请求后面部署的时候也少一层麻烦。在 vite.config.js 里加server: { proxy: { /api: http://localhost:3000 } }想让同一局域网内的手机也能访问系统后端监听地址要改成 0.0.0.0app.listen(3000, 0.0.0.0, () { console.log(server running at http://0.0.0.0:3000); });改完之后手机用电脑的局域网 IP 加 3000 端口就能访问后端前端页面同理。我在家里就是电脑当“服务器”手机扫码录入物品信息用起来很有小产品的感觉。4. 前端页面与交互实现4.1 路由与项目骨架前端先配好路由页面不多的情况下保持简单即可。在 router/index.js 里import { createRouter, createWebHistory } from vue-router; import ItemList from ../views/ItemList.vue; const routes [ { path: /, redirect: /items }, { path: /items, component: ItemList } ]; const router createRouter({ history: createWebHistory(), routes }); export default router;如果你打算把前端打包后挂到后端某个子路径下部署createWebHashHistory 会更省心不用让服务器配置路由回退。我这个项目放在根路径部署createWebHistory 没问题。线上刷新 404 大多是 history 模式没配服务端回退导致的碰到这种情况要么配置服务器把未匹配路径指到 index.html要么直接用 hash 模式二选一。main.js 里依次注册路由、Pinia 和 Element Plusimport { createApp } from vue; import App from ./App.vue; import router from ./router; import { createPinia } from pinia; import ElementPlus from element-plus; import element-plus/dist/index.css; const app createApp(App); app.use(router); app.use(createPinia()); app.use(ElementPlus); app.mount(#app);4.2 物品列表页搜索、表格、分页三大块列表页是用户最常看到的页面我把它拆成三块搜索条件区、数据表格区、分页区。页面加载时先拉一次数据const loading ref(false); const list ref([]); const total ref(0); const query reactive({ keyword: , category: , location: , page: 1, pageSize: 10 }); async function fetchData() { loading.value true; try { const res await getItems(query); list.value res.data.list; total.value res.data.total; } finally { loading.value false; } }搜索表单用 el-form 布局关键词输入框、分类选择器、位置输入框横向排列提供“搜索”和“重置”两个按钮。分类下拉的数据我用一个接口从数据库里取SELECT DISTINCT category FROM items这样加了新分类后下拉框会自动更新不用手动维护前端配置。这个细节虽然小但能省掉很多重复维护的功夫。表格用 el-table列设计为图片缩略图、名称、分类、位置、数量、状态、备注、更新时间、操作。状态这一列我用 el-tag 渲染在库显示绿色借出显示橙色一眼就能扫出来。图片列用 el-image 加一个迷你缩略图el-table-column label图片 width80 template #default{ row } el-image v-ifrow.image :srcrow.image fitcover stylewidth: 40px; height: 40px; border-radius: 4px; / span v-else-/span /template /el-table-column分页组件配置 total、current-page 和 page-size切换页码时修改 query.page 后重新 fetchData。这里有个使用体验的细节当用户在第 5 页搜索新关键词时页码应该重置回 1不然很容易出现“明明有结果却停在空白页”的困惑。我就是在搜索函数里先把 query.page 重置为 1再做请求。4.3 新增与编辑弹窗表单校验和图片上传新增和编辑可以共用一个 el-dialog 弹窗通过一个 editingId 变量区分模式。表单字段和数据库字段一一对应名称、分类、存放位置、数量、状态、图片、备注。校验规则我用了 el-form 的 rulesconst rules { name: [{ required: true, message: 请输入物品名称, trigger: blur }], quantity: [{ required: true, message: 请输入数量 }] };数量字段还想限制最少 1我在提交前做了个判断小于 1 就弹提示并中断提交。前端校验是体验的一部分后端校验才是安全底线两边都要写。图片上传用的 el-upload关键配置是把 action 指到后端上传接口el-upload :actionuploadUrl :on-successhandleUploadSuccess :show-file-listfalse acceptimage/* el-button上传图片/el-button /el-uploaduploadUrl 我放在环境变量里开发时用http://localhost:3000/api/upload部署后改成实际地址。on-success 回调里会拿到后端返回的 JSON取出 data.url 赋给表单的 image 字段。这里有个容易踩的坑el-upload 的 on-success 拿到的参数里还有一层封装别直接用 response.data.url要看清楚响应结构再取值我第一次写就取错了层级图片一直回显不了。弹窗提交时区分流程编辑模式调 PUT 接口新增模式调 POST 接口成功后关闭弹窗、重置表单、刷新列表。关闭弹窗时记得调用 resetFields 清理上一次的残留数据不然下次打开弹窗时旧内容还在。4.4 统计卡片让数据说话列表页顶部我放了一排统计卡片分别是总物品数、分类数、在库数量、借出数量。这些数据不是从列表接口里的分页结果算的而是单独调统计接口因为这个数据需要全量统计分页数据算不出来。后端我加了一个统计路由router.get(/stats, (req, res) { const total db.prepare(SELECT COUNT(*) AS count FROM items).get().count; const totalQty db.prepare(SELECT COALESCE(SUM(quantity), 0) AS total FROM items).get().total; const statusStats db.prepare(SELECT status, COUNT(*) AS count FROM items GROUP BY status).all(); const categoryCount db.prepare(SELECT COUNT(DISTINCT category) AS count FROM items).get().count; const statusMap {}; statusStats.forEach(item { statusMap[item.status] item.count; }); res.json({ code: 0, data: { total, totalQty, categoryCount, onHand: statusMap[在库] || 0, lentOut: statusMap[借出] || 0 } }); });这个接口的位置应该放在/:id路由之前因为/stats如果写在/:id后面会被当成 id 为 stats 的详情查询处理。这种路由顺序问题在 Express 里很常见我一开始就栽过一次后来养成了“静态路径放前面、动态参数路径放后面”的习惯。前端渲染卡片用 el-card 加几个数字和标签数据用 ref 存起来页面加载时和列表数据一起请求。这个统计模块虽然简单但极大提升了使用的愉悦感每次打开系统看到总数一目了然就觉得这项目做得值。4.5 API 请求封装统一入口更好维护前端所有接口调用我统一放在 src/api/index.js 里封装import axios from axios; const http axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api, timeout: 10000 }); http.interceptors.response.use( (response) { if (response.data.code ! 0) { return Promise.reject(new Error(response.data.message || 请求失败)); } return response.data; }, (error) Promise.reject(error) ); export const getItems (params) http.get(/items, { params }); export const getItemStats () http.get(/items/stats); export const createItem (data) http.post(/items, data); export const updateItem (id, data) http.put(/items/${id}, data); export const deleteItem (id) http.delete(/items/${id}); export const uploadImage (formData) http.post(/upload, formData);baseURL 这里用/api配合 Vite 代理开发时自动转发到 3000 端口。如果你想直接用后端地址也可以改成 http://localhost:3000/api但要记得同步改跨域配置。接口统一封装的价值在于以后后端接口地址变了或者要统一加 token、改错误提示只动一个文件就行不用满页面搜索 axios。5. 常见问题排查实录与避坑清单5.1 Windows 环境高频报错把整个项目中遇到过的典型问题整理成下表你可以直接对照排查现象原因解决办法npm.ps1 无法加载禁止运行脚本PowerShell 执行策略限制用 cmd 或 Git Bash 执行或在管理员 PowerShell 执行Set-ExecutionPolicy RemoteSignednpm install 报 ERESOLVE 错误依赖版本冲突、缓存残留删除 node_modules 和 package-lock.json重新npm install启动项目提示 EADDRINUSE端口被占用netstat -ano | findstr :3000查 PIDtaskkill /PID xxx /F杀进程前端请求后端跨域前后端不同源Vite 配置 proxy或后端启用 cors刷新页面 404history 路由未配置回退改用 hash 模式或让服务器把所有路由指向 index.html图片上传后无法显示静态托管路径不对或文件名编码问题检查app.use(/uploads, express.static(...))确认文件真实存在于 uploads 目录数据库文件被锁定多个 Node 进程同时操作 SQLite杀掉多余进程确认只有一个实例运行npm.ps1 报错我已经在前面详细说过这里不再重复。EADDRINUSE 出现的频率也很高尤其是你反复重启后端某一次 CtrlC 没彻底关干净进程时端口就被“幽灵进程”占住了。按表格里的命令查 PID 再杀掉一般都能解决。5.2 排查思路不要拿着放大镜看代码碰到项目半天跑不起来我建议按这个顺序排查先看终端报错再确认端口再测接口最后才去看数据库。终端报错是最直接的线索比如“Cannot find module express”说明你的 npm install 没装成功或者安装目录和当前项目对不上比如“relation items does not exist”说明建表语句没执行成功检查一下数据库连接模块有没有被正确 require。端口问题用 netstat 一查便知如果端口被占用一切启动报错都正常。接口测不通用浏览器直接访问后端地址看有没有 JSON 返回。如果浏览器直接访问接口是好的但前端页面拿不到数据那问题大概率在代理配置或 baseURL 上。数据库这一层除非 SQL 写错或表结构不对否则很少出现“有接口但无数据”的情况因为前面几层已经拦截了大多数问题。一个非常实用的小建议开发阶段让后端每次启动时打印一条“数据库连接成功物品总数: xxx”之类的日志能让你快速判断数据库到底有没有被正确初始化。我有一次改了数据库文件路径启动时没有任何报错但列表永远为空排查了很久才发现连的是另一个路径下的空库。5.3 数据备份与安全处理SQLite 的备份太方便了直接复制文件就行。我写了一个简单的备份脚本每天定时把 inventory.db 复制到 backup 目录文件名带上日期const fs require(fs); const path require(path); const src path.join(__dirname, inventory.db); const dir path.join(__dirname, backup); const dest path.join(dir, inventory-${new Date().toISOString().slice(0, 10)}.db); if (!fs.existsSync(dir)) fs.mkdirSync(dir); fs.copyFileSync(src, dest); console.log(backup success:, dest);个人项目数据量不大备份文件也不会占多少空间但真到误删数据的时候就知道这脚本有多值钱。我建议大家在测试“删除物品”功能之前先手动复制一份数据库文件别等手滑了再后悔。这一点我用自己的亲身教训换来的删测试数据时手一抖把整个表清空了幸好有备份不然得重新录入两个月的物品信息。另外提醒一下上传图片目录的管理。multer 上传的图片没有自动清理机制物品删除后图片文件还留在 uploads 目录里。项目初期影响不大但时间长了会积累很多无用文件。可以定期手动清理或者写删除逻辑时同步把图片文件删掉。删除图片文件的操作要谨慎建议先软删除数据库记录确认无误后再清理物理文件避免误删。写在最后一点个人经验做完这个小项目我最大的体会是技术栈不在于多能把一条链路完整走通就非常有价值。开发过程中最顺手的一件事是我先把后端接口文档整理成表格再动手写前端页面前后端协作的思路一下子清晰了很多。接口地址、请求参数、返回结构提前定好写页面时照着填就行不会出现“接口突然多一个字段前端措手不及”的情况。后续我还想给这个系统加两个功能一个是给每件物品生成二维码贴到物品或收纳箱上手机扫一下就能看存放位置和状态另一个是导出 Excel 盘点单方便定期清理闲置物品。如果你也想改造这个项目我建议从搜索和状态管理两个点开始这两个是日常使用频率最高的功能把它们做顺手了整个系统的体验就成功了一半。
返回列表