我的 2026 全栈选型:Vue3 + Elysia + Bun + AlovaJS

47 阅读26分钟

我的 2026 全栈选型:Vue3 + Elysia + Bun + AlovaJS —— 快、轻、闭环、不再折腾

作为一名前端主导的全栈开发者,结合近两年的实战经验(覆盖个人项目、小团队协作项目及中型后台系统),我总结出一套经过多场景验证的最佳实践:Vue3 + Elysia + Bun + AlovaJS。过去一年,我踩过不少技术选型的坑:

  • Node.js 跑后端要配置一堆工具,开发准备成本高;
  • tRPC 要写两套代码,前后端语法割裂,维护繁琐;
  • Vue Query 要手动维护请求和状态的割裂,代码冗余;
  • 开发效率被繁琐的配置和冗余的代码严重拖累,无法专注业务开发。

直到我切换到 Vue3 + Elysia + Bun + AlovaJS 这个组合,才真正体会到“全栈闭环”的快乐——没有多余的工具、没有冗余的代码、没有类型割裂的痛苦,开发速度翻倍,维护成本骤降。今天就详细聊聊,我为什么最终选择了这一套技术栈,每一个选型都附带真实代码对比和客观优缺点分析,让你一眼看懂差距;同时针对普通项目必备的全局中间件(日志、权限、错误处理)、强模块化及 DI 依赖注入,讲解 Elysia 中的具体实现,解决大家最关心的实操问题。

一、运行时:选 Bun,不选 Node.js —— 客观对比,明确优劣(补充体积、生态、速度)

在接触 Bun 之前,我一直用 Node.js 作为全栈运行时,两者各有优劣,并非 Bun 全优、Node.js 全差,以下从体积、速度、生态、配置四个核心维度,做客观对比,方便根据项目场景选择。

补充:BunNode.js 客观对比表

对比维度BunNode.js
体积(安装包)约 100MB(单文件可执行程序,内置所有核心工具)约 20-60MB(仅运行时,工具需额外安装)
启动速度极快(毫秒级启动,比 Node.js 快 3-5 倍)中等(启动延迟明显,大型项目启动需数秒)
依赖安装速度极快(是 npm 的 30-100 倍,缓存机制更优)较慢(大型项目依赖安装需 2-5 分钟,易出现依赖冲突)
TS 支持原生支持,零配置运行 .ts 文件需额外安装 ts-nodetypescript,配置 tsconfig.json
热重启内置 --watch 命令,毫秒级热重启,无感开发需额外安装 nodemon,重启延迟 300ms-2s,易失效
生态成熟度较新,生态不完善,部分 Node.js 插件无法直接使用极其成熟,插件丰富(expresskoa 等),社区支持强
兼容性部分 Node.js API 未完全兼容,迁移成本低但需适配兼容性极强,几乎支持所有前端/后端工具,无适配成本
优势速度快、零配置、一体化工具链,适合快速开发生态完善、兼容性强、稳定可靠,适合大型生产项目
劣势生态不完善,部分场景需手动适配,稳定性待验证配置繁琐、速度慢,工具链分散,开发准备成本高

对比1:安装依赖速度——天差地别(客观补充劣势)

  • 方案A:Node.js + npm(麻烦,但兼容性无对手)
npm install
# 大型项目依赖安装,动辄2~5分钟,偶尔还会卡死、依赖冲突
# 每次重新安装,都要喝一杯水等它结束
# 优势:兼容性极强,所有 npm 包都能正常使用,无适配成本
  • 方案B:Bun(极速,但存在生态兼容问题)
bun install
# 同样的依赖,10秒内完成,速度是 npm 的30~100倍
# 再也不用为了装依赖浪费时间
# 劣势:部分冷门 npm 包可能无法正常安装,需手动处理依赖兼容

对比2:运行 TS 后端——零配置 vs 一堆配置(客观补充)

  • 方案A:Node.js(配置繁琐,但灵活可控)
# 第一步:安装依赖
npm install ts-node typescript @types/node
# 第二步:配置 tsconfig.json
# 第三步:运行
npx ts-node src/index.ts
# 优势:tsconfig 可按需配置,支持复杂的 TS 编译规则,适配大型项目

不仅要装一堆依赖,还要手动配置 tsconfig,稍微写错一个配置,就会报错无法运行,新手很容易卡在这里。

  • 方案B:Bun(原生支持,一键运行,但配置灵活度低)
bun src/index.ts
# 无需装任何转译工具,无需配置 tsconfig
# 直接运行 .ts 文件,毫秒级启动
# 劣势:无法自定义 TS 编译规则,复杂 TS 场景(如装饰器、模块解析)适配性一般

对比3:热重启开发——无感 vs 延迟(客观补充)

  • 方案A:Node.js + nodemon(延迟高,易失效,但稳定)
# package.json 配置
"scripts": {
  "dev": "nodemon src/index.ts"
}
# 优势:稳定可靠,支持自定义忽略文件、重启规则,适配复杂项目结构

每次保存代码,都要等 300ms~2s 才能重启,偶尔还会出现“保存了但不重启”的 bug,开发节奏被频繁打断。

  • 方案B:Bun(毫秒级热重启,无感开发,但功能简单)
# package.json 配置
"scripts": {
  "dev": "bun --watch src/index.ts"
}
# 劣势:热重启规则简单,无法自定义忽略文件,部分复杂项目场景适配不足

保存代码的瞬间,后端就完成重启,几乎没有延迟,开发时完全感觉不到“等待”,专注度大幅提升。

对比4:前后端一体启动——复杂脚本 vs 简单命令(客观补充)

  • 方案A:Node.js(需额外依赖,脚本复杂,但可控性强)
# 第一步:安装 concurrently
npm install concurrently --save-dev
# 第二步:配置脚本
"scripts": {
  "dev:web": "vite",
  "dev:api": "nodemon src/index.ts",
  "dev": "concurrently "npm run dev:web" "npm run dev:api""
}
# 优势:可分别控制前后端启动顺序、日志输出,支持复杂的启动逻辑

不仅要多装一个依赖,还要写复杂的脚本,偶尔还会出现“一个服务启动失败,另一个正常运行”的问题,排查起来很麻烦。

  • 方案B:Bun(无需额外依赖,一条命令搞定,但可控性弱)
"scripts": {
  "dev:web": "bun run vite",
  "dev:api": "bun --watch src/index.ts",
  "dev": "bun run dev:web & bun run dev:api"
}
# 劣势:无法控制前后端启动顺序,日志输出混在一起,排查问题不便

无需安装任何额外依赖,一条命令就能同时启动前后端,热重载各自独立,互不干扰,开发体验直接拉满。

本段总结

  • Node.js 是“一堆工具拼凑起来的运行时”,优势是生态成熟、兼容性强、稳定可靠,适合大型生产项目;劣势是每一步都要手动配置、手动衔接,开发效率低。
  • Bun 是“一体化运行时”,一个工具搞定“依赖管理+TS运行+热重启+打包测试”,优势是速度快、零配置,适合个人项目、小团队项目和快速迭代的中型项目;劣势是生态不完善、兼容性一般,稳定性有待长期验证。
  • 选型建议:追求开发效率、项目规模不大 → 选 Bun;追求稳定性、项目复杂、依赖多 → 选 Node.js

二、后端:选 Elysia,不选 Express / NestJS / HonoJS —— 轻量、类型安全,贴合前端开发习惯(附体积对比+客观优劣)

后端框架我试过 ExpressNestJSHonoJS,四者各有定位,并非 Elysia 全优,以下先补充四者体积对比,再分维度客观对比,结合普通项目需求给出选型建议。

补充:4个后端框架体积对比表(生产环境打包后)

框架名称体积(minified + gzip)核心依赖体积备注
Elysia约 15KB无额外核心依赖(zod 为可选校验依赖,约 10KB)轻量极致,内置类型、校验、中间件等核心功能
HonoJS约 12KB无额外核心依赖,极致轻量Elysia 更轻,但基础特性需额外封装
Express约 28KB依赖 path-to-regexpsend 等,合计约 8KB基础框架轻,但核心功能(校验、类型)需额外装包
NestJS约 150KB+依赖 @nestjs/corereflect-metadata 等,合计约 100KB+重量级框架,内置所有企业级特性,体积最大

核心总结:体积排序(从小到大):HonoJS < Elysia < Express < NestJSElysia 兼顾轻量和功能完整性,是前端主导全栈项目的均衡选择。

对比1:写一个带类型的接口——手动维护 vs 自动推导(覆盖4个框架,客观优劣)

  • 方案A:Express(无类型、无推导,易出错,但灵活无约束)
import express from 'express';
const app = express();
app.use(express.json());

// 无类型提示,req.params.id 不知道是什么类型
// 前端调用时,也不知道返回值结构,容易写错字段
app.get('/user/:id', (req, res) => {
  const id = req.params.id;
  res.json({ id, name: 'test' });
});

app.listen(3000);
// 优势:无任何架构约束,写法灵活,适合快速写简单接口
// 劣势:无类型安全,无自动推导,维护成本高,易出bug

后端写接口时,没有类型提示,很容易把参数类型写错;前端调用时,不知道返回值有哪些字段,只能靠接口文档,文档过时就会出 bug。

  • 方案B:Elysia(自动类型、自动推导,零维护,兼顾轻量和功能)
import { Elysia } from 'elysia';
const app = new Elysia()
  // 自动推导 params 类型,id 是 string 类型
  .get('/user/:id', ({ params }) => {
    // 返回值自动生成类型,前端可直接继承
    return { id: params.id, name: 'test' };
  })
  .listen(3000);

// 导出后端类型,前端直接引用
export type AppType = typeof app;
// 优势:自动类型推导,零配置,写法简洁,前端友好,体积轻
// 劣势:生态不如 `Express`、`NestJS` 完善,复杂企业级场景需手动适配

后端写接口时,paramsbody 自动有类型提示;前端只需导入 AppType,就能获得所有接口的类型,无需手动维护 DTO、无需写接口文档,编译时就能发现字段错误,彻底解决前后端类型割裂的问题。

  • 方案C:NestJS(类型安全但配置繁琐,冗余度高,企业级特性完善)
// 第一步:创建模块(必须创建模块,否则无法运行)
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';

@Module({
  controllers: [UserController]
})
export class UserModule {}

// 第二步:创建控制器(接口逻辑写在这里)
import { Controller, Get, Param } from '@nestjs/common';
// 手动定义 DTO 类型,维护成本高
interface UserParams {
  id: string;
}

@Controller('user')
export class UserController {
  // 手动标注参数类型,无法自动推导
  @Get(':id')
  getUser(@Param() params: UserParams) {
    return { id: params.id, name: 'nest user' };
  }
}

// 第三步:创建主模块,注册所有业务模块
import { NestFactory } from '@nestjs/core';
import { UserModule } from './user/user.module';

async function bootstrap() {
  const app = await NestFactory.create(UserModule);
  await app.listen(3000);
}
bootstrap();
// 优势:类型安全完善,内置企业级特性(微服务、DI、权限),适合大型项目
// 劣势:配置繁琐,学习曲线陡,冗余代码多,前端开发者上手难度高

NestJS 虽然支持类型安全,但必须遵循“模块-控制器-服务”的固定架构,哪怕是一个简单的接口,也要创建多个文件、写大量冗余配置;参数类型需手动定义 DTO,无法像 Elysia 那样自动推导,前端需额外导入 DTO 类型,维护成本翻倍,前端开发者上手难度极高。

  • 方案D:HonoJS(轻量有类型,但特性极简,需额外封装)
import { Hono } from 'hono';
const app = new Hono();

// 需手动标注 params 类型,无法自动推导返回值类型
app.get('/user/:id', (c) => {
  const id = c.req.param('id'); // 手动获取参数,无自动提示
  return c.json({ id, name: 'hono user' });
});

app.listen({ port: 3000 });

// 前端需手动定义接口类型,无法像 Elysia 那样直接继承后端类型
export type AppType = typeof app;
// 优势:极致轻量,启动速度快,适合轻量接口、Serverless 场景
// 劣势:基础特性不完善,类型推导弱,普通项目需额外封装中间件、校验等功能

HonoJS 轻量、启动快,支持基础类型提示,但参数类型需手动标注,返回值类型无法自动推导,前端无法直接继承后端类型,需手动维护接口类型;且基础特性(如全局中间件、模块化)的写法不够直观,普通项目需额外封装才能满足需求。

对比2:接口校验——手动封装 vs 内置支持(覆盖4个框架,客观优劣)

  • 方案A:Express(需手动装依赖、手动校验,灵活但繁琐)
import { z } from 'zod';
import express from 'express';
const app = express();
app.use(express.json());

// 第一步:定义校验规则
const userSchema = z.object({
  name: z.string(),
  age: z.number().optional()
});

// 第二步:手动校验,手动处理错误
app.post('/user', (req, res) => {
  const result = userSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }
  res.json({ id: 1, ...result.data });
});

app.listen(3000);
// 优势:校验逻辑可完全自定义,灵活适配复杂校验场景
// 劣势:需额外装依赖,手动写校验逻辑,代码冗余,维护麻烦

不仅要额外安装 zod 依赖,还要手动写校验逻辑、手动处理错误,代码冗余,而且校验逻辑和接口逻辑混在一起,维护起来很麻烦。

  • 方案B:Elysia(内置校验,简洁高效,兼顾灵活和便捷)
import { Elysia } from 'elysia';
import { z } from 'zod';

const app = new Elysia()
  .post(
    '/user',
    ({ body }) => {
      // 校验通过后,直接使用 body,无需手动处理
      return { id: 1, ...body };
    },
    {
      // 内置校验,与接口逻辑分离
      body: z.object({
        name: z.string(),
        age: z.number().optional()
      })
    }
  )
  .listen(3000);

export type AppType = typeof app;
// 优势:内置校验,与接口逻辑分离,代码简洁,前端可同步获取校验类型
// 劣势:校验规则依赖 `zod`,自定义校验逻辑的灵活性略低于 Express

无需额外配置,直接在接口参数中定义校验规则,校验失败会自动返回 400 错误,代码简洁、逻辑清晰,前端也能通过类型推导,知道需要传哪些参数。

  • 方案C:NestJS(校验需额外装包,配置繁琐,校验体系完善)
// 第一步:安装校验依赖(必须装包,否则无法实现校验)
// npm install class-validator class-transformer
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { ValidationPipe } from '@nestjs/platform-express';

@Module({
  controllers: [UserController]
})
export class UserModule {}

// 第二步:手动定义 DTO 校验规则
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { IsString, IsOptional, IsNumber } from 'class-validator';

class CreateUserDto {
  @IsString()
  name: string;

  @IsNumber()
  @IsOptional()
  age?: number;
}

@Controller('user')
export class UserController {
  // 手动启用校验管道,否则校验不生效
  @Post()
  @UsePipes(new ValidationPipe())
  createUser(@Body() body: CreateUserDto) {
    return { id: 1, ...body };
  }
}

// 第三步:启动项目(省略主模块代码,与上文一致)
// 优势:校验体系完善,支持装饰器语法,可集成全局校验,适合大型项目
// 劣势:需额外装2个依赖,配置繁琐,修改校验规则需改动多处

NestJS 实现接口校验,需额外安装 2 个依赖,还要手动定义 DTO 类、添加校验装饰器,再手动启用校验管道,步骤繁琐;后续修改校验规则,需同时修改 DTO 类和接口逻辑,维护成本高,不符合前端开发者“简洁高效”的需求。

  • 方案D:HonoJS(校验需手动封装,无内置支持,轻量但繁琐)
import { Hono } from 'hono';
import { z } from 'zod';
const app = new Hono();

// 第一步:定义校验规则
const userSchema = z.object({
  name: z.string(),
  age: z.number().optional()
});

// 第二步:手动校验,手动处理错误(与 Express 类似)
app.post('/user', async (c) => {
  const body = await c.req.json();
  const result = userSchema.safeParse(body);
  if (!result.success) {
    return c.json({ error: result.error }, 400);
  }
  return c.json({ id: 1, ...result.data });
});

app.listen({ port: 3000 });
// 优势:轻量无冗余,校验逻辑可灵活自定义,适合轻量场景
// 劣势:无内置校验,需手动装依赖、写逻辑,维护成本高,与 Express 一样繁琐

HonoJS 本身不内置接口校验功能,需手动安装 zod 依赖、手动写校验逻辑,与 Express 一样繁琐;且校验逻辑与接口逻辑混在一起,没有 Elysia 那样“校验与接口分离”的设计,维护起来不够便捷。

对比3:全局中间件、模块化、DI 依赖注入(普通项目必备,覆盖4个框架,客观优劣)

这三个特性是普通项目(尤其是中型项目)的必备需求,也是前端开发者转全栈时最关心的“工程化能力”,下面对比四个框架的实现复杂度,重点突出 Elysia 的优势,同时客观呈现各框架的优劣。

  • 方案A:Express(无原生支持,全靠手动封装,灵活但无规范)
import express from 'express';
const app = express();
app.use(express.json());

// 1. 全局中间件:手动封装,无统一规范
const loggerMiddleware = (req, res, next) => {
  const start = Date.now();
  next();
  const end = Date.now();
  console.log(`${req.method} ${req.url} - ${end - start}ms`);
};
app.use(loggerMiddleware); // 注册全局中间件

// 2. 模块化:无原生支持,需手动拆分路由,手动注册
const userRouter = express.Router();
userRouter.get('/:id', (req, res) => res.json({ id: req.params.id }));
app.use('/user', userRouter); // 手动注册路由模块

// 3. DI 依赖注入:无原生支持,需额外装包(如 inversify),配置复杂
// 此处省略大量 DI 配置代码,普通项目几乎不会用 Express 做 DI 注入

app.listen(3000);
// 优势:无规范约束,中间件、模块化写法可完全自定义,灵活度极高
// 劣势:无原生支持,全靠手动封装,代码冗余、规范混乱,维护难度大

Express 无原生中间件、模块化、DI 支持,全靠手动封装或额外装包,代码冗余、规范混乱,普通项目维护起来难度极大,不适合前端开发者快速上手。

  • 方案B:Elysia(原生支持,写法简洁,前端友好,均衡性强)
import { Elysia } from 'elysia';
import { z } from 'zod';
import { di } from 'elysia-di';

// 1. 全局中间件:原生支持,可按需跳过,逻辑清晰
const loggerMiddleware = async ({ request, set, next }) => {
  const start = Date.now();
  await next();
  const end = Date.now();
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${end - start}ms`);
};

// 2. 模块化:原生支持,按业务拆分,统一注册
const userModule = new Elysia({ prefix: '/user' })
  .get('/:id', ({ params }) => ({ id: params.id, name: 'test' }));

// 3. DI 依赖注入:原生支持,可通过 elysia-di 简化配置
class UserService {
  getUserById(id: string) {
    return { id, name: 'test' };
  }
}

const app = new Elysia()
  .use(loggerMiddleware) // 注册全局中间件
  .use(di({ providers: [{ provide: UserService, useClass: UserService }] })) // 注册 DI 依赖
  .use(userModule) // 注册业务模块
  .listen(3000);

export type AppType = typeof app;
// 优势:原生支持三个核心特性,写法简洁,与 Vue3 风格统一,前端上手无压力
// 劣势:DI 体系不如 NestJS 完善,复杂微服务场景需额外适配

Elysia 原生支持三个核心特性,写法简洁、逻辑清晰,无需额外装包(DI 可按需用 elysia-di 简化),与 Vue3 语法风格统一,前端开发者上手无压力,完全贴合普通项目需求。

  • 方案C:NestJS(原生支持,但配置繁琐,学习成本高,企业级首选)
// 1. 全局中间件:需创建中间件类,手动注册
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();
    next();
    const end = Date.now();
    console.log(`${req.method} ${req.url} - ${end - start}ms`);
  }
}

// 2. 模块化:必须按“模块-控制器-服务”拆分,配置繁琐
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService] // 注册服务(DI 依赖)
})
export class UserModule {}

// 3. DI 依赖注入:原生支持,但需通过装饰器实现,代码冗余
import { Injectable, Controller, Get, Param } from '@nestjs/common';

@Injectable() // 必须添加装饰器,否则无法注入
export class UserService {
  getUserById(id: string) {
    return { id, name: 'nest user' };
  }
}

@Controller('user')
export class UserController {
  // 手动注入服务,需写构造函数
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.userService.getUserById(id);
  }
}

// 4. 主模块注册中间件和业务模块(省略部分代码)
import { NestFactory, MiddlewareConsumer, Module } from '@nestjs/core';
import { UserModule } from './user/user.module';
import { LoggerMiddleware } from './middleware/logger.middleware';

@Module({ imports: [UserModule] })
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*'); // 注册全局中间件
  }
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
// 优势:三个核心特性支持完善,架构规范,适合大型企业级项目、微服务场景
// 劣势:配置繁琐,学习曲线陡,冗余代码多,前端开发者上手难度大

NestJS 虽然原生支持三个核心特性,但必须遵循严格的“装饰器+固定架构”,哪怕是简单的功能,也要创建多个文件、写大量装饰器和配置;DI 注入需手动写构造函数,模块化拆分繁琐,前端开发者需要花费大量时间学习框架规范,不适合中小型全栈项目。

  • 方案D:HonoJS(部分支持,需额外封装,灵活性不足,轻量优先)
import { Hono } from 'hono';
const app = new Hono();

// 1. 全局中间件:原生支持,但写法不够直观,无按需跳过功能
app.use(async (c, next) => {
  const start = Date.now();
  await next();
  const end = Date.now();
  console.log(`${c.req.method} ${c.req.url} - ${end - start}ms`);
});

// 2. 模块化:支持拆分,但需手动拼接路由,无统一前缀管理
const userRouter = new Hono();
userRouter.get('/:id', (c) => c.json({ id: c.req.param('id') }));
app.route('/user', userRouter); // 手动拼接路由前缀

// 3. DI 依赖注入:无原生支持,需额外装包,配置复杂
// 此处省略大量 DI 配置代码,普通项目很少用 HonoJS 做 DI 注入

app.listen({ port: 3000 });
// 优势:轻量启动快,中间件、模块化写法简洁,适合轻量接口、Serverless 场景
// 劣势:中间件无法按需跳过,模块化无统一前缀,DI 需额外封装,不适合中型项目

HonoJS 支持全局中间件和简单模块化,但中间件无法按需跳过(如登录接口跳过权限校验),模块化拆分无统一前缀管理,需手动拼接;DI 依赖注入无原生支持,需额外装包,普通项目使用成本高,不如 Elysia 贴合需求。

重点:普通项目必备特性——Elysia 实现全局中间件、强模块化、DI 依赖注入(补充完善,呼应上文对比)

很多人误以为 Elysia 轻量就“功能薄弱”,但实际上,它能轻松实现普通项目(甚至中型项目)必备的全局中间件(日志、权限、错误处理)、强模块化和 DI 依赖注入,而且写法比 NestJS 更简洁,前端开发者更容易上手,也比 HonoJS 更省心(无需额外封装基础特性)。下面直接上可复制代码,落地性拉满。

1. 全局中间件:日志、权限、错误处理(普通项目必加)

Elysia 的中间件支持全局注册,可统一处理日志、权限校验、错误捕获,无需在每个接口单独写逻辑,代码更简洁、可维护性更高。

import { Elysia } from 'elysia';
import { z } from 'zod';

// 1. 全局日志中间件(记录请求方法、路径、耗时)
const loggerMiddleware = async ({ request, set, next }) => {
  const start = Date.now();
  // 执行后续逻辑(接口处理)
  await next();
  const end = Date.now();
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${end - start}ms`);
};

// 2. 全局权限中间件(校验 Token,普通项目常用)
const authMiddleware = ({ request, set, next }) => {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  // 简单 Token 校验(实际项目可对接 JWT、Redis 等)
  if (!token || token !== 'valid-token') {
    set.status = 401;
    return { error: '未授权,请登录' };
  }
  // 权限通过,执行后续逻辑
  return next();
};

// 3. 全局错误处理中间件(统一捕获接口错误,避免返回混乱)
const errorMiddleware = async ({ error, set, next }) => {
  try {
    await next();
  } catch (err) {
    set.status = 500;
    // 区分开发/生产环境,生产环境不暴露具体错误信息
    const errorMsg = process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误';
    return { error: errorMsg };
  }
};

// 注册全局中间件(顺序:日志 → 权限 → 错误处理)
const app = new Elysia()
  .use(loggerMiddleware) // 全局日志
  .use(authMiddleware)   // 全局权限(可根据路由按需跳过,见下文)
  .use(errorMiddleware)  // 全局错误处理
  // 测试接口:需要权限校验
  .get('/user/:id', ({ params }) => {
    return { id: params.id, name: 'test' };
  })
  // 测试接口:跳过权限校验(登录接口常用)
  .get('/login', ({ query }) => {
    return { token: 'valid-token' };
  }, {
    // 跳过全局权限中间件
    beforeHandle: ({ request }) => {
      if (request.url === '/login') return true;
    }
  })
  .listen(3000);

export type AppType = typeof app;

说明:中间件支持“按需跳过”(如登录接口无需权限校验),也可针对单个路由单独注册中间件,灵活适配普通项目的各种场景;日志中间件可后续对接 ELK 等日志平台,权限中间件可替换为 JWT 校验,完全满足普通项目需求。

2. 强模块化:按业务拆分,避免路由混乱(普通项目必做)

普通项目随着业务迭代,路由会越来越多,若全部写在一个文件中,会导致代码臃肿、难以维护。Elysia 支持模块化拆分,可按业务(用户、文章、订单等)拆分路由模块,统一注册,结构清晰,符合普通项目的工程化需求。

// src/modules/user.ts(用户模块)
import { Elysia } from 'elysia';
import { z } from 'zod';

// 定义用户模块路由
export const userModule = new Elysia({ prefix: '/user' }) // 路由前缀:/user
  .get('/:id', ({ params }) => {
    return { id: params.id, name: '用户详情' };
  })
  .post('/', ({ body }) => {
    return { id: '1', ...body };
  }, {
    body: z.object({ name: z.string(), age: z.number().optional() })
  });

// src/modules/article.ts(文章模块)
import { Elysia } from 'elysia';

export const articleModule = new Elysia({ prefix: '/article' }) // 路由前缀:/article
  .get('/list', () => {
    return { list: [], total: 0 };
  })
  .get('/:id', ({ params }) => {
    return { id: params.id, title: '文章标题' };
  });

// src/index.ts(主入口,统一注册模块)
import { Elysia } from 'elysia';
import { userModule } from './modules/user';
import { articleModule } from './modules/article';
import { loggerMiddleware, authMiddleware, errorMiddleware } from './middleware';

const app = new Elysia()
  .use(loggerMiddleware)
  .use(authMiddleware)
  .use(errorMiddleware)
  // 注册业务模块
  .use(userModule)
  .use(articleModule)
  .listen(3000);

export type AppType = typeof app;

效果:拆分后,每个业务模块独立维护,修改用户相关逻辑时,只需改动user.ts文件,不会影响文章模块的代码,极大降低了维护成本;路由前缀统一配置,无需手动拼接,避免出现路由路径混乱的问题;新增业务模块(如订单模块)时,只需创建对应模块文件,在主入口注册即可,扩展性极强。

补充:模块化还支持“模块嵌套”,若某个业务模块内部逻辑复杂,可进一步拆分个子模块,例如用户模块可拆分“用户认证”“用户信息管理”个子模块,层级清晰,符合前端开发者熟悉的组件化思维,上手无压力。

// src/modules/user/auth.ts(用户认证子模块)
import { Elysia } from 'elysia';

export const userAuthModule = new Elysia()
  .post('/login', ({ body }) => {
    return { token: 'valid-token', message: '登录成功' };
  })
  .post('/logout', () => {
    return { message: '登出成功' };
  });

// src/modules/user/info.ts(用户信息子模块)
import { Elysia } from 'elysia';
import { z } from 'zod';

export const userInfoModule = new Elysia()
  .get('/:id', ({ params }) => {
    return { id: params.id, name: '用户详情', avatar: 'https://example.com/avatar.jpg' };
  })
  .put('/:id', ({ params, body }) => {
    return { id: params.id, ...body, message: '信息修改成功' };
  }, {
    body: z.object({ name: z.string(), age: z.number().optional() })
  });

// src/modules/user.ts(用户主模块,嵌套子模块)
import { Elysia } from 'elysia';
import { userAuthModule } from './auth';
import { userInfoModule } from './info';

export const userModule = new Elysia({ prefix: '/user' })
  .use(userAuthModule) // 注册认证子模块,路由:/user/login、/user/logout
  .use(userInfoModule); // 注册信息子模块,路由:/user/:id、/user/:id(PUT)
3. DI 依赖注入:解耦业务逻辑,提升可维护性(普通项目进阶必备)

普通项目随着业务复杂度提升,会出现“接口逻辑与业务逻辑耦合”的问题——比如多个接口需要调用“查询用户信息”的逻辑,若直接在接口中重复编写,后续修改时需改动所有相关接口,维护成本极高。DI(依赖注入)可完美解决这个问题,将业务逻辑封装为“服务”,接口按需注入服务,实现逻辑解耦,这也是Elysia相比Express、HonoJS的核心优势之一(无需额外装包,原生适配)。

下面用可复制代码,实现“用户服务”的DI注入,贴合普通项目的实际业务场景(查询用户、创建用户、删除用户),同时演示服务之间的依赖注入,让代码更具复用性。

// src/services/user.service.ts(用户服务,封装业务逻辑)
import { Injectable } from 'elysia-di'; // 从elysia-di导入Injectable装饰器

// 标记该类可被注入(无需复杂配置,前端友好)
@Injectable()
export class UserService {
  // 模拟数据库数据(实际项目可对接Prisma、MongoDB等)
  private users = [
    { id: '1', name: '张三', age: 25 },
    { id: '2', name: '李四', age: 28 }
  ];

  // 业务逻辑:查询单个用户
  getUserById(id: string) {
    const user = this.users.find(item => item.id === id);
    if (!user) throw new Error('用户不存在');
    return user;
  }

  // 业务逻辑:查询所有用户
  getUsers() {
    return this.users;
  }

  // 业务逻辑:创建用户
  createUser(user: { name: string; age?: number }) {
    const newUser = { id: Date.now().toString(), ...user };
    this.users.push(newUser);
    return newUser;
  }

  // 业务逻辑:删除用户
  deleteUser(id: string) {
    const index = this.users.findIndex(item => item.id === id);
    if (index === -1) throw new Error('用户不存在');
    this.users.splice(index, 1);
    return { message: '删除成功' };
  }
}

// src/services/log.service.ts(日志服务,演示服务间依赖注入)
import { Injectable } from 'elysia-di';

@Injectable()
export class LogService {
  // 记录业务操作日志(实际项目可写入文件或日志平台)
  recordLog(operation: string, data: any) {
    console.log(`[业务日志] ${new Date().toISOString()} - 操作:${operation},数据:${JSON.stringify(data)}`);
  }
}

// src/modules/user.ts(用户模块,注入服务)
import { Elysia } from 'elysia';
import { z } from 'zod';
import { UserService } from '../services/user.service';
import { LogService } from '../services/log.service';

export const userModule = new Elysia({ prefix: '/user' })
  // 注入用户服务和日志服务(自动实例化,无需手动new)
  .inject({ userService: UserService, logService: LogService })
  // 接口1:查询单个用户
  .get('/:id', ({ params, userService, logService }) => {
    const user = userService.getUserById(params.id);
    logService.recordLog('查询用户', { userId: params.id, user });
    return user;
  })
  // 接口2:查询所有用户
  .get('/', ({ userService }) => {
    return userService.getUsers();
  })
  // 接口3:创建用户(带校验)
  .post('/', ({ body, userService, logService }) => {
    const newUser = userService.createUser(body);
    logService.recordLog('创建用户', newUser);
    return newUser;
  }, {
    body: z.object({ name: z.string(), age: z.number().optional() })
  })
  // 接口4:删除用户
  .delete('/:id', ({ params, userService, logService }) => {
    const result = userService.deleteUser(params.id);
    logService.recordLog('删除用户', { userId: params.id });
    return result;
  });

// src/index.ts(主入口,注册DI和模块)
import { Elysia } from 'elysia';
import { di } from 'elysia-di';
import { userModule } from './modules/user';
import { UserService } from './services/user.service';
import { LogService } from './services/log.service';
import { loggerMiddleware, authMiddleware, errorMiddleware } from './middleware';

const app = new Elysia()
  .use(loggerMiddleware)
  .use(authMiddleware)
  .use(errorMiddleware)
  // 注册DI依赖,全局可注入
  .use(di({
    providers: [
      { provide: UserService, useClass: UserService },
      { provide: LogService, useClass: LogService }
    ]
  }))
  .use(userModule)
  .listen(3000);

export type AppType = typeof app;

说明:DI依赖注入的核心优势的是“解耦”——接口只负责接收请求、返回响应,具体的业务逻辑(查询、创建、删除用户)都封装在服务中,后续修改业务逻辑时,只需改动服务代码,无需修改接口;同时服务可复用,若其他模块(如文章模块)需要查询用户信息,直接注入UserService即可,无需重复编写逻辑。

与NestJS的DI相比,Elysia的DI无需创建模块、无需复杂的装饰器配置,写法更简洁,完全贴合前端开发者的编码习惯,普通项目无需学习复杂的DI原理,就能快速上手使用。

本段总结

  • Express:无原生中间件、模块化、DI支持,全靠手动封装,代码冗余、维护困难,不适合前端主导的全栈项目;
  • NestJS:三个核心特性支持完善,但配置繁琐、学习曲线陡,冗余代码多,适合大型企业级项目,不适合中小型项目和前端开发者快速上手;
  • HonoJS:轻量但基础特性不完善,中间件无法按需跳过,模块化无统一管理,DI需额外封装,适合轻量接口、Serverless场景,不适合中型项目;
  • Elysia:原生支持三个核心特性,写法简洁、前端友好,兼顾轻量和功能完整性,可轻松实现普通项目(甚至中型项目)的工程化需求,是前端主导全栈项目的最优选择。
// src/services/user.service.ts(用户服务,封装业务逻辑)
import { Injectable } from 'elysia-di'; // 从elysia-di导入Injectable装饰器

// 标记该类可被注入(无需复杂配置,前端友好)
@Injectable()
export class UserService {
  // 模拟数据库数据(实际项目可对接Prisma、MongoDB等)
  private users = [
    { id: '1', name: '张三', age: 25 },
    { id: '2', name: '李四', age: 28 }
  ];

  // 业务逻辑:查询单个用户
  getUserById(id: string) {
    const user = this.users.find(item => item.id === id);
    if (!user) throw new Error('用户不存在');
    return user;
  }

  // 业务逻辑:查询所有用户
  getUsers() {
    return this.users;
  }

  // 业务逻辑:创建用户
  createUser(user: { name: string; age?: number }) {
    const newUser = { id: Date.now().toString(), ...user };
    this.users.push(newUser);
    return newUser;
  }

  // 业务逻辑:删除用户
  deleteUser(id: string) {
    const index = this.users.find(item => item.id === id);
    if (index === -1) throw new Error('用户不存在');
    this.users.splice(index, 1);
    return { message: '删除成功' };
  }
}

// src/services/log.service.ts(日志服务,演示服务间依赖注入)
import { Injectable } from 'elysia-di';

@Injectable()
export class LogService {
  // 记录业务操作日志(实际项目可写入文件或日志平台)
  recordLog(operation: string, data: any) {
    console.log(`[业务日志] ${new Date().toISOString()} - 操作:${operation},数据:${JSON.stringify(data)}`);
  }
}

// src/modules/user.ts(用户模块,注入服务)
import { Elysia } from 'elysia';
import { z } from 'zod';
import { UserService } from '../services/user.service';
import { LogService } from '../services/log.service';

export const userModule = new Elysia({ prefix: '/user' })
  // 注入用户服务和日志服务(自动实例化,无需手动new)
  .inject({ userService: UserService, logService: LogService })
  // 接口1:查询单个用户
  .get('/:id', ({ params, userService, logService }) => {
    const user = userService.getUserById(params.id);
    logService.recordLog('查询用户', { userId: params.id, user });
    return user;
  })
  // 接口2:查询所有用户
  .get('/', ({ userService }) => {
    return userService.getUsers();
  })
  // 接口3:创建用户(带校验)
  .post('/', ({ body, userService, logService }) => {
    const newUser = userService.createUser(body);
    logService.recordLog('创建用户', newUser);
    return newUser;
  }, {
    body: z.object({ name: z.string(), age: z.number().optional() })
  })
  // 接口4:删除用户
  .delete('/:id', ({ params, userService, logService }) => {
    const result = userService.deleteUser(params.id);
    logService.recordLog('删除用户', { userId: params.id });
    return result;
  });

// src/index.ts(主入口,注册DI和模块)
import { Elysia } from 'elysia';
import { di } from 'elysia-di';
import { userModule } from './modules/user';
import { UserService } from './services/user.service';
import { LogService } from './services/log.service';
import { loggerMiddleware, authMiddleware, errorMiddleware } from './middleware';

const app = new Elysia()
  .use(loggerMiddleware)
  .use(authMiddleware)
  .use(errorMiddleware)
  // 注册DI依赖,全局可注入
  .use(di({
    providers: [
      { provide: UserService, useClass: UserService },
      { provide: LogService, useClass: LogService }
    ]
  }))
  .use(userModule)
  .listen(3000);

export type AppType = typeof app;

说明:DI依赖注入的核心优势是“解耦”——接口只负责接收请求、返回响应,具体的业务逻辑(查询、创建、删除用户)都封装在服务中,后续修改业务逻辑时,只需改动服务代码,无需修改接口;同时服务可复用,若其他模块(如文章模块)需要查询用户信息,直接注入UserService即可,无需重复编写逻辑。

与NestJS的DI相比,Elysia的DI无需创建模块、无需复杂的装饰器配置,写法更简洁,完全贴合前端开发者的编码习惯,普通项目无需学习复杂的DI原理,就能快速上手使用。

补充:实际项目中,可将服务进一步拆分(如数据访问层、业务逻辑层),比如新增user.repository.ts负责数据库操作,user.service.ts负责业务逻辑处理,DI注入可灵活适配这种分层架构,进一步提升代码可维护性。

// src/repositories/user.repository.ts(数据访问层,负责数据库操作)
import { Injectable } from 'elysia-di';

@Injectable()
export class UserRepository {
  // 模拟数据库操作(实际项目对接Prisma/MongoDB)
  private users = [
    { id: '1', name: '张三', age: 25 },
    { id: '2', name: '李四', age: 28 }
  ];

  findById(id: string) {
    return this.users.find(item => item.id === id);
  }

  findAll() {
    return this.users;
  }

  create(user: { name: string; age?: number }) {
    const newUser = { id: Date.now().toString(), ...user };
    this.users.push(newUser);
    return newUser;
  }

  delete(id: string) {
    const index = this.users.findIndex(item => item.id === id);
    if (index === -1) return null;
    this.users.splice(index, 1);
    return true;
  }
}

// src/services/user.service.ts(业务逻辑层,依赖数据访问层)
import { Injectable } from 'elysia-di';
import { UserRepository } from '../repositories/user.repository';

@Injectable()
export class UserService {
  // 注入数据访问层服务
  constructor(private userRepository: UserRepository) {}

  // 业务逻辑:查询单个用户(包含业务校验)
  getUserById(id: string) {
    const user = this.userRepository.findById(id);
    if (!user) throw new Error('用户不存在');
    // 额外业务逻辑(如权限校验、数据格式化)
    return { ...user, avatar: 'https://example.com/avatar.jpg' };
  }

  // 其他业务方法(复用数据访问层方法)
  getUsers() {
    return this.userRepository.findAll();
  }

  createUser(user: { name: string; age?: number }) {
    // 业务校验(如用户名去重)
    const existUser = this.userRepository.findAll().find(item => item.name === user.name);
    if (existUser) throw new Error('用户名已存在');
    return this.userRepository.create(user);
  }

  deleteUser(id: string) {
    const result = this.userRepository.delete(id);
    if (!result) throw new Error('用户不存在');
    return { message: '删除成功' };
  }
}

// src/index.ts(注册数据访问层依赖)
import { Elysia } from 'elysia';
import { di } from 'elysia-di';
import { userModule } from './modules/user';
import { UserService } from './services/user.service';
import { LogService } from './services/log.service';
import { UserRepository } from './repositories/user.repository';
import { loggerMiddleware, authMiddleware, errorMiddleware } from './middleware';

const app = new Elysia()
  .use(loggerMiddleware)
  .use(authMiddleware)
  .use(errorMiddleware)
  // 注册所有DI依赖(数据访问层、服务层)
  .use(di({
    providers: [
      { provide: UserRepository, useClass: UserRepository },
      { provide: UserService, useClass: UserService },
      { provide: LogService, useClass: LogService }
    ]
  }))
  .use(userModule)
  .listen(3000);

export type AppType = typeof app;

这种分层架构+DI注入的方式,适合中型项目的长期维护,数据访问层和业务逻辑层分离,后续替换数据库(如从MongoDB切换到MySQL)时,只需修改数据访问层代码,无需改动业务逻辑和接口,扩展性拉满。

本段总结

  • Express:无原生中间件、模块化、DI支持,全靠手动封装,代码冗余、维护困难,不适合前端主导的全栈项目;
  • NestJS:三个核心特性支持完善,但配置繁琐、学习曲线陡,冗余代码多,适合大型企业级项目,不适合中小型项目和前端开发者快速上手;
  • HonoJS:轻量但基础特性不完善,中间件无法按需跳过,模块化无统一管理,DI需额外封装,适合轻量接口、Serverless场景,不适合中型项目;
  • Elysia:原生支持三个核心特性,写法简洁、前端友好,兼顾轻量和功能完整性,可轻松实现普通项目(甚至中型项目)的工程化需求,是前端主导全栈项目的最优选择。

三、前端:选 Vue3 + AlovaJS,不选 Vue3 + Vue Query / Axios —— 请求与状态闭环,代码更简洁(附实操对比)

前端部分,Vue3 作为主流框架,无需过多纠结(相比React,Vue3的模板语法更贴合前端开发者习惯,上手更快、生态更完善),重点对比“请求工具”——AlovaJS 对比 Vue Query + Axios,前者实现“请求+状态+缓存”一体化,后者需手动衔接,代码冗余,维护成本高。

对比1:普通列表请求——AlovaJS(一体化)vs Axios + Vue Query(手动衔接)

  • 方案A:Axios + Vue Query(手动维护状态,代码冗余,衔接繁琐)
<template>
  <div class="user-list">
    <div v-if="isLoading">加载中...</div>
    <div v-if="error" class="error">{{ error.message }}</div>
    <ul v-if="users">
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import axios from 'axios';
import { useQuery } from '@tanstack/vue-query';

// 1. 手动封装请求函数
const fetchUsers = async () => {
  const res = await axios.get('http://localhost:3000/user');
  return res.data;
};

// 2. 手动维护请求状态(加载、错误、数据)
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
});
</script>

看似简单,但存在两个核心问题:一是请求函数与状态管理分离,需手动衔接;二是缓存、刷新、重试等功能需额外配置,代码冗余;三是前后端类型割裂,需手动定义接口返回类型,易出错。

  • 方案B:AlovaJS(请求+状态+缓存一体化,零冗余,类型自动同步)
<template>
  <div class="user-list">
    <div v-if="users.loading">加载中...</div>
    <div v-if="users.error" class="error">{{ users.error.message }}</div>
    <ul v-if="users.data">
      <li v-for="user in users.data" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { createAlova, useRequest } from 'alova';
import { VueHook } from 'alova/vue';
// 导入后端自动生成的类型,实现前后端类型同步
import type { AppType } from '../api/src/index';

// 1. 初始化Alova(全局配置,一次配置,全局可用)
const alovaInstance = createAlova({
  baseURL: 'http://localhost:3000',
  statesHook: VueHook, // 适配Vue3
  // 自动同步后端类型(关键:无需手动定义接口类型)
  typeAssert: true
});

// 2. 定义请求(请求+状态一体化,无需手动维护状态)
const users = useRequest(
  alovaInstance.Get<AppType['/user']['get']>('/user')
);
</script>

核心优势:无需手动封装请求函数、无需手动维护加载/错误/数据状态,AlovaJS 自动集成;前后端类型自动同步,导入后端 AppType 后,请求返回值、参数自动有类型提示,彻底解决类型割裂问题;缓存、刷新、重试等功能内置,无需额外配置。

对比2:带校验的提交请求——AlovaJS(一体化校验)vs Axios + Vue Query(手动校验)

  • 方案A:Axios + Vue Query(手动校验,手动处理提交状态)
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="name" placeholder="请输入用户名" />
    <input v-model.number="age" type="number" placeholder="请输入年龄" />
    <button type="submit" :disabled="isSubmitting">提交</button>
    <div v-if="submitError" class="error">{{ submitError.message }}</div>
  </form>
</template>

<script setup lang="ts">
import axios from 'axios';
import { useMutation } from '@tanstack/vue-query';
import { ref } from 'vue';
import { z } from 'zod';

// 1. 手动定义校验规则
const userSchema = z.object({
  name: z.string(),
  age: z.number().optional()
});

// 2. 手动维护表单数据
const name = ref('');
const age = ref<number | undefined>(undefined);

// 3. 手动封装提交函数,手动校验
const { mutate, isPending: isSubmitting, error: submitError } = useMutation({
  mutationFn: async (data: { name: string; age?: number }) => {
    const result = userSchema.safeParse(data);
    if (!result.success) {
      throw new Error('参数校验失败');
    }
    const res = await axios.post('http://localhost:3000/user', result.data);
    return res.data;
  },
  onSuccess: () => {
    // 提交成功后,手动刷新列表数据
    queryClient.invalidateQueries({ queryKey: ['users'] });
    name.value = '';
    age.value = undefined;
  }
});

// 4. 手动触发提交
const handleSubmit = () => {
  mutate({ name: name.value, age: age.value });
};
</script>

代码冗余严重:手动定义校验规则、手动维护表单数据、手动处理提交状态、手动刷新列表,后续修改校验规则或提交逻辑,需改动多处代码,维护成本高。

  • 方案B:AlovaJS(内置校验,自动维护状态,提交后自动刷新)
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.name" placeholder="请输入用户名" />
    <input v-model.number="form.age" type="number" placeholder="请输入年龄" />
    <button type="submit" :disabled="createUser.loading">提交</button>
    <div v-if="createUser.error" class="error">{{ createUser.error.message }}</div>
  </form>
</template>

<script setup lang="ts">
import { createAlova, useRequest } from 'alova';
import { VueHook } from 'alova/vue';
import { z } from 'zod';
import type { AppType } from '../api/src/index';
import { ref } from 'vue';

// 复用全局Alova实例(可单独封装到utils文件夹)
const alovaInstance = createAlova({
  baseURL: 'http://localhost:3000',
  statesHook: VueHook,
  typeAssert: true
});

// 1. 定义提交请求(内置校验,与后端校验规则一致)
const createUser = useRequest(
  alovaInstance.Post<AppType['/user']['post']>('/user', {
    // 内置校验,与后端Elysia的校验规则同步
    body: z.object({ name: z.string(), age: z.number().optional() })
  }),
  {
    // 提交成功后,自动刷新用户列表请求的缓存
    update: {
      // 关联用户列表请求的key(与列表请求的key一致)
      related: 'usersList'
    }
  }
);

// 2. 表单数据(无需手动维护复杂状态)
const form = ref({
  name: '',
  age: undefined as number | undefined
});

// 3. 触发提交(自动校验,自动处理加载/错误状态)
const handleSubmit = () => {
  createUser.send(form.value);
  // 提交成功后,自动重置表单(可自定义)
  createUser.onSuccess(() => {
    form.value = { name: '', age: undefined };
  });
};
</script>

核心优势:校验规则与后端Elysia同步,无需重复编写;自动维护提交状态(加载、错误),无需手动定义;提交成功后自动刷新关联请求的缓存,无需手动调用刷新方法;前后端类型自动同步,表单数据、请求参数、返回值均有类型提示,减少bug。

重点:AlovaJSElysia 类型同步——彻底解决前后端类型割裂(普通项目必做)

这是这套技术栈的核心优势之一:后端Elysia自动生成接口类型,前端AlovaJS直接导入使用,无需手动定义接口类型、无需写接口文档,编译时就能发现字段错误,彻底解决“前端写错字段、后端返回字段不符”的痛点,这也是比 tRPC 更简洁的地方(无需写两套类型代码)。

// 后端:src/index.ts(导出接口类型,无需额外编写)
import { Elysia } from 'elysia';
// 省略其他导入和配置...

const app = new Elysia()
  .use(loggerMiddleware)
  .use(authMiddleware)
  .use(errorMiddleware)
  .use(di({ providers: [...] }))
  .use(userModule)
  .use(articleModule)
  .listen(3000);

// 关键:导出后端接口类型,前端直接导入
export type AppType = typeof app;

// 前端:src/api/request.ts(封装Alova实例,关联后端类型)
import { createAlova, AlovaOptions } from 'alova';
import { VueHook } from 'alova/vue';
// 导入后端接口类型
import type { AppType } from '../../api/src/index';

// 定义Alova实例,关联后端类型
type ElysiaAppType = AppType;
type AlovaRequestConfig = AlovaOptions<ElysiaAppType>;

export const alovaInstance = createAlova<ElysiaAppType>({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000',
  statesHook: VueHook,
  typeAssert: true, // 开启类型断言,确保前后端类型一致
  // 全局请求拦截器(添加Token)
  requestInterceptor: (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  // 全局错误拦截器(统一处理错误)
  responseInterceptor: (response) => {
    if (response.status === 401) {
      // 未授权,跳转登录页
      window.location.href = '/login';
    }
    return response;
  }
});

// 前端:src/views/UserList.vue(使用类型同步的请求)
<script setup lang="ts">
import { useRequest } from 'alova';
import { alovaInstance } from '../api/request';

// 1. 查询用户列表(自动获得类型提示,返回值类型与后端一致)
const usersList = useRequest(
  alovaInstance.Get('/user'),
  {
    key: 'usersList' // 与提交请求的related关联,用于自动刷新
  }
);

// 2. 查询单个用户(params类型自动推导,返回值类型与后端一致)
const getUser = useRequest(
  alovaInstance.Get('/user/:id', {
    params: { id: '1' } // params.id自动提示为string类型
  })
);

// 3. 提交用户(body类型自动校验,与后端Elysia的校验规则一致)
const createUser = useRequest(
  alovaInstance.Post('/user', {
    body: { name: '张三', age: 25 } // body自动提示为{ name: string; age?: number }
  })
);
</script>

说明:前后端类型同步的核心是 Elysia 自动生成接口类型(AppType),AlovaJS 无缝对接该类型,无需手动编写任何接口类型代码,就能实现“后端接口修改,前端类型自动更新”,编译时就能发现字段错误,大幅减少前后端联调成本。

本段总结

  • Axios + Vue Query:需手动衔接请求与状态,代码冗余,类型割裂,维护成本高,适合简单项目;
  • AlovaJS:与 Vue3 深度适配,实现“请求+状态+缓存+校验+类型同步”一体化,写法简洁,与 Elysia 完美联动,彻底解决前后端类型割裂问题,是前端主导全栈项目的最优请求工具。

四、整套技术栈闭环:从开发到部署,全流程极简(普通项目落地指南)

这套技术栈的核心优势的是“闭环”——从开发、调试到部署,全流程极简,无需额外配置过多工具,前端开发者可独立完成全栈开发,无需依赖后端开发者协助。下面梳理普通项目的全流程落地步骤,附关键代码和注意事项。

1. 项目初始化(一键搭建,零配置)

# 1. 初始化后端项目(Elysia + Bun)
mkdir alova-elysia-demo
cd alova-elysia-demo
mkdir api
cd api
bun init -y
bun add elysia elysia-di zod
# 新建src/index.ts、src/middleware/index.ts、src/modules/*、src/services/*(如前文代码)

# 2. 初始化前端项目(Vue3 + Vite + AlovaJS)
cd ..
npm create vite@latest web -- --template vue-ts
cd web
npm install alova alova/vue zod
# 新建src/api/request.ts(封装Alova实例)、src/views/*(如前文代码)

# 3. 配置前后端一体启动脚本(package.json)
# 后端api/package.json
"scripts": {
  "dev": "bun --watch src/index.ts",
  "build": "bun build src/index.ts --outdir ./dist"
}

# 前端web/package.json
"scripts": {
  "dev": "vite",
  "build": "vue-tsc && vite build",
  "preview": "vite preview"
}

# 根目录package.json(一键启动前后端)
"scripts": {
  "dev:api": "cd api && bun run dev",
  "dev:web": "cd web && npm run dev",
  "dev": "bun run dev:web & bun run dev:api",
  "build:api": "cd api && bun run build",
  "build:web": "cd web && npm run build",
  "build": "bun run build:api && bun run build:web"
}

2. 开发调试(热重载,无感开发)

  • 后端:bun run dev:api,修改代码毫秒级热重启,无需手动重启服务;
  • 前端:npm run dev:web,Vite热重载,修改组件实时生效;
  • 前后端联调:AlovaJS自动关联后端类型,接口参数、返回值有实时类型提示,无需Postman调试,直接在前端代码中调试即可;
  • 错误排查:全局错误中间件统一捕获后端错误,前端AlovaJS自动捕获请求错误,日志中间件记录请求详情,排查问题高效。

3. 生产部署(极简部署,无需复杂配置)

这套技术栈部署极其简单,无需配置Nginx反向代理(可选),无需额外安装Node.js,直接用Bun运行后端,前端静态文件部署到CDN或服务器即可。

# 1. 打包前后端
bun run build

# 2. 部署后端(Bun运行,无需额外依赖)
# 服务器安装Bun:curl -fsSL https://bun.sh/install | bash
cd api/dist
bun index.js # 启动后端服务,默认监听3000端口
# 可选:用PM2守护进程(避免服务意外中断)
bun add pm2 -g
pm2 start index.js --name "elysia-api"

# 3. 部署前端(静态文件,可部署到CDN或服务器)
# 方式1:部署到服务器Nginx
cp -r web/dist /usr/share/nginx/html
# 配置Nginx反向代理(可选,解决跨域)
server {
  listen 80;
  server_name your-domain.com;

  # 前端静态文件
  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html; # 解决Vue路由刷新404
  }

  # 反向代理后端接口
  location /api {
    proxy_pass http://localhost:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

# 方式2:部署到CDN(如阿里云OSS、腾讯云COS)
# 直接上传web/dist目录下的所有文件,配置CDN加速,即可访问前端

注意事项:部署时需配置环境变量(如后端接口地址、NODE_ENV等),避免硬编码;生产环境建议开启HTTPS,保障接口安全;Bun目前在部分服务器系统上兼容性一般,若部署失败,可切换为Node.js运行后端(需修改package.json脚本,安装Node.js依赖)。

五、最终选型总结:适合前端主导的全栈项目,拒绝折腾

结合前文所有对比和实操,这套 Vue3 + Elysia + Bun + AlovaJS 技术栈,核心优势是“快、轻、闭环、前端友好”,完美解决前端开发者转全栈时的痛点——无需学习复杂的后端框架、无需手动衔接各种工具、无需维护冗余代码、无需解决前后端类型割裂问题。

选型建议(按项目场景划分)

  • 个人项目、小团队项目、快速迭代的中型项目(如后台管理系统、个人博客、小型电商):优先选这套技术栈,开发效率最高,维护成本最低;
  • 大型企业级项目、微服务场景:优先选 Vue3 + NestJS + Node.js + Axios,架构更规范,生态更完善,适合多人协作;
  • 轻量接口、Serverless场景(如小程序后端、简单接口服务):优先选 HonoJS + Bun,极致轻量,启动速度快;
  • 传统后端主导的项目:优先选 Vue3 + Express + Node.js + Vue Query,兼容性强,后端开发者上手无压力。

最后,这套技术栈不是“银弹”,但对于前端主导的全栈项目来说,是“最不折腾”的选择——一个人就能搞定从开发到部署的全流程,无需依赖后端开发者,无需配置一堆工具,专注业务开发,这才是全栈开发的核心意义。

补充:整套技术栈标准目录结构(贴合实操,可直接复用)

结合前文项目初始化步骤和代码示例,以下是标准化目录结构,按“前后端分离+模块化+分层架构”设计,适配个人项目、小团队项目及中型后台系统,可直接复制使用,无需额外调整。

# 根目录(项目总入口,统一管理前后端脚本)
alova-elysia-demo/
├── package.json          # 根目录脚本(一键启动/打包前后端)
├── .gitignore            # 全局忽略文件(node_modules、dist等)
├── api/                  # 后端项目(Elysia + Bun)
│   ├── package.json      # 后端依赖及脚本(dev/build)
│   ├── bun.lockb         # Bun依赖锁文件(替代package-lock.json)
│   ├── src/
│   │   ├── index.ts      # 后端入口文件(注册中间件、模块、DI依赖)
│   │   ├── middleware/   # 全局中间件目录(日志、权限、错误处理)
│   │   │   └── index.ts  # 中间件统一导出(方便入口文件导入)
│   │   ├── modules/      # 业务模块目录(按业务拆分)
│   │   │   ├── user.ts   # 用户模块(路由+接口逻辑)
│   │   │   ├── article.ts# 文章模块(路由+接口逻辑)
│   │   │   └── user/     # 复杂模块可嵌套子模块(示例:用户模块拆分)
│   │   │       ├── auth.ts  # 用户认证子模块(登录/登出)
│   │   │       └── info.ts  # 用户信息子模块(查询/修改)
│   │   ├── services/     # 业务服务层(封装业务逻辑,供模块调用)
│   │   │   ├── user.service.ts  # 用户服务(查询/创建/删除用户)
│   │   │   └── log.service.ts   # 日志服务(业务日志记录)
│   │   ├── repositories/ # 数据访问层(封装数据库操作,解耦业务)
│   │   │   └── user.repository.ts # 用户数据访问(模拟/对接数据库)
│   │   └── types/        # 额外类型定义目录(可选,复杂项目用)
│   │       └── index.ts  # 自定义类型统一导出
│   └── dist/             # 后端打包目录(bun build生成)
│       └── index.js      # 后端打包产物(生产环境运行)
└── web/                  # 前端项目(Vue3 + AlovaJS + Vite)
    ├── package.json      # 前端依赖及脚本(dev/build/preview)
    ├── package-lock.json # npm依赖锁文件
    ├── vite.config.ts    # Vite配置(可选,如跨域、别名)
    ├── tsconfig.json     # TypeScript配置
    ├── src/
    │   ├── main.ts       # 前端入口文件(创建Vue实例)
    │   ├── App.vue       # 根组件
    │   ├── api/          # 接口请求目录(Alova封装)
    │   │   └── request.ts# Alova实例封装(关联后端类型、全局拦截)
    │   ├── views/        # 页面组件目录
    │   │   ├── UserList.vue # 用户列表页面
    │   │   └── UserForm.vue # 用户新增/编辑页面
    │   ├── components/   # 公共组件目录(可复用组件)
    │   │   ├── Loading.vue # 加载组件
    │   │   └── ErrorTip.vue # 错误提示组件
    │   ├── router/       # 路由配置目录(可选,多页面用)
    │   │   └── index.ts  # 路由定义
    │   ├── store/        # 全局状态管理(可选,复杂项目用)
    │   ├── utils/        # 工具函数目录(可选,如格式化、本地存储)
    │   └── types/        # 前端类型目录(可选,关联后端类型)
    │       └── index.ts  # 导入后端AppType,统一导出
    ├── public/           # 静态资源目录(图片、图标等)
    └── dist/             # 前端打包目录(vite build生成)
        ├── index.html    # 前端入口HTML
        ├── assets/       # 打包后的静态资源(JS/CSS/图片)
        └── favicon.ico   # 网站图标

目录说明:

  • 层级清晰:按“根目录→前后端分离→模块/分层”设计,后续新增业务(如订单模块),只需在对应目录下新建文件,无需调整整体结构;
  • 贴合实操:与前文代码示例完全对应(如middleware、modules、services目录),复制目录后可直接粘贴前文代码,快速启动项目;
  • 可扩展性强:支持模块嵌套(如用户模块拆分子模块)、分层架构(服务层+数据访问层),可根据项目规模灵活增减目录(如小型项目可删除repositories目录,直接在services中处理数据);
  • 规范统一:前后端目录命名、结构保持一致,便于维护和协作,即使多人开发也能快速上手。