vue3 + tsrpc +mongodb 实现后台管理系统

news/2024/7/10 1:06:49 标签: mongodb, vue, node.js

前言

之前上线了一个vue后台管理系统,有小伙伴问我有没有后端代码,咱只是个小前端,这就有点为难我了。不过不能辜负小伙伴的信任,nodejs也可以啊,废话不多说,开搞!后端采用 TSRPC 框架实现 API 接口,前端采用 vue-manage-system 后台管理系统框架,数据库采用 mongodb。TSRPC 是专为 TypeScript 设计的 RPC 框架,经千万级用户验证。适用于 HTTP API、WebSocket 实时应用、NodeJS 微服务等场景。有兴趣深入了解可以参考 TSRPC官方文档。

创建项目

用 TSRPC 脚手架快速创建一个项目,会生成 backend 和 frontend 两个文件夹,把 vue-manage-system 前端代码替换到 frontend 中,安装相关依赖,就完成一个基本的前后端完整项目了。

使用 mongodb,在backend/src下创建目录和文件 mongodb/index.ts

import { Db, MongoClient } from "mongodb";

export class Global {
    static db: Db;
    static async initDb() {
        const uri = 'mongodb://127.0.0.1:27017/test?authSource=admin';
        const client = await new MongoClient(uri).connect();
        this.db = client.db();
    }
}

在 src/index.ts 中初始化 mongodb 连接

import { Global } from './mongodb/index';

async function init() {
    // ...
    await Global.initDb();
};

vue-manage-system 是基于vue3实现的一个后台管理系统解决方案,代码简单,上手容易,已经在多个项目中应用。下载代码覆盖到 frontend 文件夹下,保留 src/client.ts 文件,这是 tsrpc 框架提供给客户端调用后端接口的方法。重装依赖,即可运行起来。
接下来实现一个用户管理的前后端功能。

后端接口

在 backend/shared/protocols 下新建一个 users 文件夹,用于定义用户管理的相关接口。在该目录下,新建 db_User.ts 文件,用于定义用户集合的字段类型,先按照vue-manage-system前端框架中已有的表格字段随便定义下吧。

import { ObjectId } from 'mongodb';

export interface db_User {
    _id: ObjectId;
    name: string;	// 用户名
    pwd: string;    // 密码
    thumb?: string;  // 头像
    money: number;  // 账户余额
    state: number;  // 账户状态
    address: string;    // 地址
    date: Date; // 注册日期
}

一个用户拥有以上的字段,接下来实现用户管理的增删查改操作。在users目录下分别创建 PtlAdd.ts、PtlDel.ts、PtlGet.ts、PtlUpdate.ts文件,TSRPC 完全通过文件名和类型名来识别协议,务必要严格按照 TSRPC 规定的名称前缀来命名,文件名为:Ptl{接口名}.ts,在 src/api/users 目录下,也会生成对应的 Apixxx.ts 文件,就是对应的接口 users/Add、users/Del、users/Get、users/Update。

新增

// PtlAdd.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";
import { db_User } from "./db_User";

export interface ReqAdd extends BaseRequest {
    query: Omit<db_User, '_id'>		// 除了_id自动生成,db_User其它属性都作为入参
}

export interface ResAdd extends BaseResponse {
    newID: string;		// 请求成功时返回_id
}

TSRPC 有统一的 错误处理 规范,这里不需要考虑成功、失败和错误的情况,不用定义code、data、message等字段,TSRPC 会返回以下格式

{
	isSucc: true,
	data: {
		newID: 'xxx'
	}
}

在 src/api/users/ApiAdd.ts 中,实现接口的主要逻辑,把数据插入数据库集合中。

import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqAdd, ResAdd } from "../../shared/protocols/users/PtlAdd";

export default async function (call: ApiCall<ReqAdd, ResAdd>) {
	// 这里就省略了各种判断
    const ret = await Global.db.collection('User').insertOne(call.req.query);
    return call.succ({ newID: ret.insertedId.toString() })
}

同理,把另外三个接口也加上

删除

// PtlDel.ts
import { ObjectId } from "mongodb";
import { BaseRequest, BaseResponse, BaseConf } from "../base";

export interface ReqDel extends BaseRequest {
    _id: ObjectId
}

export interface ResDel extends BaseResponse {
    matchNum: number;
}

// ApiDel.ts
import { ApiCall } from "tsrpc";
import { Global } from "../../mongodb";
import { ReqDel, ResDel } from "../../shared/protocols/users/PtlDel";

export default async function (call: ApiCall<ReqDel, ResDel>) {
    const ret = await Global.db.collection('User').deleteOne({ _id: call.req._id });
    return call.succ({ matchNum: ret.deletedCount })
}

查询

// PtlGet.ts
import { db_User } from './db_User';
import { BaseRequest, BaseResponse, BaseConf } from "../base";

export interface ReqGet extends BaseRequest {
    query: {
        pageIndex: number;
        pageSize: number;
        name?: string;
    };
}

export interface ResGet extends BaseResponse {
    data: db_User[],
    pageTotal: number
}

// ApiGet.ts
import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqGet, ResGet } from "../../shared/protocols/users/PtlGet";

export default async function (call: ApiCall<ReqGet, ResGet>) {
    const { pageIndex, pageSize, name } = call.req.query;
    const filter: any = {}
    if (name) {
        filter.filter = new RegExp(name!)
    }
    const ret = await Global.db.collection('User').aggregate([
        {
            $match: filter
        },
        {
            $facet: {
                total: [{ $count: 'total' }],
                data: [{ $sort: { _id: -1 } }, { $skip: (pageIndex - 1) * pageSize }, { $limit: pageSize }],
            },
        },
    ]).toArray()
    return call.succ({
        data: ret[0].data,
        pageTotal: ret[0].total[0]?.total || 0
    })
}

修改

// PtlUpdate.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";
import { db_User } from "./db_User";

export interface ReqUpdate extends BaseRequest {
    updateObj: Pick<db_User, '_id'> & Partial<Pick<db_User, 'name' | 'money' | 'address' | 'thumb'>>;
}

export interface ResUpdate extends BaseResponse {
    updatedNum: number;
}

// ApiUpdate.ts
import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqUpdate, ResUpdate } from "../../shared/protocols/users/PtlUpdate";

export default async function (call: ApiCall<ReqUpdate, ResUpdate>) {
    let { _id, ...reset } = call.req.updateObj;

    let op = await Global.db.collection('User').updateOne(
        {
            _id: _id,
        },
        {
            $set: reset,
        }
    );

    call.succ({
        updatedNum: op.matchedCount,
    });
}

后端的增删查改接口已经完成,接下来在前端中调用接口。

前端调用接口

在 frontend/src/client.ts 中,TSRPC 提供了 client.callApi 来调用 API 接口,在 table.vue 中我们来调用查询接口并加载到表格中。

import { client } from '../client';
const query = reactive({
	name: '',
	pageIndex: 1,
	pageSize: 10
});
const tableData = ref<TableItem[]>([]);
const pageTotal = ref(0);
// 获取表格数据
const getData = async () => {
	const ret = await client.callApi('users/Get', {
		query: query
	});
	if (ret.isSucc) {
		tableData.value = ret.res.data;
		pageTotal.value = ret.res.pageTotal;
	}
};
getData();

删除操作

const handleDelete = async (id: string) => {
	const ret = await client.callApi('users/Del', { _id });
	if (ret.isSucc) {
		ElMessage.success('删除成功');
	}
};

接口调用比较简单,新增和修改这里就不多描述了,有需要可以看代码。在用户字段中,有个头像,需要后端提供上传图片的接口,在实际业务中,大多数文件上传都会上传到cdn服务器上,不过这里没钱买cdn存储,就只能直接上传到服务器本地。

上传文件

先实现后端上传文件的接口,在 backend/shared/protocols 下新建一个 upload 文件夹,然后在 upload 里创建 PtlUpload.ts 文件

// PtlUpload.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";

export interface ReqUpload extends BaseRequest {
    fileName: string;
    fileData: Uint8Array;
}

export interface ResUpload extends BaseResponse {
    url: string;
}

这里用到了 Uint8Array 类型,它用于表示8位无符号整数的值的数组。Uint8Array主要提供字节级别的处理能力,如文件读写、二进制数据处理等。

import { ApiCall } from "tsrpc";
import { ReqUpload, ResUpload } from "../../shared/protocols/upload/PtlUpload";
import fs from 'fs/promises';

export default async function (call: ApiCall<ReqUpload, ResUpload>) {
    await fs.access('uploads').catch(async () => {
        await fs.mkdir('uploads')
    })
    await fs.writeFile('uploads/' + call.req.fileName, call.req.fileData);

    call.succ({
        url: call.req.fileName,
    });
}

把上传的文件存储到 uploads 目录下,如果该目录不存在,则先创建。如果想要比较细的话,可以多创建出一个日期的目录,按天存储。

注意:这里文件名是由用户传过来的,有可能出现重名的,按上面的逻辑会覆盖到之前的文件,所以这里可以改成文件名由后端自己生成。

在前端结合 element-plus 的上传组件调用api上传

<el-upload class="avatar-uploader" action="#" :show-file-list="false" :http-request="localUpload">
	<img v-if="form.thumb" :src="UPLOADURL + form.thumb" class="avatar" />
	<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
const localUpload = async (params: UploadRequestOptions) => {
	const ab = await params.file.arrayBuffer();
	var array = new Uint8Array(ab);
	const res = await client.callApi('upload/Upload', {
		fileName: Date.now() + '__' + params.file.name,
		fileData: array
	});
	if (res.isSucc) {
		form.value.thumb = res.res.url;
	} else {
		ElMessage.error(res.err.message);
	}
};

可是在上传后会发现,上传接口成功了,服务器的图片文件也存在,但是图片地址加载失败。原来是 TSRPC 默认创建的项目中没有直接支持静态文件服务,需要我们通过中间件简单处理下即可

静态文件服务

创建 getStaticFile.ts 文件,在中间件中自定义 HTTP 响应,对 Get 类型的请求,找到服务器上对应的文件并返回

import { HttpConnection, HttpServer } from 'tsrpc';
import fs from 'fs/promises';
import * as path from 'path';

export function getStaticFile(server: HttpServer) {
    server.flows.preRecvDataFlow.push(async (v) => {
        let conn = v.conn as HttpConnection;
        if (conn.httpReq.method === 'GET') {
            // 静态文件服务
            if (conn.httpReq.url) {
                // 检测文件是否存在
                let resFilePath = path.join('./', decodeURI(conn.httpReq.url));
                let isExisted = await fs
                    .access(resFilePath)
                    .then(() => true)
                    .catch(() => false);
                if (isExisted) {
                    // 返回文件内容
                    let content = await fs.readFile(resFilePath);
                    conn.httpRes.end(content);
                    return undefined;
                }
            }
            // 默认 GET 响应
            conn.httpRes.end('Not Found');
            return undefined;
        }
        return v;
    });
}

在 backend/src/index.ts 中使用,让每个网络请求都经过这个工作流

import { HttpServer } from "tsrpc";
import { serviceProto } from "./shared/protocols/serviceProto";
import { getStaticFile } from './models/getStaticFile'
const server = new HttpServer(serviceProto, {
    port: 3000,
    json: true
});
getStaticFile(server);

于是图片在前端就可以正常加载出来了。

总结

作为一个小前端,也能做一个完整前后端功能的后台管理系统,再也不用可怜兮兮的等后端接口了,自己一把梭哈,挺适合发展自己的副业余爱好。上面只是个基础的功能,还有许多功能需要慢慢完善,有兴趣可以看代码:tsrpc-manage-system


http://www.niftyadmin.cn/n/5328805.html

相关文章

基于Java SSM框架实现学生综合考评管理系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现学生学生综合考评管理系统演示 摘要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 学生综合考评管理系统&#xff0c;主要的模块包括查看&#xff1b;管理员&#xff1b;个…

PB获取随机字符串

// // 函数: randstr() //-------------------------------------------------------------------- // 描述:获取随机字符串 //-------------------------------------------------------------------- // 参数: // value string as_str 一个随机因子字符串 // value intege…

武汉灰京文化:抓住用户心理,游戏推广不可或缺的前提

在当今激烈竞争的游戏市场中&#xff0c;了解目标用户成为游戏推广的不可或缺的前提。不同类型的游戏适合不同的用户群体&#xff0c;因此通过深入研究用户画像&#xff0c;准确定位目标用户群体&#xff0c;成为游戏成功推广的关键一环。游戏推广不仅仅是让更多的人知道游戏的…

Proxy的使用方法和13种拦截操作

前言 proxy是ES6新推出的方法,功能很强大。属于元编程,也就是修改js本身的一些东西。可以对数组,对象,函数等引用类型的对象进行一些复杂的操作。 其中,大部分人应该最熟悉的莫过于vue3中使用proxy替换了defineProperty,而且还实现了本身defineProperty不能实现的一些东西。 …

超形象图解Python NumPy

超形象图解Python NumPy&#xff01;

Puppeteer让你网页操作更简单(1)屏幕截图

网页自动化设计爬虫工具 中就使用了Puppeteer进行对网页自动化处理&#xff0c;今天就来看看它是什么东西&#xff01; 我们将学习什么? 在本教程中,您将学习如何使用JavaScript自动化和抓取 web。 为此,我们将使用Puppeteer。 Puppeteer是一个Node库API,允许我们控制无头Ch…

仅用三张图片实现任意场景三维重建:ReconFusion

论文题目&#xff1a; ReconFusion: 3D Reconstruction with Diffusion Priors 论文作者&#xff1a; Rundi Wu, Ben Mildenhall, Philipp Henzler, Keunhong Park, Ruiqi Gao, Daniel Watson, Pratul P. Srinivasan, Dor Verbin, Jonathan T. Barron, Ben Poole, Aleksande…

基于多媒体的深度学习 Midreport自我总结分析

Resistor Ohm Value Estimation Challenge 需要将误差降低到1%以下 1、调整模型架构&#xff1a;增加模型的复杂性&#xff0c;例如增加卷积层或全连接层的数量&#xff0c;增加神经元数量等 # 在 ResistorEstimator 类的 CNN 方法中进行修改 def CNN(self, type):input In…