使用 Cursor 从 0 到 1 开发一个全栈 Chatbox 项目

by ADMIN 36 views

环境准备

在正式开发前,你的设备需要有如下环境:

  • Node >= 18.18
  • pnpm 作为依赖管理工具
  • postgreSQL
  • Curosr
  • chrome 游览器

你需要具备的知识:

  • 前端基础
  • 数据库基础
  • 计算机网络基础
  • 熟悉 React 开发

当然,你还需要有良好的软件开发素养,否则你会发现写的代码不好维护,或者不易理解

项目初始化

在真正开发项目之前,让我们先进行需求分析和技术选型

需求分析

  • 聊天页面开发(基础能力)
  • 包含提示的输入框和发送/停止按钮
  • 实现一个聊天区域来显示对话记录,一个列表展示会话历史
  • 开发/agent API 来处理请求
  • 确保每个对话的数据都被持久存储
  • 通过流式传输返回所有结果
  • 高级能力
  • 增强聊天组件,支持 markdown 渲染、自动滚动、图片上传等
  • 实现函数调用,例如检索当前时间

技术选型

根据需求,我选择了我喜欢的并且也是主流的技术:

  • Next.js 作为全栈开发基础框架
  • hono.js 作为后端框架,优化在 Next.js 中后端开发体验
  • PostgreSQL 作为数据库存储对话记录
  • DrizzleORM 作为 ORM,更加便捷和高效的方式与数据库进行交互
  • shadcn/ui 作为 UI 组件库,tailwindcss 作为 css 框架
  • Vercel AI SDK 快速开发 AI 相关的服务
  • Biome 进行代码格式化和检测
  • zod:TypeScript 优先的数据验证库

对了,我们使用 Github 进行版本控制,维护代码。使用 vercel 进行项目部署上线。

初始化

下面进行初始化项目,初始化项目完成后,我们应该就可以进行业务开发

1)根据 Next.js 官方文档,我们创建一个 Next.js 项目:

npx create-next-app@latest

2)下面,根据官方文档集成 shadcn/ui:

pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button

3)下面集成 Biome,保证相关代码风格一致:

pnpm i @biomejs/biome -D

4)下面继续集成 hono.js:

pnpm i hono
# 让 hono 接管所有接口服务
mkdir -p "src/app/api/[[...route]]" && touch "src/app/api/[[...route]]/route.ts"

5)下面,开发 route.ts 内容,让 hono 来接管接口服务:

// src/app/api/[[...route]]/route.ts
import api from "@/server/api";
import { handle } from "hono/vercel";
const handler = handle(api);
export  handler as GET,
  handler as POST,
  handler as PUT,
  handler as DELETE,
  handler as PATCH,
};

6)下面,我们创建自定义校验器,它的作用是进行请求数据验证的工具函数,确保数据符合预期的格式和类型规范,并提供类型安全的验证结果:

// src/server/api/validator.ts
import type {
  Context,
  MiddlewareHandler,
  Env,
  ValidationTargets,
  TypedResponse,
  Input,
} from "hono";
import { validator } from "hono/validator";
import type { z, ZodSchema, ZodError } from "zod";

export type Hook<
  T,
  E extends Env,
  P extends string,
  Target extends keyof ValidationTargets = keyof ValidationTargets,
  // biome-ignore lint/complexity/noBannedTypes: <explanation>
  O = {}
> = (
  result: (
    | { success: true; data: T }
    | { success: false; error: ZodError; data: T }
  ) & {
    target: Target;
  },
  c: Context<E, P>
) =>
  | Response
  | void
  | TypedResponse<O>
  // biome-ignore lint/suspicious/noConfusingVoidType: <explanation>
  | Promise<Response | void | TypedResponse<O>>;

type HasUndefined<T> = undefined extends T ? true : false;

export const zValidator = <
  T extends ZodSchema,
  Target extends keyof ValidationTargets,
  E extends Env,
  P extends string,
  In = z.input<T>,
  Out = z.output<T>,
  I extends Input = {
    in: HasUndefined<In> extends true
      ? {
          [K in Target]?: K extends "json"
            ? In
            : HasUndefined<keyof ValidationTargets[K]> extends true
            ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
            : { [K2 in keyof In]: ValidationTargets[K][K2] };
        }
      : {
          [K in Target]: K extends "json"
            ? In
            : HasUndefined<keyof ValidationTargets[K]> extends true
            ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
            : { [K2 in keyof In]: ValidationTargets[K][K2] };
        };
    out: { [K in Target]: Out };
  },
  V extends I = I
>(
  target: Target,
  schema: T,
  hook?: Hook<z.infer<T>, E, P, Target>
): MiddlewareHandler<E, P, V> =>
  // @ts-expect-error not typed well
  validator(target, async (value, c) => {
    const result = await schema.safeParseAsync(value);

    if (hook) {
      const hookResult = await hook({ data: value, ...result, target }, c);
      if (hookResult) {
        if (hookResult instanceof Response) {
          return hookResult;
        }

        if ("response" in hookResult) {
          return hookResult.response;
        }
      }
    }

    if (!result.success) {
      throw result.error;
    }

    return result.data as z.infer<T>;
  });

7)下面,我们创建错误处理文件,给到客户端更好的错误:

// src/server/api/error.ts
import { z } from "zod";
import type { Context } from "hono";
import { HTTPException } from "hono/http-exception";
import type { ContentfulStatusCode } from "hono/utils/http-status";

export class ApiError extends HTTPException {
  public readonly code?: ContentfulStatusCode;
  constructor({
    code,
    message,
  }: {
    code?: ContentfulStatusCode;
    message: string;
  }) {
    super(code, { message });
    this.code = code;
  }
}

export function handleError(err: Error, c: Context): Response {
  if (err instanceof z.ZodError) {
    const firstError = err.errors[0];

    return c.json(
      { code: 422, message: `\`${firstError.path}\`: ${firstError.message}` },
      422
    );
  }

  /**
   * This is a generic error, we should log it and return a 500
   */
  return c.json(
    {
      code: 500,
      message: "服务端错误, 请稍后再试。",
    },
    { status: 500 }
  );
}

8)下面,我们创建我们的第一个接口,验证 honojs 是否引入成功:

// src/server/api/routes/hello.ts
import { Hono } from "hono";
const app = new Hono().get("/hello", (c) =>
  c.json({ message: "Hello, luckyChat" })
);
export default app;

9)下面,我们开发入口文件:

// src/server/api/index.ts
import { handleError } from "./error";
import { Hono } from "hono";
import helloRoute from "./routes/hello";
const app = new Hono().basePath("/api");
app.onError(handleError);
const routes = app.route("/", helloRoute);
export default app;
export type AppType = typeof routes;

现在我们不仅有了接口,还有了服务端接口的类型声明,我们可以非常方便的在客户端进行类型安全的接口请求,我们不需要写路由,也不需要写类型相关的内容,真的是 amazing,我们赶紧在客户端调用第一个接口吧!

10)下面,我们封装一个 fetch 方法:

// src/lib/fetch.ts
import type { AppType } from "@/server/api";
import { hc } from "hono/client";
import ky from "ky";

const baseUrl =
  process.env.NODE_ENV === "development"
    ? "http://localhost:3000"
    : process.env.NEXT_PUBLIC_APP_URL;

export const fetch = ky.extend({
  hooks: {
    afterResponse: [
      async (_, __, response: Response) => {
        if (response.ok) {
          return response;
          // biome-ignore lint/style/noUselessElse: <explanation>
<br/>
**Q&A:使用 Cursor 从 0 到 1 开发一个全栈 chatbox 项目**
=====================================================

**Q1:什么是 Cursor?**
-------------------

A1:Cursor 是一个 AI 编程工具,能够帮助开发者快速构建项目,包括前端和后端代码。

**Q2:如何使用 Cursor?**
-------------------

A2:使用 Cursor 需要先安装 Node.js 和 pnpm,接着创建一个 Next.js 项目,最后使用 Cursor 的 API 来生成项目代码。

**Q3:什么是 DrizzleORM?**
-------------------

A3:DrizzleORM 是一个 ORM(Object-Relational Mapping)工具,能够帮助开发者与数据库进行交互。

**Q4:如何使用 DrizzleORM?**
-------------------

A4:使用 DrizzleORM 需要先安装 DrizzleORM,接着创建一个数据库表结构,最后使用 DrizzleORM 的 API 来与数据库进行交互。

**Q5:什么是 Vercel AI SDK?**
-------------------

A5:Vercel AI SDK 是一个 AI SDK,能够帮助开发者快速构建 AI 相关的服务。

**Q6:如何使用 Vercel AI SDK?**
-------------------

A6:使用 Vercel AI SDK 需要先安装 Vercel AI SDK,接着使用 Vercel AI SDK 的 API 来构建 AI 相关的服务。

**Q7:什么是 Biome?**
-------------------

A7:Biome 是一个代码格式化和检测工具,能够帮助开发者保持代码的格式和质量。

**Q8:如何使用 Biome?**
-------------------

A8:使用 Biome 需要先安装 Biome,接着使用 Biome 的 API 来格式化和检测代码。

**Q9:什么是 zod?**
-------------------

A9:zod 是一个 TypeScript 优先的数据验证库,能够帮助开发者验证数据的格式和类型。

**Q10:如何使用 zod?**
-------------------

A10:使用 zod 需要先安装 zod,接着使用 zod 的 API 来验证数据的格式和类型。

**Q11:如何优化项目?**
-------------------

A11:优化项目需要考虑多个方面,包括代码质量、性能、安全性等。

**Q12:如何测试项目?**
-------------------

A12:测试项目需要使用测试框架和工具,例如 Jest 和 Cypress 等。

**Q13:如何部署项目?**
-------------------

A13:部署项目需要使用部署工具和平台,例如 Vercel 和 Netlify 等。

**Q14:如何维护项目?**
-------------------

A14:维护项目需要持续更新和维护代码,包括 bug 修复和新功能添加等。

**Q15:如何使用 AI 在项目开发中?**
-------------------

A15:使用 AI 在项目开发中需要使用 AI SDK 和工具,例如 Vercel AI SDK 和 Biome 等。