亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

NextJS構建API的那些坑:它更適合當個BFF

標簽:
API 疑難雜癥

BFF的后端

我已经用了几年的 NextJS,用得越多,在更广泛的场景中使用它,越发明显地发现 NextJS 存在着一个大问题。

嗯,确实很多问题。技术问题一大堆:开发服务器慢得跟蜗牛似的,基于目录的路由简直是噩梦,还有一些其他的问题。不过我要说的不是这些技术问题,我要说的是一个身份认同上的问题。

在我多年的从业经历中,我经常看到同一件事发生:后端被轻视。从微框架技术到后端即服务(BaaS),认为可以草率处理或完全忽略后端的想法一直存在并顽固不化。

因此,当 Next 开始受到欢迎时,人们首先想做的是让服务器完全接管一个 API,将 NextJS 应用程序转变为一个全栈应用,处理所有任务。

这可能最初看起来有效,但这种解决办法可能带来不少麻烦,特别是当应用程序的需求和复杂性增加时。

这就是所谓的“反模式”,非常准确。

这篇文章讲的是什么

我一直在反复修改这篇文章,重写和重新思考,因为这篇文章有两个关键点,总是倾向于忽视其中一个关键点,另一个则因此被忽略。所以我在这里明确声明这一点,解释一下我的意图,以便为下面的内容提供一些背景。

这两点大致是这样的:

  1. 好的API是Web应用程序的重要且强大的部分,而NextJS在创建功能丰富的API方面表现不佳。即使在JavaScript中,使用NestJS(这可能听起来有些让人困惑)、Adonis,或最差的情况下使用Express,也会比使用Next更好。这些框架不仅功能丰富,而且比Next更强大。在JavaScript之外,还有Rails、Phoenix、Golang、Rust、Django或不可胜数的工具可以创建强大且功能丰富的API。
  2. NextJS不是特别适合用于API的一个主要原因是,我认为它原本并不是为此而设计的。它也擅长作为后端为前端的框架。本文接下来将详细阐述这些观点并提供支持。
打造更好的API

后端代码输出 JSON 并没有让它摆脱复杂的特性。测试、日志记录、消息传递、入职流程、分析、认证、速率限制、迁移、电子邮件服务、错误处理、性能监控、负载测试、访问控制、WebSocket、部署、报告、序列化、集成、审计、安全以及其他许多功能,让这些应用变得庞大,需要一个“单体”框架,而不仅仅是 HTML 生成。

当然,你可以将 Prisma 连接放入服务器中,但你打算如何真正实现 WebSocket 以支持实时连接呢?负载均衡呢?API 版本控制呢?将端点拆分到不同的微服务?看你有多少时间和精力,你可能只能搞定其中的一部分或全部。

但这些功能是其他框架、其他语言和其他后端解决方案的核心。它们已经内置,或者只需安装一个简单的官方库即可,预配置且高度集成。仅就NextJS中的中间件而言,它的功能非常有限。没有任何实际的后端系统会有这样的限制——一个单一的函数和一个复杂的匹配规则。没有组合能力,没有路由关联规则,无法对路由进行分组或分配。只是一个单一且粗暴的函数,你可以用复杂的条件来加载它。

你选择什么以及如何解决后端 API 是由你自己决定的。我完全转向了 Golang 来进行后端开发,但你还是按你喜欢的方式来。即使在 JavaScript 中也有比 NextJS 更好的选项来搭建真正的后端。Express 只是最基本的框架——我个人并不太喜欢。NestJS(和 NextJS 对比)和 Adonis 都是优秀的 JavaScript/TypeScript 后端框架。

Next.js实际上用来做什么——就是那个超级棒的小伙伴!

这并不是说要贬低 NextJS,而是要指出,它在这些方面表现一般,是因为它并不是为这些用途而设计的。

NextJS 是一个功能简单的后端,但它是前端后端开发中的佼佼者。

后端为前端模式是一种特定的架构模式,承认了前端和后端环境的复杂性日益增加。这种模式反映了前端和后端环境复杂性的提升。

目标是将前端(也就是客户端)与外部世界隔离开来。有了 BFF 之后,它就变成了客户端与外界通信的唯一接口。

直接连接

本质上,BFF 是一种为前端服务的后端,就像管家一样照顾你特别珍贵的客户端。

以下是构建一个BFF的一些优点。

微服务架构

这种模式的主要来源是微服务架构中的设计。微服务在分割功能和允许多个团队、方法和语言独立工作方面有其优势。但作为客户端时处理这种情况,明显存在一个问题。如果用户服务、项目服务和活动服务被分别独立设计,它们可能使用不同的字段、序列化方式、标准和预期。

BFF旨在简化那种复杂性,将数据统一到一个标准,让客户端只需与单一数据源打交道。

外部提供的服务功能

数据来源并不总是一致。也许你的价格数据、地图信息或用户KYC来自第三方?使用Backend-For-Frontend(BFF)可以帮助你集中这些来源,并从一个地方发起请求,让BFF负责处理这些数据来源的实际获取,可能还会在城市信息中添加天气详情,或者在交易中添加当前价格。

序列化(将数据转换为序列的过程)

或在更随意的语境下使用:串起来(将数据转换为序列的过程)

我自己使用过BFF与后端应用一起工作,这些应用使用了我不熟悉的序列化格式,让BFF在不同的标准之间进行转换,比如将company_id转为companyId,或者包装和解包实体。

错误处理

可能你的后端会出现一些错误,你希望将这些错误与前端需求隔离开。也许你想传递一个错误信息的翻译字符串,或者抑制错误。这听起来不太好,但确实发生过,我记得曾经处理过一个所谓的“REST” API,如果用户没有请求的那些实体,比如请求他们未完成的交易,它不会返回空数组,而是返回422状态码。后端团队对此没有进行讨论,所以我们不得不专门处理这种情况,我们开玩笑地称它为“422成功错误”。这种荒谬的情况正是你的BFF可以帮助你隔离开的。

中级持久性概念

应用程序中有各种各样的状态,你不一定希望永久保存或以特定方式保存。例如,用户根据名称字段将表格升序排列,或者他们设置了过滤器。你希望他们在返回或重新加载后不会丢失这些操作,但这并不意味着你一定要把它们存到 PostgreSQL 里。至少,为了调整这样的小设置而往返服务器似乎没必要。(可以,且应该乐观地处理,但这不是重点。)

在 BFF 处拥有一个中间级别的缓存(例如 redis)对于那些不属于本地存储且也不需要占用真正数据库的数据来说,是非常合适的。

安全方面

由于你的后端朋友是后端,你不需要在客户端放任何安全链接和密钥。后端可以发起请求,通过最佳实践(如使用保险库和环境变量)来访问这些密钥。然后客户端只需要简单地请求已经被授权的数据。

跨源资源共享 (跨源资源共享是一种允许网页从不同源加载资源的方法)

作为一个相关点,可以通过使用像 NextJS 这样的前端后端架构来轻松处理 CORS 问题。Next 后端处理 CORS 头部信息,满足客户端需求。后端发起请求时无需考虑 CORS 问题,因为 CORS 问题由客户端负责。

认证和授权

虽然这个额外的步骤增加了复杂性,但添加选项对于质量认证来说却意外地很有帮助。具体来说,每个人总是告诉你不要在客户端的本地存储中保存JWT,但没有人告诉你应该如何正确地存储JWT。使用NextJS作为BFF(Backend For Frontend)给你提供了一个好选择——一个只读的后端HTTP Cookie。这既安全,也使你可以将所有授权令牌逻辑简单地交给后端处理。

不同的客户或客户群

可能你有多个不同的客户端——一个移动应用、一个网页应用,甚至像三星智能冰箱这样的设备等。不同的 BFF(前端后端)为非常不同的用户界面服务可以意味着这些 BFF 会从一个中央数据源组织数据以便各自 UI 使用。因为 NextJS 与一个特定客户端紧密相连,而不是特指 NextJS,而是更多地关注这种模式本身。

数据mock

这可能令人惊讶,但当你处理所有来自bff(最好的朋友 forever,简称)的东西时,将外部API完全隔离变得极其可能。bff可以从中检索API数据,但它也可以生成自己的数据,创建模拟数据等。这种隔离对快速开发和测试非常有帮助。我自己也这么干过,让后端返回测试用的模拟数据,而不是连接复杂的外部API,从而实现离线工作。

有什么坏处?

总有那么一些什么。好处总是伴随着代价。这种情况下的代价很简单也很明显,从一开始就看得出来。相比直接连接到中心API,增加了复杂性。可能还会增加基础设施成本,特别是如果你想在BFF上有一个小数据库。

还有一个问题是,如果你有多个客户端——比如你的移动应用和 React 应用——你就没有从 BFF 模式(Backend For Frontend 后端前置)中得到任何好处。事实上,每个客户端都需要一个独立的 BFF。BFF 的作用就是为特定客户端提供定制化的后端代理。

如何进行后端为前端开发

就像大多数正式流程一样,有官方正确的做法,也有差不多的方式。正式要求,所有客户端请求都必须通过后端为前端。

Next.js:API代理指南

这是我亲自尝试过的一个方法,提供了一个简单且有效的解决办法。只需在本地NextJS API目录中创建一个通配符路由,并将所有API请求代理到该路由。这样可以让我将所有的JavaScript companyId转换成GoLang期望的company_id。完全透明地说,这导致了许多与请求头长度相关的问题,我最后还是放弃了这种方法,不过这个想法还是不错的,也许更有经验的开发者能找到让它工作的办法。

    import { type NextRequest } from "next/server";  

    export async function GET(  
      req: NextRequest,  
      { params }: { params: { proxy: string[] } }  
    ) {  
      return handleRequest(req, { proxy: params.proxy });  
    }  

    export async function POST(  
      req: NextRequest,  
      { params }: { params: { proxy: string[] } }  
    ) {  
      return handleRequest(req, { proxy: params.proxy });  
    }  

    export async function PATCH(  
      req: NextRequest,  
      { params }: { params: { proxy: string[] } }  
    ) {  
      return handleRequest(req, { proxy: params.proxy });  
    }  

    export async function PUT(  
      req: NextRequest,  
      { params }: { params: { proxy: string[] } }  
    ) {  
      return handleRequest(req, { proxy: params.proxy });  
    }  

    export async function handleRequest(req: NextRequest, query: any) {  
      const { searchParams } = req.nextUrl;  
      const method = req.method.toUpperCase();  
      const headers = Object.fromEntries(req.headers.entries());  

      // 获取名为 'token' 的 cookie
      const token = req.cookies.get("token");  
      if (token) {  
        headers["Authorization"] = `Bearer ${token}`;  
      }  

      // 如果有搜索参数,则构建查询字符串
      const params = searchParams.size > 0 ? "?" + searchParams.toString() : "";  
      const url =  
        process.env.NEXT_PUBLIC_REMOTE_API_HOST + query.proxy.join("/") + params;  

      // 远程API主机地址
      const fetchOptions: RequestInit = {  
        method,  
        headers: {  
          ...headers,  
          "Content-Type": "application/json",  
        },  
        ...(method !== "GET" && {  
          body: JSON.stringify(await req.json()),  
        }),  
      };  

      // 尝试执行以下代码块
      try {  
        const fetchResponse = await fetch(url, fetchOptions);  
        const responseBody = await fetchResponse.json();  

        const body = JSON.stringify(responseBody);  
        const responseHeaders = new Headers(fetchResponse.headers);  
        return new Response(body, {  
          status: fetchResponse.status,  
          headers: responseHeaders,  
        });  
      } catch (error) {  
        // 如果发生错误,执行以下代码
        console.log(error.message);  // 输出错误信息
        console.log(error);  // 输出完整的错误对象
        return new Response(  
          JSON.stringify({ message: "遇到了一个未知的问题" }),  
          { status: 500 }  
        );  
      }  
    }
如何决定你是否想要一个最好的朋友

这里有两条关键指南——你需要为多个客户提供独立的API,还是只有一个客户需要连接到多个不同的服务?

如果你符合上述任何一种情况,那么使用 BFF 会很有帮助。此外,如果你正在连接到一个外部 API,并且该 API 有一些“不想要的”部分,你希望将这些部分从客户端隔离开。最后,如果有安全方面的考虑,那么最好将这些需求移至后端。在这里,“BFF”指的是“好友前端”(Friend Frontend 的简称)。

真正的好处在于这里,NextJS 为 BFF 提供了一个极低的入门门槛。既然现在有这么多人在使用 NextJS,不管他们是否对这种设计模式感兴趣,为什么不充分利用框架的所有好处呢?

总之.

这里我想说三点,主要是想对三种不同的人说说。

首先,对于那些希望将NextJS用作“全栈框架”的人来说,将Prisma或Drizzle连接添加到NextJS应用中作为概念验证、最小可行产品(MVP)或技术演示是完全没问题的。但是,如果你正在构建一个业务或长期应用,你可能希望后端能提供更多功能。

也许你想要Golang或Rust的性能,或者Phoenix、Laravel或Rails的丰富功能集。或者你可能只想使用Express提供的最基本的特性。不管怎样,你总能找到比Next.js更适合用作后端的选择。让它发挥长处,而不是勉强适用的地方。

第二个用户群体是那些目前使用NextJS搭配外部后端的用户。但相关文档不太充分,特别是关于授权的部分,但这也是一种完全有效的做法。如果你在这样做,可以研究一下“后端为前端”这种正式模式(即BFF模式),看看能否做出优化或发现一些你未曾注意到的好处。

最后一点:如果你为 NextJS 制作内容,请不要再重复同样的限制。请不要再制作关于如何用 JWT 从 Prisma 返回用户的视频,而是展示人们如何通过 REST API 实现相同功能,并教大家如何管理会话。不要再用 Next 做它不擅长的事情,也不要对由此产生的复杂性避而不谈。

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消