首页
/ bulletproof-react Next.js Pages 应用:app 层与 pages 层分离的项目结构与本地开发流

bulletproof-react Next.js Pages 应用:app 层与 pages 层分离的项目结构与本地开发流

2026-09-05 17:14:45作者:蔡怀权

本篇围绕 bulletproof-react 仓库中 Next.js Pages 应用 的启动流程与项目结构展开:先完成 Node 20+ / Yarn 1.22+ 环境下的环境配置、Mock 服务器与开发服务启动,再深入解析该应用“业务代码收敛在 app 目录、pages 目录仅做 Next.js 入口再导出”的双层结构设计及其服务端数据预取实现。读完后你将掌握该应用的完整本地运行方式,并能理解为什么 Pages Router 应用要刻意绕开 pages 目录直接写业务代码。

环境要求与启动命令

官方文档列出的前置条件为:

  • Node 20+
  • Yarn 1.22+

进入 apps/nextjs-pages 目录后,按如下步骤初始化:

# 克隆仓库后进入 Pages Router 应用目录
cd bulletproof-react
cd apps/nextjs-pages
cp .env.example .env
yarn install

其中 cp .env.example .env 一步很关键。该应用通过 src/config/env.ts 中的 Zod schema 对环境变量做运行时强校验,createEnv 会解析以下变量并在缺失时直接抛出带字段清单的错误:

内部变量 读取的环境变量 约束与默认值
API_URL NEXT_PUBLIC_API_URL 必填字符串
ENABLE_API_MOCKING NEXT_PUBLIC_ENABLE_API_MOCKING 可选,只接受 'true'/'false',会被转换为布尔值
APP_URL NEXT_PUBLIC_URL 可选,默认 http://localhost:3000
APP_MOCK_API_PORT NEXT_PUBLIC_MOCK_API_PORT 可选,默认 '8080'

也就是说,不复制 .env.example 就运行应用,会在解析 API_URL 时立刻失败。NEXT_PUBLIC_ 前缀变量会被 Next.js 暴露给浏览器端代码,与 src/lib/api-client.ts 中发起的 HTTP 请求配合使用。

先启动 Mock 服务器:yarn run-mock-server

文档明确要求:运行应用前必须先启动 Mock 服务器,其 API 入口为 http://localhost:8080/api

该脚本在 package.json 中定义为 tsx ./mock-server.ts,实际实现见 mock-server.ts:它用 Express 挂载 cors(来源限制为 NEXT_PUBLIC_URLcredentials: true)、express.json()pino-http 日志(silent 级别),然后通过 @mswjs/http-middlewarecreateMiddleware(...handlers) 注入全部 MSW handler;在 initializeDb() 初始化基于 @mswjs/data 的内存数据库后,监听 NEXT_PUBLIC_MOCK_API_PORT 端口并打印 Mock DB initialized / Mock API server started 日志。

从源码结构看,Mock handler 定义在 src/testing/mocks/handlers(覆盖 auth、discussions、comments、teams、users 五组路由),数据库工厂在 src/testing/mocks/db.ts。测试与 E2E 也复用同一套 Mock 设施:test-e2e 脚本会先用 pm2 拉起 yarn run-mock-server,再执行 yarn playwright test(对应 e2e/tests 下的用例)。

开发模式:yarn dev

yarn dev 等价于 next dev,启动后访问 http://localhost:3000 即可在浏览器中查看应用(该默认地址与 env.tsAPP_URL 的默认值一致)。

package.json 中还提供了配套脚本,日常开发会频繁用到:

脚本 命令 用途
dev next dev 开发模式
build / start next build / next start 生产构建与启动
lint next lint 静态检查
test vitest 单元/组件测试
test-e2e pm2 start "yarn run-mock-server" ... && yarn playwright test 拉起 Mock 服务器后跑 Playwright E2E
check-types tsc --noEmit 类型检查
storybook / build-storybook storybook dev -p 6006 / storybook build 组件开发工作台
generate plop 基于 plopfile.cjsgenerators/component 生成组件脚手架
run-mock-server tsx ./mock-server.ts 独立 Mock API 服务

Next.js 侧配置非常克制,next.config.mjs 只开启了 reactStrictMode: true,路由与渲染行为全部由 Pages Router 约定驱动。

项目结构:app 是应用层,pages 只做再导出

README 给出的结构性解释是:pages 文件夹不够灵活、也不支持文件同层归置(collocation),因此把真正的应用层放在 app 目录——所有 feature 在这里组合;pages 目录则只负责再导出 Next.js 所需的页面文件和 getServerSideProps,让 Next.js 能够识别并按路由提供服务。

bulletproof-react 单向代码库结构示意,展示 app 层与 feature 的同层组织方式

这一设计在源码中体现得非常直接。src/pages/app/index.tsx 全部内容只有一行:

export { DashboardPage as default } from '@/app/pages/app/discussions/discussions';

真实的页面组件位于 src/app/pages/app/dashboard.tsx,它同时通过 getLayout 声明自己的布局:

DashboardPage.getLayout = (page: ReactElement) => {
  return (
    <DashboardLayout>
      <ContentLayout title="Dashboard">{page}</ContentLayout>
    </DashboardLayout>
  );
};

getLayout 的消费方是 src/pages/_app.tsx:这里定义并扩展了 NextPageWithLayout 类型,App 组件默认执行 Component.getLayout ?? ((page) => page),把整棵页面树包进 AppProvider。这套布局注入机制正是“页面组件写在哪里都行”的前提——布局信息与页面组件同文件共存,而不依赖目录位置。

一个应用层组件,两个路由入口

讨论详情页 src/app/pages/app/discussions/discussion.tsx 是这套结构的最佳例证。该文件同时导出三样东西:

  1. getServerSideProps:在服务端用独立的 QueryClient 依次 prefetchQuery 讨论详情与 prefetchInfiniteQuery 评论列表(均透传请求 Cookie),再把 dehydrate(queryClient) 的结果塞进 props.dehydratedState,完成 React Query 的服务端预取与序列化;
  2. DiscussionPage:纯展示组件,内部用 useDiscussion 消费水合后的数据,评论区块还套了一层 ErrorBoundary,失败时提示 “Failed to load comments. Try to refresh the page.”;
  3. PublicDiscussionPage:把 dehydratedState 包进 HydrationBoundary 后再渲染 DiscussionPage,作为公开路由的默认导出。

而两个 Next.js 入口只是各自挑取所需导出:

列表页 src/pages/app/discussions/index.tsx 则展示了应用层组件里“路由无关”的交互细节:DiscussionsPage 在用户悬停列表项时通过 queryClient.prefetchInfiniteQuery 预取该讨论的评论数据,而这份逻辑写在 src/app/pages/app/discussions/discussions.tsx 中,pages 目录完全不知道它的存在。

小结

apps/nextjs-pages 应用值得借鉴的点有两个层面:

  • 流程层面:环境变量经 Zod 强校验、Mock 服务器(Express + MSW http-middleware + @mswjs/data)与应用共用同一套 handler 定义、E2E 用 pm2 自动拉起 Mock 服务器,构成一条可独立复现的本地开发链路(Node 20+ / Yarn 1.22+,run-mock-server 需在 dev 之前启动);
  • 结构层面:业务代码、布局注入(getLayout)与服务端数据预取全部收敛在 app 应用层,pages 目录退化为单行再导出的路由适配器,从而在不放弃 Pages Router 的前提下实现 feature 级文件同层归置,并让同一个页面组件能同时挂载到受保护路由与公开路由两个入口。
登录后查看全文
热门项目推荐
相关项目推荐