Next.js 网页应用编码规范 是做什么的?
将这些规范应用于所请求的工作。用户的显式指令优先于本技能中的任何规则或默认设置。保留现有的产品需求和集成;应用本技能不授权进行不相关的迁移或安装每个可选的依赖项。
在实施之前,检查项目指令、已安装版本和现有约定。当可用时,阅读相关的已安装 Next.js 文档,特别是 node_modules/next/dist/docs/;否则,查阅与版本匹配的官方文档。不要假设其他版本的框架 API。
默认值和条件特性
| 领域 | 默认值 | 覆盖或激活规则 |
|---|---|---|
| 框架 | Next.js App Router 和严格的 TypeScript | 尊重项目选择的版本和用户的明确选择。 |
| 组件 | Base UI + Tailwind CSS,封装在本地组件中 | 支持 Radix、shadcn/ui 或其他请求的库。保留相同的组件组织方式。 |
| 变体 | class-variance-authority 和一个可识别 Tailwind 的类合并辅助函数 | 重用已建立的等效工具。 |
| API 传输 | Axios,封装在自定义的 APIClient 和 ServerAPIClient 抽象之后 | 现有的自定义客户端、fetch 或其他请求的传输方式都有效。保持契约独立于传输方式。 |
| 验证 | Zod 模式,带有推断出的 TypeScript 类型 | 默认在外部数据边界需要;仅在用户明确指示时更改。 |
| 远程客户端数据 | TanStack Query,带有一个共享的查询包装器 | 重用请求的等效工具。 |
| 共享交互状态 | Zustand | 对单个组件拥有的状态使用本地 React 状态。尊重明确的替代方案。 |
| 身份验证 | 当请求身份验证时,使用与提供商无关的电子邮件 OTP 流程 | 遵循选定的提供商和请求的登录方法;不要隐式选择或提供提供商。 |
| 跨标签同步 | 对相关的共享变更事件启用 | 可按需选择或移除。仅在跨标签一致性有用时添加事件。 |
| 日志和追踪 | 遵循现有的日志系统 | 如果不存在,除非被请求,否则不要引入、提议或提及日志/追踪基础设施。 |
| 模拟数据和仿真 | 关闭 | 仅在用户请求模拟数据或开发/测试仿真时添加。 |
| 开发工具 | 独立的仅开发模块 | 仅在相关或被请求时添加工具;不规定特定的工具或覆盖层。 |
当需要决策时,选择默认值;不要询问每个默认值。不要在不需要它们的特性中引入身份验证、状态存储、同步或基础设施。
必需的文件夹结构
对新代码使用此组织方式。随着职责变得必要而创建目录,而不是用空的占位符填充项目。在现有项目中,避免在没有迁移请求的情况下移动不相关的文件。
src/
├── app/ 路由、布局、API 端点
├── components/
│ ├── ui/ 共享的 UI 原语
│ ├── standalone/ 可复用的领域组件
│ ├── pages/<page>/ 特定页面的组件
│ └── <feature>/ 特性子系统
├── controllers/
│ ├── client/<domain>/ 浏览器 API 调用
│ └── server/<domain>/ 服务器端 API 调用
├── providers/ React 上下文提供者
├── stores/ Zustand 存储
├── hooks/ 共享的 React 钩子
├── helpers/ 身份验证/访问及其他工作流
├── lib/ API 客户端、缓存、日志、同步
├── db/ 数据库/Redis 客户端
├── utils/ 小型工具和常量
└── assets/icons/ 源 SVG 文件
基础设施的文件夹名称描述的是放置位置,而不是必需的服务。此结构不暗示任何数据库、Redis、缓存或日志栈。
- 将领域代码组织在这些职责文件夹下;避免用不同的顶层架构替换它们。
- 使用 PascalCase 命名组件文件,
useSomething.ts命名钩子(仅当包含 JSX 时使用.tsx),以及类似updateName.ts的驼峰命名操作文件。 - 使用描述性领域和特性目录。保持框架文件名如
page.tsx、layout.tsx和route.ts不变。 - 使用
index.ts组装控制器命名空间,使用index.tsx作为多文件组件的入口点。 - 使用
@/进行根目录在src的导入;对紧密的内部同级文件使用相对导入。对仅类型依赖使用import type。 - 避免临时副本、备份文件夹、重复实现以及职责不清的汇总文件。
路由和组件组合
保持 app/ 专注于路由、布局组合、请求处理和初始数据加载。当布局边界有用时,使用类似 (auth)、(no-auth) 和 (settings) 的路由组。路由组名称本身不提供访问保护。
保持页面精简:加载初始数据,解决页面级别的决策,并从 components/pages/<page>/ 或相关的特性目录渲染组件。服务器组件是默认选择;为浏览器状态、事件和 API 引入客户端边界。
组件放置:
components/ui/:与领域无关的原语和所选 UI 库的包装器。components/standalone/:跨页面或特性使用的可复用领域控件。components/pages/<page>/:特定于一个页面的组件。components/<feature>/:跨路由共享的较大子系统,例如导航或命令界面。
将复杂的组件拆分为聚焦的文件。 这是一个核心约定。一个大型组件应有一个目录,其中包含一个 index.tsx 组合入口点,以及根据需要命名的支撑组件、标签页、行、钩子和类型。不要在一个文件中累积多个大的子组件和不相关的副作用。根据职责而不是任意行数限制进行拆分;避免将琐碎的标记碎片化。
将交互机制保留在特性本地的钩子或聚焦的处理组件中:键盘导航、选择、事件订阅、拖拽行为和视图协调。当钩子被共享时,将其移动到 src/hooks/。保持输入快捷键的作用域,使其不干扰可编辑控件;在卸载时清理监听器。
在本地包装原始库,并暴露可组合的部分,例如 Dialog、DialogTrigger、DialogContent 和 DialogTitle。保留可访问的语义、焦点行为以及实际库的组合 API。不要在不检查兼容性的情况下将 Radix 特定的属性复制到 Base UI 包装器中。
从基础原始组件或原生元素中获取包装器属性的类型,并仅添加所需的自定义属性。对尺寸、强调和状态使用类型化变体。以可预测的方式合并消费者类,并根据安装的 React/库 API 保留 ref 支持。
使用连接到 Tailwind 实用程序的语义 CSS 变量。保持主题值集中化,组件变体本地化。此技能规定的是样式组织,而不是视觉主题、间距比例或产品设计。
在 app/providers.tsx 中组合相关的上下文提供者;将其实现保留在 providers/ 中。将仅浏览器的提供者放在明确的客户端边界之后,避免将开发工具初始化与正常的提供者职责混在一起。
控制器和 HTTP 客户端
在 controllers/client/<domain>/ 或 controllers/server/<domain>/ 下,每个文件一个操作。通过 index.ts 文件组装嵌套的命名空间对象,例如:
ClientController.org.members.list()
ClientController.profile.updateName(input)
ServerController.org.info()
控制器提供类型化操作和端点构造。组件应调用控制器,而不是组装 URL 或传输配置。控制器保持精简;将可复用的工作流逻辑放在 helpers/ 中,将基础设施放在 lib/ 中。
保持浏览器和服务器 HTTP 客户端分离:
lib/APIClient.ts:浏览器安全的传输、查询/正文序列化、响应处理和取消支持。lib/ServerAPIClient.ts:仅服务器传输、可信的基础 URL 和请求范围的凭据/会话上下文。- 将服务器密钥和仅服务器的依赖项排除在浏览器导入之外。永远不要将每个用户的凭据保存在共享的可变单例中。
- 当服务器控制器调用应用程序端点时,仅将所需的凭据转发到可信目的地。不要信任调用者提供的身份标头作为身份验证。
- 一致地规范化传输错误、非 2xx 响应和应用程序错误。保留有意义的状态/代码信息。不要将失败的请求变成成功的空数据。
- 通过控制器和传输层支持
AbortSignal。取消不应触发错误通知或重试。
API 模式、契约和处理程序
Zod 模式是输入的默认事实来源。为正文、查询和路径参数定义具体的模式,包括格式、允许的值、长度/范围限制、可选性和有意的强制转换。在使用外部值之前进行验证。避免使用 z.any()、宽泛的记录、类型转换和非空断言作为契约的替代。对每个边界有意地处理未知键。
默认情况下,将特定于方法的模式以及导出的请求/查询/响应类型与关联路由放在一起。使用 z.infer 推断请求类型;定义显式的响应契约,并在需要运行时验证的地方使用响应模式。在控制器和组件中重用这些类型,而不是重新声明它们。如果运行时契约必须与浏览器代码共享,或者路由模块的导出约束要求这样做,则使用相邻的 schema.ts 或 types.ts 和仅类型导入,以便服务器实现永远不会被拉入客户端包。
使用一致的区分响应信封:
type APIResponse<T> =
| { success: true; data: T; error: null }
| {
success: false;
data: null;
error: { code: string; message: string; details?: unknown };
};
在使用时,将 details 缩小为明确的、安全的验证/错误详情类型。保持所有分支的 HTTP 状态码一致。不要在生成环境的响应中暴露内部的异常或堆栈跟踪。
将多方法路由组织为特定于方法的模式/类型部分和函数,例如 get_request 和 post_request。通过 lib/api/APIRequestHandler.ts 中的 withAPIRequestHandler() 导出处理程序。
维护 lib/api/error/,包含一个 APIError 类和一个按领域组织、类型化的错误代码、消息和状态目录。共享处理程序将验证失败、领域错误和意外失败映射到相同的响应契约。仅在上面的日志条件下集成日志记录。
将重复的组织查询、身份解析、查询解析和访问工作流提取到 helpers/ 中。将小的纯转换和常量保留在 utils/ 中。将较大的集成拆分为聚焦的工作流文件;路由应编排这些步骤,而不是包含整个集成。
数据获取和状态
在适当的时候通过服务器控制器加载初始页面数据,并将类型化的、可序列化的属性传递给交互式组件。对独立的请求使用 Promise.all;保留对依赖操作的处理顺序。
使用 TanStack Query 进行远程客户端数据。提供共享的默认值和一个 useApiQuery 包装器,该包装器解包 API 信封、传播错误并转发查询取消信号。查询键必须包含相关的身份/资源/过滤器输入。在每个请求范围内创建服务器端查询客户端;在变更后失效或更新受影响的查询条目。
对共享的交互状态(例如选择、筛选器、面板和命令状态)使用特定于特性的 Zustand 存储。订阅单个切片或使用浅层选择来获取分组值。保持设置器/操作的显式性,并在其拥有的资源更改时重置状态。当 Query 已拥有远程数据时,避免在 Zustand 中维护第二个独立的副本。
对组件拥有的输入、脏状态、验证消息和保存状态使用本地状态。防止重复提交并在失败时保留用户输入。没有必需的表单库。
取消已过时的搜索/过滤请求并在清理时中止。防止过时的完成更新新资源。仅将实际的取消归类为取消;不要静默地吞掉网络故障。
对于大型列表,在有用时应用虚拟化和分页,并包含加载、空、错误、重试和列表结束状态。当资源/过滤器更改时,适当地重置分页和选择。保持键盘导航与虚拟化行一致。
可选的跨标签同步
默认情况下为相关的共享变更启用同步,除非用户禁用它。不要将其添加到没有同步需求的隔离状态。
在 lib/local-sync/ 中使用一个类型化的事件注册表,在 events/ 下使用领域事件模式,以及一个共享的 hooks/useLocalSync.ts 钩子。从 Zod 推断负载类型,并验证传出和传入的消息。
存储事件实现应通知其他标签页和同标签页的监听器,使用瞬态事件负载而不是持久化的应用状态,并清理监听器。现有的等效传输方式也是可接受的。防止回显循环和重复处理;不要同步密钥。事件应更新或失效规范的拥有者状态。跨标签事件是 UI 协调,而不是授权或保证的交付。
身份验证和授权
当身份验证在范围内时应用此部分。保持实现与提供商无关,并适应选定的服务或自定义后端。
- 对于电子邮件 OTP 身份验证,将发送代码、验证代码和注销流程分开。
…
(已为翻译截断)