我的博客项目架构 - Tanstack Start与依赖注入实战

这个博客项目其实花了挺多时间开发的，从一开始的一知半解到现在学了很多东西。整个过程中边开发边学，遇到不懂的就问 AI，而这期间我收获最大的就是对项目架构的理解——讲究一个高内聚，低耦合。而实现这一目标的关键手段，就是依赖注入 (Dependency Injection) 模式。这两个概念说起来其实是一个道理，带来的好处显而易见：代码可读性高、可维护性强、容易测试。接下来，我就详细说说在这个博客项目中，我是如何利用新一代全栈框架 Tanstack Start 来实践这套架构的。核心亮点：Tanstack Start 中间件 Tanstack Start 作为一个全栈框架，我觉得最亮眼的功能之一就是它的中间件 (Middleware) 系统。它不仅灵活，更是整个代码结构的中心。在我的架构中，我利用它作为依赖注入的载体，实现了优雅的上下文 (Context) 传递。什么是依赖注入？依赖注入说白了很简单：当你写一个函数时，把这个函数运行所依赖的东西（比如数据库连接、用户 Session）作为参数传进去，而不是在函数内部自己去创建或获取。我们将这些依赖参数统称为 Context。例如，下面这个函数明确声明它需要一个 DbContext 才能运行： export async function findPostBySlug( context: DbContext, data: { slug: string }, ) { // ... 业务逻辑 } 这样做的好处是，如果你想测试这个函数，只需要 Mock 一下这个 context 依赖就行了，完全不需要启动真实的数据库。架构概览：三层架构与目录结构在深入代码之前，先看看整体的分层设计。主要分为三层： API 层 (Interface Layer)：定义 Server Functions（对外接口），负责参数校验、调用服务层。 Service 层 (Business Logic Layer)：定义核心业务逻辑。 Repo 层 (Data Access Layer)：负责原始数据的查询与持久化。在目录结构上，我使用了 Feature-based 的组织方式，每个功能模块自包含这三层： src/ ├── features/ │ ├── posts/ # 文章管理模块 │ │ ├── api/ # Server Functions (对外接口) │ │ ├── data/ # Repo 层 (Drizzle 查询) │ │ ├── posts.service.ts # Service 层 (业务逻辑) │ │ ├── posts.schema.ts # Zod Schema (定义数据形状) │ │ ├── components/ # 功能专属组件实战实现 1. 定义入口与基础 Context 一切的起点是请求的入口。Tanstack Start 提供了一个入口 Handler，这是定义基础 Context 的绝佳位置。由于我部署在 Cloudflare Workers 上，env 和 executionCtx 是全局通用的依赖，必须在这里注入。 src/server-entry.ts： import handler from "@tanstack/react-start/server-entry"; // 定义好Context的类型，以便在Server Function中获取类型提示 declare module "@tanstack/react-start" { interface Register { server: { requestContext: { env: Env; executionCtx: ExecutionContext; }; }; } } export default { fetch(request, env, ctx) { return handler.fetch(request, { context: { env: env, executionCtx: ctx, }, }); }, } satisfies ExportedHandler; 2. 类型系统的基石为了开发时的类型提示和代码规范，我定义了一系列全局的 Context 类型。 src/global.d.ts： import type { DB as DBType } from "@/lib/db"; import type { Auth as AuthType, Session as SessionType, } from "@/lib/auth/auth.server"; declare global { type DB = DBType; type Auth = AuthType; type Session = SessionType; type BaseContext = { env: Env; }; type DbContext = BaseContext & { db: DB; }; // 包含用户会话的 Context type SessionContext = DbContext & { auth: Auth; session: Session | null; }; // 强制要求已登录的 Context type AuthContext = Omit & { session: Session; }; } 3. Repo 层：数据访问让我们从数据层写起。这里是一个获取文章列表的底层函数，它显式依赖 db: DB。 src/features/posts/data/posts.data.ts： export async function getPosts( db: DB, options: { offset?: number; limit?: number; // ... 其他筛选参数 } = {}, ) { const { limit = DEFAULT_PAGE_SIZE, offset = 0, ...filters } = options; // 构建查询条件... const posts = await db .select({ id: PostsTable.id, title: PostsTable.title, // ... }) .from(PostsTable) .limit(limit) .offset(offset); // ... return posts; } 4. Service 层：业务逻辑服务层负责编排业务流程。它接收 DbContext，并调用 Repo 层的数据方法。 src/features/posts/posts.service.ts： import * as PostRepo from "@/features/posts/data/posts.data"; export async function getPosts(context: DbContext, data: GetPostsInput) { // 这里可以处理额外的业务逻辑，比如日志、缓存检查等 return await PostRepo.getPosts(context.db, { offset: data.offset ?? 0, limit: data.limit ?? 10, status: data.status, // ... }); } 这里的 GetPostsInput 类型复用了 Schema 定义，确保前后端类型一致： src/features/posts/posts.schema.ts： export const GetPostsInputSchema = z.object({ offset: z.number().optional(), limit: z.number().optional(), status: z.custom().optional(), // ... }); export type GetPostsInput = z.infer; 5. Middleware：注入的核心这是最关键的一步。我们的 server-entry 只提供了 env，但 Service 层需要 db。我们使用中间件来“升级” Context。 src/lib/middlewares.ts： import { createMiddleware } from "@tanstack/react-start"; import { getDb } from "@/lib/db"; export const dbMiddleware = createMiddleware({ type: "function" }).server( async ({ next, context }) => { // 利用 env 初始化数据库连接 const db = getDb(context.env); // 将 db 注入到下一个环节的 context 中 return next({ context: { db, }, }); }, ); 6. API 层：闭环最后，在 API 层（Server Function），我们将中间件、Validator 和 Handler 组合在一起。 API 定义： import { createServerFn } from "@tanstack/react-start"; import { GetPostsInputSchema } from "@/features/posts/posts.schema"; import * as PostService from "@/features/posts/posts.service"; export const getPostsFn = createServerFn() .middleware([dbMiddleware]) // 1. 注入 DB 依赖 .inputValidator(GetPostsInputSchema) // 2. 校验输入参数 .handler(({ data, context }) => { // 3. 这里的 context 已经包含了 db，可以安全传入 Service return PostService.getPosts(context, data); }); 进阶：扩展性展示这套模式的扩展性非常强。比如，如果你需要处理用户鉴权，可以继续叠加中间件： export const sessionMiddleware = createMiddleware({ type: "function" }) .middleware([dbMiddleware]) // 依赖 dbMiddleware .server(async ({ next, context }) => { const auth = getAuth({ db: context.db, env: context.env, }); const session = await auth.api.getSession({ headers: getRequestHeaders(), }); return next({ context: { auth, session, // 又注入了 session }, }); }); 你甚至可以链式调用多个中间件，分别处理日志、鉴权、缓存等逻辑。总结通过这套架构，我们成功实现了：高内聚：业务逻辑集中在 Service 层，数据访问集中在 Repo 层。低耦合：各层之间通过显式的 Context 和参数依赖，不直接依赖全局状态或具体实现。极致的开发体验：利用 Typescript 和 Zod 实现了端到端的类型安全，代码提示无敌。这套“打法”虽然在初期搭建时稍微繁琐一点，但对于长期维护和测试来说，绝对是值得投入的。这不仅是代码的组织方式，更是思维方式的升级。