使用 honojs + appWrite 构建后端服务

Xayah,hara-projectie项目

Honojs 相当于 Express,是一个跨平台、轻量级的高性能 Web 框架

Next.js 中使用 Hono

官网说明需要在 app/api/[[...route]]/route.ts 写如下代码:

import { Hono } from 'hono'  
import { handle } from 'hono/vercel'  
  
const app = new Hono().basePath('/api')  
  
const routes = app
	.route("/auth", auth) // 配置路由路径
	
// 为app声明各种请求指令
export const GET = handle(app)  
export const POST = handle(app)  
export const PUT = handle(app)  
export const DELETE = handle(app)
 
export type AppType = typeof routes
 
// 以上代码一般情况写完就不用动了

但把后端代码插进前端代码文件会显得混乱,会导致不必要的热更新,除了声明以外的其他后端业务代码放在根目录下的 server 目录会更好

各种请求响应和处理逻辑

一般写在 server/(类似user)/index.ts 下,指该模块下的所有 api

import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { loginSchema, registSchema } from "@/data/schemas";
// ↑ 指的是数据类型们
 
const app=new Hono()
	.post("/login", zValidator("json",loginSchema),
	                // ↑ 检查数据类型
	    async(c)=>{
	        // ...相关逻辑
	        return c.json({ ... })
	    }
	)
	.post("/regist", zValidator("json",registSchema),
	    async(c)=>{
	        // ...相关逻辑
	        return c.json({ ... })
	    }
	);
 
export default app;

设置客户端 api 调用逻辑

一般写在 app/lib/rpc.ts 文件中

import { hc } from "hono/client";
import { AppType } from "@/app/api/[[...route]]/route";
 
export const client = hc<AppType>(process.env.NEXT_PUBLIC_APP_URL!);
// http://localhost:3000

注意这个 NEXT_PUBLIC_APP_URL 写于 .env.local

定义自定义 hook use-xxx

例如:use-login 处理用户登录操作,使用 @tanstack/react-query 库中的 useMutation 钩子来管理异步的登录请求

import { useMutation } from "@tanstack/react-query";
import { InferRequestType, InferResponseType } from "hono";
// 用于推断请求和响应的类型
import { client } from "@/lib/rpc";
 
type ResponseType = InferResponseType<typeof client.api.auth.login["$post"]>;
type RequestType = InferRequestType<typeof client.api.auth.login["$post"]>;
 
export const useLogin = () => {
    const mutation = useMutation<ResponseType,Error,RequestType>({
        mutationFn: async ({ json }) => {
            const response = await client.api.auth.login["$post"]({ json });
            // 即将接收的json作为参数发送POST请求
            return await response.json();
        },
    })
    return mutation;
    // 包含了mutate/isLoading/isError等,可以在组件中使用这些状态和方法来处理操作辣
}

现在开始在组件中使用它们吧!

例如:在 app/features/components/sign-in-card.tsx 使用

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
 
import { useLogin } from "../api/use-login"
import { loginSchema } from "../schemas"
...
 
export const SignInCard = () => {
	const { mutate } = useLogin(); // 提出mutate方法
	
	// 和shadcn的form组件配合使用,仍然出现了zod
	const form=useForm<z.infer<typeof loginSchema>>({
	    resolver: zodResolver(loginSchema),
	    defaultValues: {
	        email: "",
	        password: ""
	    }
	})
	
	const onSubmit=(values:z.infer<typeof loginSchema>){
	    mutate({json: values})
	}
	
	return (
	    // ...
	)
}

获取并处理 appwrite 的错误

完善业务逻辑的时候,发现如果出现参数错误以及 appwrite 发现的类似“账号或密码不正确”的报错,客户端会没有任何反应或者只出现 Internet Server Error,查了一圈文档知道了是因为没有处理来自 appwrite 返回的错误信息。

// 原代码:
.post(
    "/login",
    zValidator("json", loginSchema),
    async (c) => {
        ...
        return c.json({ success: true, message: "Login successful" });
    }
)

// 导入
import { AppwriteException } from "node-appwrite"
// 使用try-catch加上错误处理:
.post(
    "/login",
    zValidator("json", loginSchema),
    async (c) => {
        try {
            ...
            return c.json({success: true, message: "Login successful"});
        } catch (error) {
            if (error instanceof AppwriteException) {
                return c.json({success: false, message: error.message}, {status: error.code});
            }
            console.error("Unexpected error:", error);
            return c.json({ success: false, message: "An unexpected error occurred" }, { status: 500 });
        }
    }
)

或者另外写一个 error-handler.tslib 里,会更简洁:

import { AppwriteException } from "node-appwrite";
export const ErrorHandler = (error: unknown) => {
    if (error instanceof AppwriteException) {
      return { success: false, message: error.message, statusCode: error.code };
    }
    // 处理其他未知错误
    console.error("Unexpected error:", error);
    return { success: false, message: "An unexpected error occurred", statusCode: 500 };
};

导入使用: Local Image

使用 sonner 通知组件,成功或者失败都会呼出提示信息,这里我选择使用 useEffect 来监听 data.success 的值(data 是在定义 api 时候 useMutation 钩子赋予的属性 不确定!! 待考证!!,指的是响应的 json 内容)

// 感觉哪里怪怪的...待优化(
useEffect(()=>{
  if(status==="pending") setIsLoading(true)
  else if(status!== "idle") {
    setIsLoading(false)
    if(!data?.success) {
      toast.error(data?.message)
    }else {
      toast.success(data?.message)
    }
  }
},[status,data])

为什么不用 nextjs 自带的 api route?

Nextjs 的 API Routes 是基于 nodejs 的,不如 Hono 框架轻量,而且 Hono 支持浏览器运行环境,使得 API 请求可以更接近用户地理位置被处理,从而提升响应速度,比 Nextjs 更适合在服务器上运行。

最重要的一点是,使用 Hono 可以与 TS 结合得更加紧密,使用类型约束来确保前后端的类型安全,保证使用客户端调用 api 时不会传递错误的参数类型,虽然 nextjs 也可以使用 TS,但 Hono 提供的 hc 客户端会更优雅地处理类型定义和调用 API。

碎碎念

国外的服务器真的慢死了...现在好后悔不用传统的express来自己搭建后端

© Xayah.RSS