作為一名前端主導的全端開發者,結合近兩年的實戰經驗(涵蓋個人專案、小團隊協作專案及中型後台系統),我總結出一套經過多場景驗證的最佳實踐:Vue3 + Elysia + Bun + AlovaJS。過去一年,我踩過不少技術選型的坑:
直到我切換到 Vue3 + Elysia + Bun + AlovaJS 這個組合,才真正體會到「全端閉環」的快樂——沒有多餘的工具、沒有冗餘的程式碼、沒有型別割裂的痛苦,開發速度翻倍,維護成本驟降。今天就詳細聊聊,我為何最終選擇了這套技術棧,每一個選型都附上真實程式碼比較與客觀優缺點分析,讓你一眼看懂差距;同時針對普通專案必備的全域中介軟體(日誌、權限、錯誤處理)、強模組化與 DI(依賴注入),說明 Elysia 中的具體實作,解決大家最關心的實作問題。
在接觸 Bun 之前,我一直以 Node.js 作為全端執行時,兩者各有優劣,並非 Bun 完勝、Node.js 全差。以下從體積、速度、生態、設定四個核心維度,做客觀比較,方便依專案場景選擇。
補充:Bun 與 Node.js 客觀對比表
| 對比維度 | Bun | Node.js |
|---|---|---|
| 體積(安裝包) | 約 100MB(單檔可執行程式,內建所有核心工具) | 約 20–60MB(僅執行時,工具需額外安裝) |
| 啟動速度 | 極快(毫秒級啟動,比 Node.js 快 3–5 倍) | 中等(啟動延遲明顯,大型專案啟動需數秒) |
| 套件安裝速度 | 極快(是 npm 的 30–100 倍,快取機制更優) | 較慢(大型專案依賴安裝需 2–5 分鐘,易出現相依衝突) |
| TypeScript 支援 | 原生支援,零設定可執行 .ts 檔案 | 需額外安裝 ts-node、typescript,並設定 tsconfig.json |
| 熱重啟 | 內建 --watch 命令,毫秒級熱重啟 | 需額外安裝 nodemon,重啟延遲 300ms–2s,偶爾失效 |
| 生態成熟度 | 較新,生態尚未完全成熟,部分 Node.js 套件無法直接使用 | 極其成熟,套件豐富(express、koa 等),社群支援強 |
| 相容性 | 部分 Node.js API 尚未完全相容,遷移需做適配 | 相容性強,幾乎支援所有前端/後端工具,無適配成本 |
| 優勢 | 速度快、零設定、一體化工具鍊,適合快速開發 | 生態完善、相容性強、穩定可靠,適合大型生產專案 |
| 劣勢 | 生態不完善,部分場景需手動適配,穩定性需長期驗證 | 設定繁瑣、速度較慢,工具鍊分散,開發準備成本高 |
對比 1:安裝相依套件速度——天差地別(客觀補充劣勢)
npm install
# 大型專案相依安裝,動輒 2~5 分鐘,偶爾還會卡住、相依衝突
# 每次重新安裝,都要喝一杯水等它結束
# 優勢:相容性極強,所有 npm 包都能正常使用,無適配成本
bun install
# 同樣的相依,10 秒內完成,速度是 npm 的 30~100 倍
# 再也不用為了裝相依浪費時間
# 劣勢:部分冷門 npm 套件可能無法正常安裝,需手動處理相依相容
對比 2:執行 TypeScript 後端——零設定 vs 一堆設定(客觀補充)
# 第一步:安裝相依
npm install ts-node typescript @types/node
# 第二步:配置 tsconfig.json
# 第三步:執行
npx ts-node src/index.ts
# 優勢:tsconfig 可按需配置,支援複雜的 TS 編譯規則,適配大型專案
不僅要裝一堆相依,還要手動設定 tsconfig,稍微寫錯一個設定就會報錯無法執行,新手很容易卡在這裡。
bun src/index.ts
# 無需裝任何轉譯工具,無需配置 tsconfig
# 直接執行 .ts 檔案,毫秒級啟動
# 劣勢:無法自訂 TypeScript 編譯規則,複雜 TS 場景(如裝飾器、模組解析)適配度一般
對比 3:熱重啟開發——無感 vs 延遲(客觀補充)
// package.json 範例
"scripts": {
"dev": "nodemon src/index.ts"
}
每次儲存程式,都要等 300ms~2s 才能重啟,偶爾還會出現「儲存了但不重啟」的 bug,開發節奏被頻繁打斷。
// package.json 範例
"scripts": {
"dev": "bun --watch src/index.ts"
}
# 劣勢:熱重啟規則簡單,無法自訂忽略檔案,部分複雜專案場景適配不足
儲存程式的瞬間,後端就完成重啟,幾乎沒有延遲,開發時完全感覺不到等待,專注度大幅提升。
對比 4:前後端一體啟動——複雜腳本 vs 簡單命令(客觀補充)
# 第一步:安裝 concurrently
npm install concurrently --save-dev
# 第二步:配置 scripts
"scripts": {
"dev:web": "vite",
"dev:api": "nodemon src/index.ts",
"dev": "concurrently \"npm run dev:web\" \"npm run dev:api\""
}
# 優勢:可分別控制前後端啟動順序、日誌輸出,支援複雜啟動邏輯
不僅要多裝一個相依,還要寫複雜腳本,偶爾還會出現「一個服務啟動失敗,另一個正常運作」的問題,排查麻煩。
"scripts": {
"dev:web": "bun run vite",
"dev:api": "bun --watch src/index.ts",
"dev": "bun run dev:web & bun run dev:api"
}
# 劣勢:無法控制前後端啟動順序,日誌輸出混在一起,排查問題不便
無需安裝任何額外相依,一條命令就能同時啟動前後端,熱重載各自獨立,互不干擾,開發體驗直接拉滿。
本段總結:
後端框架我試過 Express、NestJS 和 HonoJS,四者各有定位,並非 Elysia 完勝。以下先補充四者體積對比,再分維度客觀比較,結合普通專案需求給出選型建議。
補充:4 個後端框架體積對比表(生產環境打包後)
| 框架名稱 | 體積(minified + gzip) | 核心依賴體積 | 備註 |
|---|---|---|---|
| Elysia | 約 15KB | 無額外核心依賴(zod 為可選驗證依賴,約 10KB) | 輕量極致,內建型別、校驗、中介軟體等核心功能 |
| HonoJS | 約 12KB | 無額外核心依賴 | 更輕,但基礎特性需額外封裝 |
| Express | 約 28KB | 依賴 path-to-regexp、send 等,合計約 8KB | 基礎框架輕,但核心功能(校驗、型別)需額外安裝套件 |
| NestJS | 約 150KB+ | 依賴 @nestjs/core、reflect-metadata 等,合計約 100KB+ | 重量級框架,內建企業級特性,體積最大 |
核心總結:體積排序(從小到大):HonoJS、Elysia、Express、NestJS;Elysia 兼顧輕量與功能完整性,是前端主導全端專案的均衡選擇。
對比 1:寫一個帶型別的介面——手動維護 vs 自動推導(覆蓋 4 個框架,客觀優劣)
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。
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 完善,複雜企業級場景需手動適配
後端寫介面時,params、body 自動有型別提示;前端只需匯入 AppType,就能獲得所有介面的型別,無需手動維護 DTO、無需寫介面文件,編譯時就能發現欄位錯誤,徹底解決前後端型別割裂問題。
// 第一步:建立模組(必須建立模組,否則無法執行)
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 型別,維護成本翻倍,前端開發者上手難度極高。
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 個框架,客觀優劣)
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,相依還要手動寫驗證邏輯與錯誤處理,程式碼冗餘,驗證邏輯與介面邏輯混在一起,維護麻煩。
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 錯誤,程式碼簡潔、邏輯清晰,前端也能透過型別推導得知需傳入哪些參數。
# 第一步:安裝驗證相依(必須安裝,否則無法實現驗證)
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 };
}
}
// 第三步:啟動專案(省略主模組代碼,與上文一致)
// 優勢:驗證體系完善,支援裝飾器語法,可整合全域驗證,適合大型專案
// 劣勢:需額外安裝兩個相依,設定繁瑣,修改驗證規則需改動多處
NestJS 實作介面驗證需額外安裝相依,手動定義 DTO 類別並加驗證裝飾器,還要手動啟用驗證管線,步驟繁瑣;後續修改驗證規則,需同時修改 DTO 與介面邏輯,維護成本高,不符合前端開發者「簡潔高效」的需求。
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 並撰寫驗證邏輯;且驗證邏輯與介面邏輯混在一起,沒有 Elysia 那樣「驗證與介面分離」的設計,維護上沒那麼便捷。
對比 3:全域中介軟體、模組化、DI(依賴注入)(普通專案必備,覆蓋 4 個框架,客觀優劣)
這三個特性是普通專案(尤其是中型專案)的必備需求,也是前端開發者轉全端時最關心的「工程化能力」,下面比較四個框架的實作複雜度,重點突出 Elysia 的優勢,同時客觀呈現各框架優劣。
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 支援,全靠手動封裝或額外安裝套件,程式碼冗餘、規範混亂,不利於前端主導的全端專案快速上手。
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 的風格統一,前端開發者上手無壓力,完全貼合普通專案需求。
// 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 注入需手動寫建構子,模組化拆分繁瑣,前端開發者需要花大量時間學習框架規範,不適合中小型全端專案。
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 依賴注入,且寫法比 NestJS 更簡潔,前端開發者更容易上手,也比 HonoJS 更省心(無需額外封裝基礎特性)。下面直接給出可複製程式碼,落地性拉滿。
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 校驗,完全滿足普通專案需求。
普通專案隨著業務演進,路由會愈來愈多,若全部寫在一個檔案會導致程式碼臃腫、難以維護。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)
普通專案隨著業務複雜度提升,會出現「介面邏輯與業務邏輯耦合」的問題——例如多個介面都要呼叫「查詢使用者資訊」邏輯,若直接在介面中重複撰寫,日後修改時需改動所有相關介面,維護成本極高。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 原理就能快速上手使用。
本段總結:
(文中後續範例程式碼與說明在原文後半段仍有更多細節,原始內容於此處中斷。)