还在前后端分离?来试试tRPC与Next.js吧!

7,603 阅读8分钟

前言

前后端分离在目前的开发中大行其道。
传统的Java后端与Typescript前端的开发中,往往需要对后端返回的数据再创建一遍TS的类型,再封装对应的请求方法发送请求获取数据。
虽然现在有很多工具可以根据APi自动生成类型与请求方法,例如Antd Pro中的open-api插件,但还是不够完美,之前就在想,如果后端一样使用TS进行开发,是否可以将对后端的请求处理为一个函数呢? 例如后端创建一个create数据的函数。

export const createUser = (user: User) => {
    // 数据库创建用户
    return database.user.create(user)
}

前端不用再查看请求的URL、参数、返回值,而是直接调用后端创建的函数createUser

import { createUser } from '@server/userModel'
....
const Component = () => {
    return (
        <button onClick={() => createUser({...})>创建用户</button>
    )
}

只要前后端类型是一致的那就可以直接看到函数的入参与返回值,字段的注释也可以直接使用JsDoc,不用通过注解(装饰器)来指定,比去翻api文档方便太多了。

Next.js

image.png Getting Started: Project Structure | Next.js (nextjs.org)

Next.js被许多大型公司使用,通过扩展最新的React功能,您可以创建全栈Web应用程序,并集成强大的基于Rust的JavaScript工具以实现最快的构建。

首先锁定的技术就NextJs,因为不止一次在Next.js听到【全栈开发】这个词。

服务器组件与客户端组件

在Next13中,React组件分为客户端组件与服务器组件

  • 客户端组件(client component):与普通的React组件一致可以使用基本的state等功能,运行在浏览器。
  • 服务器组件(server component):没有state或者生命周期等功能只能输出基本的JSX,但是是在服务器的Node环境中运行。

例子:通过服务器组件的引入Node的方法,读取当前开发目录与内存使用率

'use server'

import fs from 'fs'
import { cwd } from 'process';
import React from 'react';
import os from 'os'

const ServerComponent: React.FC<React.PropsWithChildren> = ({ children }) =>{
  const paths = fs.readdirSync(cwd())
  return (
    <>
      <div>内存使用率{ os.freemem()/os.totalmem() }</div>
      <div>
        {paths.map(_ => <div key={_}>{ _ }</div>)}
      </div>
      {children}
    </>
  )
}

export default ServerComponent

image.png

当我们右键点击查看源代码可以看到,这些数据已经在返回的html中而不是通过前端代码渲染出来的,这就是常说的服务端渲染(Server Side Render)。

image.png 这种写法让我想起了以前学习JavaWeb时学习的JSP或者SpringBoot的Thymeleaf模板语法,这也是前后端分离出现之前Web最常规的开发方式。

那你可能会问了,这样和JSP有什么区别呢,服务端组件也只是一个基于JSX的模板语法罢了。

首先当然是同一种语言方便维护,使用JSP稍微复杂一点的页面就需要操作Dom或者Jquery来实现,增加维护成本。其次,服务端组件和客户端组件是可以进行嵌套的!虽然有一定的限制,但是相比传统模板语法灵活度非常的高,

image.png

JSP如果需要对数据进行更新删改就需要刷新页面,体验极差。ajax异步请求是前后端能够分离的重要因素。

Server Action

还记得开头提到的问题吗?Next.js中有一个完美的解决方案:Server Action

Server Functions:函数在服务器运行,但是可以在客户端调用

image.png

不过该方案目前还是实验性质的,只能简单先玩一玩。

将一个async函数中标记 'use server' 这个函数即为 server action

export const getServerMemo = async (str: string) => {
  'use server'
  return `${str}${os.totalmem()}`
}

客户端组件可以直接调用这个函数获取数据

const Button: FC<PropsWithChildren> = ({ children }) => {
  const [memo, setMemo] = useState<string>()
  useEffect(() => {
    getServerMemo('内存为:').then(res => {
      setMemo(res);
    })
  })

  return <div>
    <h1>{ memo }</h1>
  </div>;
};

}

image.png

打开网络调试可以看到实现原理其实就是发送了一次HTTP请求,这也就意味了这个函数的入参与返回值必须是能被序列化的值,其他的例如函数就没办法作为入参,还是有一定的局限性。 image.png

tRPC

前端的同学可能对RPC比较陌生,在后端与大数据分布式架构中RPC起到了重要的作用

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信的方式,它是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不是程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

tRPC官网:trpc.io/

tRPC允许您轻松构建和使用完全类型安全的API,无需模式或代码生成。随着TypeScript和静态类型成为Web开发的最佳实践,API契约成为一个主要问题。我们需要更好的方式来静态类型化我们的API端点,并在客户端和服务器(或服务器之间)之间共享这些类型。我们致力于构建一个简单的库,用于构建类型安全的API,利用现代TypeScript的完整功能。

通过介绍可以看出,他的思想和NextJs新推出的Server Action差不多。如果我们直接使用Server Action为了项目的维护肯定也是要做一些封装的,那不如直接就用tRPC为我们定义好的功能与规范。

在github中的Star历史可以看到,2022年开始一年半的时间涨了超过20K的stars。 image.png

写一个简单的Demo

还是实现之前的读取服务器内存的功能

定义procedure

import { TRPCError, initTRPC } from "@trpc/server";
import os from "os";

// 初始化tRPC
const { procedure, router } = initTRPC.create();

// 创建路由
export const appRouter = router({
  // 定义一个路由/过程
  getServerMemo: procedure
    // 可以在这里对输入进行效验,返回值类型就是客户端调用的入参类型
    .input((value: unknown): string => {
      if (typeof value === "string" && value.includes("内存")) {
        // 返回类型为string
        return value;
      } else {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "输入内容包含 [内存] 的字符串!",
        });
      }
    })
    .query(async (opt) => {
      // 这里的opt.input的类型被自动推断为string
      return `${opt.input}${os.totalmem()}`;
    }),
});

// 导出类型给客户端使用
export type AppRouter = typeof appRouter;

client调用

首先在客户端创建tRPC实例,指定数据传输使用 http://localhost:3000/api/trpc 这个URL

"use client";
import React, { FC, PropsWithChildren, useEffect, useState } from "react";
import "./index.css";
import { createTRPCNext } from "@trpc/next";
import { httpBatchLink } from "@trpc/client";
/**
 * 这里我们可以看到导入的只有类型,并没有appRouter的实例
 * 我们知道在TS编译为JS时这种类型是不会被编译的
 */
import type { AppRouter } from "../../server/demo";

// 创建客户端tRPC实例
export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      links: [
        // 我们使用http作为传输的载体
        httpBatchLink({
          url: `http://localhost:3000/api/trpc`,
        }),
      ],
    };
  },
});

const Button: FC<PropsWithChildren> = ({ children }) => {
  // tRPC/Next中集成了 react-query 帮我们封装好了hooks 方便调用
  const { data } = trpc.getServerMemo.useQuery("内存为:");
  return (
    <div>
      <h1>{data}</h1>
    </div>
  );
};
// 通过tRPC提供的HOC对组件进行包裹
export default trpc.withTRPC(Button);

前后端桥梁

此时我们访问 http://localhost:3000/api/trpc 其实还什么都没有,需要借助Next的api能力进行创建并对tRPC进行处理。

tRPC本质上还是发送的网络请求,所以需要一种协议来支持网络的传输,目前可以有websocket和http,这个是框架无关的,你可以自己通过node的http相关api来创建,也可以通过Next提供的api功能来创建。如果使用websocket,因为Next没有提供websocket能力所以需要自己启一个websocket服务器。

注意:这里创建api使用的是App文件目录,如果是老版本请参考pages的文件目录

app/api/trpc/[trpc] 中创建route.ts文件,定义api

nextjs.org/docs/app/bu…

import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "../../../../server/demo";

// 处理请求
const handler = (request: Request, context: any) => {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    req: request,
    router: appRouter,
    createContext: (opts) => ({}),
  });
};
// 导出为POST和GET方法
// 详情请参考Next官方文档 https://nextjs.org/docs/app/building-your-application/routing/router-handlers
export const GET = handler;
export const POST = handler;

测试

首先看看正常的效果 image.png
完美!

再看看能不能正常校验类型,我们入参传入一个number看看TS是否会报错 image.png

如果入参内容不包含【内存】是否会报错 image.png

image.png

其他

除了procedure中除了可以定义query(查询数据)外,还可以定义mutation(用来改变数据,例如数据库的增删改操作)与subscription(类似websocket的长连接,但是传输协议也必须是websocket)。

同时tRPC也并不是和Next.js强绑定的,纯客户端渲染的项目也可以使用tRPC,只要前后端使用monorepo就可以使用。此外,tRPC也可以直接创建为Restful api服务器。

总结与扩展

对于小型应用来说这种开发方法是很不错的,相比于php等开发方法也更符合前端开发人员的习惯。

前端程序猿有时候也可以不仅限于对浏览器的钻研,Node的出现让我们可以把视野放的更长远了解更多领域的知识,在前端这些卷王的努力下,Node的地位相信也会越来越高,只有不断的学习才不会被时代给淘汰。

说到后端开发,就不得不提CRUD,现在Node的ORM框架生态也很丰富,例如Prisma,它对Typescript的支持也非常的好,Next.js+tRPC+Prisma 尝试了一下开发起来非常舒服。不用再手写sql语句,甚至不用定义实例类,Prisma都帮你搞定。 www.prisma.io/