Arno 在使用 next.js 为阿里巴巴的钉钉(DingTalk)和其他大型项目提供他最好的最佳实践(BP)。
该文章使用 GPT 翻译自原文:Next.js Full Stack App Architecture Guide
Server
通用 API 和架构风格
- 架构设计应该是多样化的,随项目生命周期的不同阶段而演化,不要总是首先使用相同的架构风格,如
微服务
(micro-services),它们并不总是最佳选择,尤其是对于刚起步或小型项目。 - 发挥 Restful API 设计 的力量,保持并遵循严格的 API 设计原则,或使用其他正式的 API 设计原则或 RPC 框架,如
GraphQL
或gRPC
,谨慎地在一个项目中应用不同的通信协议,不要增加不必要的复杂性。 - 无状态(Stateless)和无服务器(Serverless) 设计应作为首选,以便服务易于扩展和管理。
Vercel
为 next.js 应用提供了一个很好的无服务器平台。我们也可以将无服务器函数部署在Aliyun
或CloudFlare EdgeWorker
中作为服务供应商。 - 对于公共 API 或内部 API 管理,我们应该在路径名中添加版本控制,如
/api/v1/*
,以确保向后兼容,遵循标准的 API 设计规范,如OpenAPI
或Swagger
,确保 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 或其他数据库服务供应商来处理数据持久性,如
Prisma
或TypeORM
。 - 总是从数据扩展和数据一致性的角度出发设计数据库,考虑分布式数据库和分片等,在您真正需要时。例如,如果您的数据记录在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/import
与Suspense
和React.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`: 公共静态文件
我的实践:
- 🌟 Github - arno packages:遵循上述原则的我的基础包。
Web 客户端
React 最佳实践指南
- 将逻辑和视图分离在不同层次
- JSX / 组件应当纯粹使用函数式方式
- 不要在 JSX / 组件中编写大段的业务逻辑,如处理程序(handler)/ 回调(callbacks)/ 钩子(hooks),应使用客户端管理器或服务中的函数
- 尽可能尝试使用 Server Actions 以更简单明了的方式同时处理服务器端和客户端的逻辑
- 将客户端的
状态(State)
分离在不同层次ServerState
:由服务器管理的状态- 使用钩子来作为服务器的状态,比如位置 / url 路径名 / 搜索参数 / next 参数等...
- 使用
SWR
或ReactQuery
来处理服务器状态和缓存,或者直接在服务器组件的页面中处理,以减少客户端的复杂性
SharedContext
通常不会频繁变动,使用React.Context
来处理共享状态和上下文,例如主题(Theme)、地区设置(Locale)、用户(User)等作为全局共享上下文。SharedState
使用redux
、zustand
或recoil
以及日志记录器和中间件来更好地处理组件之间共享的不可变状态,这样的状态可以轻松地进行可视化、追踪、记录和调试。ReactiveState
考虑使用RxJS
或Mobx
来处理反应性状态和事件驱动的状态LocalComponentState
尝试使用useState
和useReducer
来处理组件的本地状态
- **首先考虑钩子风格(Hook-Style First)**来封装逻辑,使其在组件之间更加可复用,减少 JSX 组件主体中的代码,使代码更清晰易维护
逻辑封装
- 服务(Service) 类似于服务器服务、客户端服务以及封装的 api,可通过
useSwr
或useQuery
或其他 API 客户端调用。- 一些典型的例子包括:
- 本地的数据库服务
- 本地数据缓存服务
- Restful API 服务
- 服务可以封装逻辑并优雅地处理错误和响应,不应该抛出异常或错误
- 服务可以协调不同的服务或管理器来处理复杂的逻辑
- ...
- 一些典型的例子包括:
- 管理器(Manager) 类似于服务器管理器、客户端管理器以及封装的业务逻辑,可由服务或组件调用
- 一些典型的例子包括:
- 业务逻辑管理器
- 日志管理器
- 事件发布/订阅管理器
- ...
- 管理器是业务逻辑的基本构建块,可以被不同的服务重用
- 管理器可以抛出错误,无需额外处理,应由服务适当处理
- 一些典型的例子包括:
性能和存储的提示
- 使用 web worker 来处理繁重的计算任务
- 使用 Rust / WASM 来处理繁重的计算任务
- 使用 Service Worker 来处理离线和缓存数据及请求
- 使用本地数据库如
IndexedDB
来处理本地存储和缓存 - 使用
localStorage
/sessionStorage
来处理用户偏好和临时数据缓存的本地存储和缓存
网络和通信
- 使用 WebRTC / Websocket 进行更实时和交互式的任务
- 首先使用原生
fetch
API 而不是 ajax 或其他网络通信机制 - 使用Fetch API 结合 WebStream 来处理大数据传输和流数据
UI & UX 提示
- 使用
TailwindCSS
或ChakraUI
来处理用户界面和布局 - 使用
AntD
或MaterialUI
,它们具有高度可定制的主题,并封装了复杂的逻辑和功能 - 使用
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
/error
和fatal
来处理不同级别的错误,对于严重级别的错误和致命错误,我们应该使用日志
(log)服务来报告,以保证稳定性和可靠性 - 在服务/管理层使用
try
/catch
来处理错误,并且try-catch语句块的范围应该尽可能小,以避免性能问题和代码的模糊性 - 在组件的客户端层使用
ErrorBoundary
来处理错误,用于显示备用
(fallback)信息和来自客户端和服务器的错误信息- 比如:
error-boundary.tsx
用于实现统一的 UI 错误处理
- 比如:
文件类型和约定
*.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
操作,使用create
、get / list
、update
、delete
作为方法名前缀
参考资料
我的相关文章:
关于本文的注释
- 2024-03-12:修复错误并为某些主题添加更多细节,添加与
db
相关的主题指南 - 2024-03-29:增加一些关于组件目录的组织规范和命名方式
- 2024-10-21:增加了一些 server-action 的最佳实践,同时增加了一些关于错误处理的最佳实践