写点什么

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

作者:--linshuai
  • 2024-01-12
    广东
  • 本文字数:5315 字

    阅读完需:约 17 分钟

前言

之前上线了一个 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.tsimport { 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.tsimport { ObjectId } from "mongodb";import { BaseRequest, BaseResponse, BaseConf } from "../base";
export interface ReqDel extends BaseRequest { _id: ObjectId}
export interface ResDel extends BaseResponse { matchNum: number;}
// ApiDel.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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

发布于: 刚刚阅读数: 5
用户头像

--linshuai

关注

还未添加个人签名 2018-07-03 加入

还未添加个人简介

评论

发布
暂无评论
vue3 + tsrpc +mongodb 实现后台管理系统_mongodb_--linshuai_InfoQ写作社区