我的服务端项目
oi, 我的服务端项目又又又更新了。从最初的nestjs
到express
,再到koa
,我经历了很多。各有千秋,不过现在我使用了elysia。
之前喜欢折腾,不过现在对我来讲,没有那么多时间精力再去折腾其他技术了。所以我现在的项目主要是用elysia
来写的。
之前nestjs
对个人项目来讲太重了,装饰器,管道,拦截器等,我认为这些东西用于企业项目是非常合适的,但是对于一些个人项目或者测试项目来讲,没必要非搞的这么正规。
express
也是我最早使用的一款,它是通过中间件的形式去处理请求的,例如请求拦截需要返回统一的数据格式等,获取登录拦截等。
而现在的elysia
它使用的是bun
构建的,同时内置了一些 插件, 例如:定时任务,swagger 文档,cors 等。还可以自定义装饰器,只需定义一次,即可在多个路由中使用。这样重复性比较高,也比较方便。
elysia
的性能也比较好,因为它是基于bun
构建的,bun
是一个基于rust
的js
运行时,性能比较好。它的打包形式有俩种,
- 打包成 js 文件,最后执行 js
- 打包成二进制文件,执行二进制文件
先看下我的项目目录
├── src
│ ├── config // 配置文件
│ │ ├── cli // 命令行配置
│ │ ├── swagger // swagger 文档配置
│ ├── enum // 枚举
│ │ ├── cli // 命令行枚举
│ │ ├── http // http 枚举
│ ├── exceptions // 异常
│ │ ├── response.ts // 响应体
│ │ ├── error.ts // 异常
│ ├── modules // 模块
│ │ ├── sim-admin // 管理员模块
│ │ │ ├── index.ts // 控制器
│ │ │ ├── schemas.ts // 数据传输对象
│ │ │ ├── model.ts // 模型
│ │ │ ├── service.ts // 服务
│ │ ├── cron // 定时任务
│ ├── utils // 工具
│ │ ├── logger // 日志
│ │ │ ├── index.ts
│ │ ├── os // 操作系统
│ │ │ ├── index.ts
│ ├── main.ts
├── package.json
├── tsconfig.json
入口文件
可以参考一下我的项目的入口文件:
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { CliEnum } from "@/enum/cli";
import { getLocalIpAddresses } from "@/utils/os";
import { logger } from "@/utils/logger";
import { swagger } from "@elysiajs/swagger";
import { swagger_config } from "@/config/swagger";
import { ResponseUtil } from "./exceptions/response";
import { HttpEnum } from "./enum/http";
import { admin } from "@/modules/sim-admin";
import { cron } from "@/modules/cron";
import { serverTiming } from "@elysiajs/server-timing";
// TODO: 测试
new Elysia()
// 全局的错误拦截, 统一返回错误格式
.onError(({ error, code }) => {
return ResponseUtil.error(
error.toString(),
typeof code === "number" ? code : HttpEnum.INTERNAL_SERVER_ERROR
);
})
// 请求日志,可以写入文件,方便服务器运维查看请求日志
.onAfterHandle(({ request: { url, method }, response }) => {
logger.info(`[${url}] ${method} ${JSON.stringify(response)} \n\n`);
})
// 跨域
.use(cors())
.use(serverTiming())
// swagger 文档
.use(swagger(swagger_config))
// 管理员模块,路由器
.use(admin)
// 定时任务
.use(cron)
// 监听
.listen(CliEnum.APP_PORT, () => {
const [local_ip_address] = getLocalIpAddresses();
logger.info(
`\nApp running at:\n\t- Local: http://localhost:${CliEnum.APP_PORT}\n\t- Network: http://${local_ip_address}:${CliEnum.APP_PORT}`
);
});
装饰器,统一返回数据格式
import { Elysia } from "elysia";
import { SimAdminService } from "./service";
import { SimAdminModel } from "./model";
import { ResponseUtil } from "@/exceptions/response";
import { bearer } from "@elysiajs/bearer";
export const admin = new Elysia({
prefix: "/sim-admin",
detail: {
tags: ["sim-admin中后台管理系统"],
},
})
// 登录校验
.use(bearer())
// 装饰器,统一格式数据返回
.decorate("success", ResponseUtil.success)
.post(
"/login",
async ({ body, success }) => {
return success(await SimAdminService.login(body));
},
{
// 内置的登录参数校验
body: SimAdminModel.loginBody,
// swagger 文档
detail: {
summary: "用户登录",
description:
"用户名分别为:[admin,user,test],密码分别为:[admin,user,test]。不同的用户展示的权限也是不同的。",
},
}
);
登录参数校验
import { t } from "elysia";
export namespace SimAdminModel {
export const loginBody = t.Object({
username: t.String({
minLength: 1,
error: "用户名不能为空",
default: "admin",
description: "用户名",
examples: ["admin", "user", "test"],
}),
password: t.String({
minLength: 1,
error: "密码不能为空",
default: "admin",
description: "密码",
examples: ["admin", "user", "test"],
}),
});
export type loginBody = typeof loginBody.static;
export const getTotalData = t.Object({
page: t.Number({
default: 1,
description: "当前页码",
error: "当前页码不能为空",
examples: [1, 2, 3],
}),
pageSize: t.Number({
default: 10,
description: "每页数量",
error: "每页数量不能为空",
examples: [10, 20, 30],
}),
});
export type getTotalData = typeof getTotalData.static;
}
docker 部署
我这里没有打包二进制,如果需要参考
FROM docker.1ms.run/oven/bun AS build
WORKDIR /opt/apps/node-service
# Cache packages installation
COPY package.json package.json
COPY bun.lock bun.lock
RUN bun install
COPY ./src ./src
COPY tsconfig.json tsconfig.json
ENV NODE_ENV=production
CMD ["bun", "src/main.ts"]
EXPOSE 9999