发布于: -/最后更新: -/6 分钟/

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

摘要

博客项目的开发过程中,作者学习了很多东西,尤其是对项目架构的理解,强调高内聚、低耦合的重要性。作者使用了Tanstack Start框架,利用其中间件系统实现了依赖注入,优雅地传递上下文。作者还介绍了三层架构、目录结构、类型系统的基石、Repo层、Service层、Middleware等,展示了如何利用中间件注入依赖,实现业务逻辑的封装和解耦。这种架构模式具有高内聚、低耦合、极致的开发体验等优点,适合长期维护和测试。

这个博客项目其实花了挺多时间开发的,从一开始的一知半解到现在学了很多东西。整个过程中边开发边学,遇到不懂的就问 AI,而这期间我收获最大的就是对项目架构的理解——讲究一个高内聚,低耦合

而实现这一目标的关键手段,就是依赖注入 (Dependency Injection) 模式。这两个概念说起来其实是一个道理,带来的好处显而易见:代码可读性高可维护性强容易测试

接下来,我就详细说说在这个博客项目中,我是如何利用新一代全栈框架 Tanstack Start 来实践这套架构的。

核心亮点:Tanstack Start 中间件

Tanstack Start 作为一个全栈框架,我觉得最亮眼的功能之一就是它的中间件 (Middleware) 系统。它不仅灵活,更是整个代码结构的中心。在我的架构中,我利用它作为依赖注入的载体,实现了优雅的上下文 (Context) 传递。

什么是依赖注入?

依赖注入说白了很简单:当你写一个函数时,把这个函数运行所依赖的东西(比如数据库连接、用户 Session)作为参数传进去,而不是在函数内部自己去创建或获取。我们将这些依赖参数统称为 Context

例如,下面这个函数明确声明它需要一个 DbContext 才能运行:

TypeScript
export async function findPostBySlug(
  context: DbContext,
  data: { slug: string },
) {
  // ... 业务逻辑
}

这样做的好处是,如果你想测试这个函数,只需要 Mock 一下这个 context 依赖就行了,完全不需要启动真实的数据库。

架构概览:三层架构与目录结构

在深入代码之前,先看看整体的分层设计。主要分为三层:

  1. API 层 (Interface Layer):定义 Server Functions(对外接口),负责参数校验、调用服务层。

  2. Service 层 (Business Logic Layer):定义核心业务逻辑。

  3. Repo 层 (Data Access Layer):负责原始数据的查询与持久化。

在目录结构上,我使用了 Feature-based 的组织方式,每个功能模块自包含这三层:

Plain Text
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 上,envexecutionCtx 是全局通用的依赖,必须在这里注入。

src/server-entry.ts

TypeScript
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<Env>;

2. 类型系统的基石

为了开发时的类型提示和代码规范,我定义了一系列全局的 Context 类型。

src/global.d.ts

TypeScript
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<SessionContext, "session"> & {
    session: Session;
  };
}

3. Repo 层:数据访问

让我们从数据层写起。这里是一个获取文章列表的底层函数,它显式依赖 db: DB

src/features/posts/data/posts.data.ts

TypeScript
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

TypeScript
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

TypeScript
export const GetPostsInputSchema = z.object({
  offset: z.number().optional(),
  limit: z.number().optional(),
  status: z.custom<PostStatus>().optional(),
  // ...
});
export type GetPostsInput = z.infer<typeof GetPostsInputSchema>;

5. Middleware:注入的核心

这是最关键的一步。我们的 server-entry 只提供了 env,但 Service 层需要 db。我们使用中间件来“升级” Context。

src/lib/middlewares.ts

TypeScript
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 定义

TypeScript
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);
  });

进阶:扩展性展示

这套模式的扩展性非常强。比如,如果你需要处理用户鉴权,可以继续叠加中间件:

TypeScript
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
      },
    });
  });

你甚至可以链式调用多个中间件,分别处理日志、鉴权、缓存等逻辑。

总结

通过这套架构,我们成功实现了:

  1. 高内聚:业务逻辑集中在 Service 层,数据访问集中在 Repo 层。

  2. 低耦合:各层之间通过显式的 Context 和参数依赖,不直接依赖全局状态或具体实现。

  3. 极致的开发体验:利用 Typescript 和 Zod 实现了端到端的类型安全,代码提示无敌。

这套“打法”虽然在初期搭建时稍微繁琐一点,但对于长期维护和测试来说,绝对是值得投入的。这不仅是代码的组织方式,更是思维方式的升级。

正文结束