🪐 Next.js 全栈研发架构指南

February 2, 2024 (9mo ago)

Arno 在使用 next.js 为阿里巴巴的钉钉(DingTalk)和其他大型项目提供他最好的最佳实践(BP)。

该文章使用 GPT 翻译自原文:Next.js Full Stack App Architecture Guide

Server

通用 API 和架构风格

  • 架构设计应该是多样化的,随项目生命周期的不同阶段而演化,不要总是首先使用相同的架构风格,如微服务(micro-services),它们并不总是最佳选择,尤其是对于刚起步或小型项目。
  • 发挥 Restful API 设计 的力量,保持并遵循严格的 API 设计原则,或使用其他正式的 API 设计原则或 RPC 框架,如 GraphQLgRPC,谨慎地在一个项目中应用不同的通信协议,不要增加不必要的复杂性。
  • 无状态(Stateless)和无服务器(Serverless) 设计应作为首选,以便服务易于扩展和管理。 Vercel 为 next.js 应用提供了一个很好的无服务器平台。我们也可以将无服务器函数部署在 AliyunCloudFlare EdgeWorker 中作为服务供应商。
  • 对于公共 API 或内部 API 管理,我们应该在路径名中添加版本控制,如 /api/v1/*,以确保向后兼容,遵循标准的 API 设计规范,如 OpenAPISwagger,确保 API 文档完善且易于使用。随着项目复杂性的增加,API 也会变得复杂,我们需要选择适合项目的方法来控制这种复杂性。
  • 充分利用 Next.js 的全栈开发能力,快速构建 Web 应用并验证您的想法,快速试错。随着项目的增长和复杂性的增加,再添加和隔离不同的层次,再用微服务的思想进行解构。

Application 层(控制器)

控制器 / 路由:对于 next.js 来说,api-routes 使用 函数 编程风格,传统的应用层。

  • 处理 Web 和应用层的请求与响应
  • 处理 Web 或其他协议响应的错误 -> 可以参看下面错误处理模式章节的内容
  • 返回 统一 的响应或服务结果对象,供客户端处理
  • 控制器可以写在全局 中间件 函数中,或由高阶函数包装以处理公共逻辑
  • 处理错误代码和错误消息
  • 这一层最好不包含业务逻辑

Service 层

Service:用面向对象(OO)风格封装领域服务和管理器,处理业务逻辑的模块

  • 领域驱动设计(Domain Driven Design)提供领域服务来处理业务逻辑
  • 不能抛出错误,并应处理错误
  • 返回 统一 的响应或服务结果对象,供控制器处理
  • 控制器层可以试用 GlobalMiddleware 或者高阶函数来处理公共逻辑(类似 koa 的思路),包括:
    • 可组合的 页面路由
    • 可组合的 API 路由
    • 可组合的 服务器动作
// API Route
export const POST = composeAPIRoute([authUser, authBizLogic, async(req, res, ctx) => {}])
// Page Server
export const getServerSideProps = composePageServer(function(params, ctx) {
  return <div>...</div>
}, [authUserInPage, authBizLogicInPage])
// Server Action
export const serverAction = composeServerAction([authUserAction, authBizLogicAction, async(req, res, ctx) => {}])

具体的 Sample 代码可以参看:https://github.com/SurfaceW/arno-packages/tree/main/server/next

  • 使用 Swagger 或其他正式的 API 规范与第三方服务供应商集成,确保代码中的 API 文档完善,并使用工具生成这些 API 客户端代码
  • 尝试针对各种类似服务的面向接口设计,例如 *.service.impl.ts
  • 遵守面向对象最佳实践(OO BP)的规则,例如 SOLID、DRY、KISS 等。
    • 使用依赖注入(DI)风格注入依赖,构建依赖注入驱动的服务依赖和实例管理

Manager 层

Manager:以面向对象(OO)风格管理可复用代码的业务逻辑模块

  • 可以抛出错误
  • 为服务层提供可选的依赖注入(DI)
  • 使其更加原子化和模块化以封装业务逻辑
  • 工具函数(utils)和 帮助函数(helper)可以作为管理器的子类别放在这里

数据持久层

  • 为您的业务特性精确选择 SQL 或 NoSQL 数据库类型,不要忘记为数据添加冗余和备份。
  • 选择 ORM 或其他数据库服务供应商来处理数据持久性,如 PrismaTypeORM
  • 总是从数据扩展和数据一致性的角度出发设计数据库,考虑分布式数据库和分片等,在您真正需要时。例如,如果您的数据记录在3年内超过100万、1000万或1亿等,应该计划使用不同类型的数据库技术,并以不同的方式设计数据模型和数据结构。
    • 考虑使用 Serverless 数据库来处理数据扩展和管理
  • 在数据库中仔细设计数据模型和数据结构,充分利用数据库索引和查询优化,但不要过度优化数据库查询。
  • 遵循社区的 SQL、创建表、创建索引、查询优化等最佳实践指南。-> 例如,阿里巴巴的 Java 开发者指南为 MySQL 和数据库提供了一些最佳实践指南。
  • 使用 事务(transaction)和 隔离(isolation)来处理敏感数据和操作的数据一致性和完整性

Next.js 最佳实践指南

  • 使用 turbo-pack + git-submodules + npm packages 来处理不同级别的代码共享以及单体仓库(monorepo)和多仓库(multi-repo)
  • 充分利用 Next.js 在页面/路由中的分层缓存系统React.cache,获取缓存,客户端缓存,外部 redis-cache 等。
  • 尝试使用 EdgeRuntime 来处理静态内容和相关简单任务的获取
  • 对于高性能计算任务,使用 Rust + WASM,例如通过 tiktoken 计算大数据的令牌
  • 分离 ServerComponentData / ClientComponentData,巧妙地处理服务器端渲染和客户端渲染
  • 使用 CDN 为静态文件和内容提供服务,使用 next/image 等进行资源优化
  • 使用 next/script 进行脚本优化
  • 利用 next.js 页面内嵌的 metadata 能力提升 SEO 和 SNS 分享
  • 通过 Next.js 的 dynamic/importSuspenseReact.lazy 机制提升性能
  • 使用 server / client / shared 文件夹来分隔不同运行时的代码
  • 在进入生产环境前,检查 next.config.js 中的每一个可能的配置,以优化性能和安全性
  • 使用 sever-action 来避免重复的 API 路由声明,并无缝处理服务器端逻辑和客户端逻辑

充分利用 Node / JavaScript 生态系统中的工具/库

避免重复造轮子,使用 Node / JavaScript 生态系统中最好的工具和库。

  • 数据库层可以使用 Prisma ORM 或其他数据库服务供应商
  • 使用跟踪 API 和工具进行错误/请求追踪,例如 Sentry / Raygun
  • 使用日志 API 和工具,包括 SLS / ELK 进行日志记录和调试
  • 考虑使用 GlobalRef 用于长时间存在的 node 运行时,而不是无服务器运行时(serverless runtime)
  • 添加单元测试以覆盖基本的库/工具和关键的业务逻辑
  • 使用配置中间件如 Diamond 或 Vercel 配置来管理应用配置
  • 使用 Redis 或其他内存共享工具来处理不同服务器实例间的缓存和会话共享
  • 考虑使用 AB 测试服务或灰度发布服务来处理功能发布
  • 使用 Kafka 或其他消息队列来处理异步任务和事件驱动的任务
  • 使用 K8S 或其他容器服务来处理部署和扩展
  • 使用 Prometheus 或其他监控服务来处理监控和报警
  • 使用 Grafana 或其他仪表板服务来处理仪表板和可视化
  • 使用 Jenkins 或其他 CI/CD 服务(如 Github Actions 或 Vercel)来处理 CI/CD 流水线和生产过程
  • 使用 Jest 或其他测试服务来处理单元测试和集成测试
  • ...

库和目录结构指南

- `app`: next.js 应用的主入口
  - `[bizRoute]`: 业务页面路由
    - `page.tsx`: 业务页面
    - 其它 Next.js 规定的文件
    - `*.server.tsx`: 服务器业务组件
    - `*.client.tsx`: 客户端业务组件
  - `api`: api 路由
    - `/v1`: 供公众使用的版本化 api 路由
      - `*.ts`: api 路由
- `components`: 共享组件
  - `server`: 服务器端组件
  - `client`: 客户端组件
  - `shared`: 共享组件
- `configs`: 应用共享配置
- `lib`: 共享库和工具
  - `server`: 服务器端库和工具
  - `client`: 客户端库和工具
  - `shared`: 共享库和工具
    - `*.manager.ts`: 可共享的业务管理器
- `services`: 共享服务
  - `server`: 服务器端服务
  - `client`: 客户端服务
  - `shared`: 共享服务
    - `*.service.ts`: 可共享的业务服务
- `public`: 公共静态文件

我的实践:

Web 客户端

React 最佳实践指南

  • 逻辑视图分离在不同层次
    • JSX / 组件应当纯粹使用函数式方式
    • 不要在 JSX / 组件中编写大段的业务逻辑,如处理程序(handler)/ 回调(callbacks)/ 钩子(hooks),应使用客户端管理器或服务中的函数
    • 尽可能尝试使用 Server Actions 以更简单明了的方式同时处理服务器端和客户端的逻辑
  • 将客户端的状态(State)分离在不同层次
    • ServerState:由服务器管理的状态
      • 使用钩子来作为服务器的状态,比如位置 / url 路径名 / 搜索参数 / next 参数等...
      • 使用 SWRReactQuery 来处理服务器状态和缓存,或者直接在服务器组件的页面中处理,以减少客户端的复杂性
    • SharedContext 通常不会频繁变动,使用 React.Context 来处理共享状态和上下文,例如主题(Theme)、地区设置(Locale)、用户(User)等作为全局共享上下文。
    • SharedState 使用 reduxzustandrecoil 以及日志记录器和中间件来更好地处理组件之间共享的不可变状态,这样的状态可以轻松地进行可视化、追踪、记录和调试。
    • ReactiveState 考虑使用 RxJSMobx 来处理反应性状态和事件驱动的状态
    • LocalComponentState 尝试使用 useStateuseReducer 来处理组件的本地状态
  • **首先考虑钩子风格(Hook-Style First)**来封装逻辑,使其在组件之间更加可复用,减少 JSX 组件主体中的代码,使代码更清晰易维护

逻辑封装

  • 服务(Service) 类似于服务器服务、客户端服务以及封装的 api,可通过 useSwruseQuery 或其他 API 客户端调用。
    • 一些典型的例子包括:
      • 本地的数据库服务
      • 本地数据缓存服务
      • Restful API 服务
    • 服务可以封装逻辑并优雅地处理错误和响应,不应该抛出异常或错误
    • 服务可以协调不同的服务或管理器来处理复杂的逻辑
    • ...
  • 管理器(Manager) 类似于服务器管理器、客户端管理器以及封装的业务逻辑,可由服务或组件调用
    • 一些典型的例子包括:
      • 业务逻辑管理器
      • 日志管理器
      • 事件发布/订阅管理器
      • ...
    • 管理器是业务逻辑的基本构建块,可以被不同的服务重用
    • 管理器可以抛出错误,无需额外处理,应由服务适当处理

性能和存储的提示

  • 使用 web worker 来处理繁重的计算任务
  • 使用 Rust / WASM 来处理繁重的计算任务
  • 使用 Service Worker 来处理离线和缓存数据及请求
  • 使用本地数据库如 IndexedDB 来处理本地存储和缓存
  • 使用 localStorage / sessionStorage 来处理用户偏好和临时数据缓存的本地存储和缓存

网络和通信

  • 使用 WebRTC / Websocket 进行更实时和交互式的任务
  • 首先使用原生 fetch API 而不是 ajax 或其他网络通信机制
  • 使用Fetch API 结合 WebStream 来处理大数据传输和流数据

UI & UX 提示

  • 使用 TailwindCSSChakraUI 来处理用户界面和布局
  • 使用 AntDMaterialUI,它们具有高度可定制的主题,并封装了复杂的逻辑和功能
  • 使用 Storybook 或其他 UI 测试服务来处理用户界面测试和视觉测试
  • 考虑在不同的视口、尺寸、版本等方面的兼容性
  • 遵循移动 Web 应用开发原则的最佳实践(BP)
  • 遵循并应用 Google / Apple / Microsoft / Mozilla / W3C 等的一些 web 开发指南
  • 考虑遵守无障碍性(A11Y)和国际化(I18N)原则
  • 添加一些合理的动画和过渡效果,使 UI 更生动和互动
  • 响应式设计优先
  • 暗色方案兼容性
  • 使用 SSG / SSR / ISR / CSR / Reactive / Hybrid 来处理不同的渲染策略,以提升性能和用户体验
  • ...

特定领域的技术

不要重复造轮子,使用特定领域的技术来处理特定任务。

  • 文本编辑器 / 集成开发环境(IDE)/ 代码格式化器(Code Formatter)/ 代码检查工具(Linter)...
  • 图表 / 图形 / 数据可视化...
  • 地图 / 定位 / 地理信息...
  • 实时协作...
  • 3D 渲染 / 2D 渲染 / 动画...
  • 音频 / 视频 / 媒体...
  • Web XR / VR / AR...
  • ...

原生客户端

  • 可以等待为原生客户端提供最佳实践(BP),例如 iOS / Android / Windows / Mac / Linux / WebOS / Tizen / HarmonyOS / ... 的操作。

Arno 仍在探索 🧭

常见模式和约定

为 next.js 全栈应用开发提供开发者可以遵循和使用的一些常见模式和约定。

错误处理模式 (Error Handling Patterns)

0错误处理是最佳的错误处理,但在现实世界中我们无法避免错误,因此我们需要优雅且恰当地处理错误。

  • 低级别的API应该减少抛出错误,并在高级别的API中处理错误
  • 使用一个地方处理和记录错误,不要在不同地方用重复的代码处理错误
  • 服务器(Server)和客户端(Client)中都使用原生的Error对象,并且不要扩展它以增加复杂性,使用统一的错误信息格式,如:
[2024-03-12 12:00:00] [ERROR level] [ServiceName] [ErrorTypeCode] [ErrorMsg] 
[?ErrorStack] 如有必要
  • 对于常见的HTTP协议的服务器响应,错误应该充分利用HTTP状态码、消息来描述响应中的错误,其他RPC协议应该遵循它们自己的错误处理格式模式
  • 使用warn / errorfatal来处理不同级别的错误,对于严重级别的错误和致命错误,我们应该使用日志(log)服务来报告,以保证稳定性和可靠性
  • 在服务/管理层使用try / catch来处理错误,并且try-catch语句块的范围应该尽可能小,以避免性能问题和代码的模糊性
  • 在组件的客户端层使用ErrorBoundary来处理错误,用于显示备用(fallback)信息和来自客户端和服务器的错误信息

文件类型和约定

  • *.type.ts:可以共享的 TypeScript 语言类型
  • *.constant.ts:可以共享的常量
  • *.util.ts:可以共享的实用工具函数
  • *.manager.ts:可以共享的业务管理器
  • *.manager.impl.ts:抽象管理器的实现
  • *.service.ts:可以共享的业务服务
  • *.service.impl.ts:抽象服务的实现
  • *.store.ts:可以共享的 zustand 或任何其他 React 状态管理单元的业务存储
  • *.hook.ts:可以共享的业务钩子(Hook)
  • *.client.tsx: 客户端共享组件
  • *.server.tsx: 服务器端共享组件
  • *.shared.tsx: 共享组件
  • *.server.action.ts: Next.js 的 Server Action 可以在服务器和客户端之间共享
  • *.[designPatternName].*.ts : 按照设计模式的命名方式来命名文件
    • account.adapter.impl.ts 特定服务的适配器模式实现

类方法命名约定

  • 对于服务的 CRUD 操作,使用 createget / listupdatedelete 作为方法名前缀

参考资料

我的相关文章:

关于本文的注释

  • 2024-03-12:修复错误并为某些主题添加更多细节,添加与 db 相关的主题指南
  • 2024-03-29:增加一些关于组件目录的组织规范和命名方式
  • 2024-10-21:增加了一些 server-action 的最佳实践,同时增加了一些关于错误处理的最佳实践

Arno Crafting Apps

ELABORATION STUDIO 🦄

Elaborate your ideas and solve your problems with AI in fully boosted context way ~