CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/读 platform 源码
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

读 platform 源码

对照 tracks/roadmaps 页面与 lib/api.ts 走读。

40 分钟

读 platform 源码

这一课你将能做到

  1. 在 platform 仓库里用「URL → 文件 → 组件 → lib → 后端」的路径独立走读任意一个页面
  2. 对照源码指出第 1-10 课每个知识点的真实用例(文件路由、RSC、缓存、BFF、门控、Tailwind 全部对上号)
  3. 具备给 platform 提 PR 的代码定位能力:改一个需求,知道动哪几个文件

为什么要学

前 10 课的所有概念都还停在「示例项目」里。这一课把它们放进一个真实生产仓库——你正在用的学习平台,前端目录就是 platform/,Next.js 15 + App Router + Tailwind + 自研 Spring Boot 后端。读真实源码和读教程是两种能力:教程代码干净漂亮,真实代码有历史包袱、有取舍、有「为什么这么写」的深意。面试里最有说服力的一句话是「我读过一个生产级 Next.js 项目的完整源码」,而读源码的能力可以迁移到任何新仓库。

走读方法论先立住——固定四步,任何页面通用:

① 从 URL 定位文件:/tracks/java/lessons/intro → src/app/tracks/[slug]/lessons/[lessonSlug]/page.tsx
② 读 page.tsx 骨架:取了什么数据、渲染了哪些组件、有没有特殊导出(dynamic/metadata)
③ 沿 import 追依赖:@/lib/api(取数)→ @/components(交互)→ 逐个打开
④ 对照后端:lib/api 里调的 /api/xxx,对应 Spring Boot 的 Controller

先建立图像

platform 的目录全景(带功能注释版):

platform/
├── next.config.ts              # Next 配置
├── .env.example                # 环境变量样例(NEXT_PUBLIC_API_URL 等)
├── src/
│   ├── app/                    # ★ App Router 路由
│   │   ├── page.tsx            # 首页
│   │   ├── layout.tsx          # 根布局(全站导航/页脚)
│   │   ├── login/ register/    # 登录注册页
│   │   ├── tracks/
│   │   │   ├── page.tsx                # 课程列表
│   │   │   └── [slug]/
│   │   │       ├── page.tsx            # 课程详情
│   │   │       └── lessons/[lessonSlug]/
│   │   │           └── page.tsx        # ★ 课时页(本课主角)
│   │   ├── practice/[trackSlug]/       # 练习判题页
│   │   ├── roadmaps/[slug]/            # 路线图(嵌套动态段)
│   │   └── api/roadmap-node/[slug]/[nodeKey]/
│   │       └── route.ts        # ★ BFF 转发接口(第 7 课真实用例)
│   ├── components/             # 业务组件(LessonQuiz、LoginGate…)
│   └── lib/                    # ★ 服务层(下表详解)
└── ...

src/lib/ 服务层速查——这层是全站的「关节」:

文件职责对应课程
api.ts所有后端调用的封装(fetch + 类型)第 5 课 数据获取
apiBase.tsAPI 地址分流(公网/内网)第 5 课 环境变量
serverAuth.ts服务端读 Cookie 取 token第 10 课 认证
mdxSanitize.ts课时内容消毒第 8 课 MDX
assignment.ts
作业/内容渲染
第 8 课 MDX
code.ts读课时代码文件第 4 课 Server 能力

跟着做

步骤 1:打开仓库,先跑起来

仓库根目录下(platform 是其中的前端子目录):

cd platform
npm install
npm run dev     # 本地开发,通常代理到本地或测试环境后端

跑不起来没关系——本课以静态走读为主,但能跑起来体验最好。

步骤 2:走读课时页(全站最核心页面)

打开 src/app/tracks/[slug]/lessons/[lessonSlug]/page.tsx,先看开头(节选):

import Link from "next/link";
import { notFound } from "next/navigation";
import { LessonPractice } from "@/components/LessonPractice";
import { LessonQuiz } from "@/components/LessonQuiz";
import { LoginGate } from "@/components/LoginGate";
import { getTrack, getLessons, getLessonDetail } from "@/lib/api";
import { getServerToken } from "@/lib/serverAuth";
import { renderAssignmentMd, renderMdSource } from "@/lib/assignment";
import { readCodeFiles } from "@/lib/code";

interface PageProps {
  params: Promise<{ slug: string; lessonSlug: string }>;  // ← Next 15:Promise 参数
}

export const dynamic = "force-dynamic";  // ← 第 6 课:个人化页面强制动态

export default async function LessonPage({ params }: PageProps) {
  const { slug, lessonSlug } = await params;             // ← 第 3 课 动态段
  const token = await getServerToken();                  // ← 第 10 课 登录态
  const track = await getTrack(slug);
  const lesson = await getLessonDetail(slug, lessonSlug, token);
  if (!track || !lesson) notFound();                     // ← 第 3 课 404
  // ...
}

20 行代码里至少 6 个前 10 课的知识点。继续读主体逻辑:

  const lessons = (await getLessons(slug, token)) ?? [];
  const sorted = [...lessons].sort((a, b) => a.orderIndex - b.orderIndex);
  const idx = sorted.findIndex((l) => l.slug === lesson.slug);
  const prev = sorted[idx - 1] ?? null;   // 上一课/下一课:前端算序
  const next = sorted[idx + 1] ?? null;
  const locked = lesson.locked === true;  // 付费/登录门控标记来自后端

  const courseContent = lesson.contentMd
    ? await renderMdSource(lesson.contentMd)        // ← 第 8 课:字符串渲染 MDX
    : locked && lesson.preview
      ? await renderMdSource(lesson.preview)        // 锁定课时只渲染预览
      : null;

最后 JSX 里组件各就各位:LoginGate 包住需要登录的区域、LessonQuiz/LessonPractice 是交互块、ProgressToggle 管进度打钩。整个页面是一个 async Server 组件,交互全部下沉到 'use client' 的子组件——第 4 课的拆分套路的生产版。

步骤 3:追进 lib/api.ts,看取数层

打开 src/lib/api.ts,找 getLessonDetail(节选示意):

export async function getLessonDetail(
  trackSlug: string,
  lessonSlug: string,
  token?: string
): Promise<ApiLesson | null> {
  const res = await fetch(
    `${getApiBase()}/api/tracks/${trackSlug}/lessons/${lessonSlug}`,
    {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
      cache: "no-store",   // ← 带登录态的数据:绝不缓存(第 5 课铁律)
    }
  );
  if (!res.ok) return null;
  return res.json();
}

再打开 src/lib/apiBase.ts(全文很短):

export function getApiBase(): string {
  if (typeof window === "undefined") {
    const internal = process.env.API_INTERNAL_URL?.trim();
    if (internal) return internal.replace(/\/$/, "");
  }
  return getPublicApiBase();
}

typeof window === "undefined" = 当前在服务端 → 走内网直连后端;浏览器端走公网域名。第 5 课「环境变量分流」在这里落地。

步骤 4:走读 BFF 接口

打开 src/app/api/roadmap-node/[slug]/[nodeKey]/route.ts(节选结构):

import { NextRequest, NextResponse } from "next/server";

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ slug: string; nodeKey: string }> }
) {
  const { slug, nodeKey } = await params;
  const token = request.cookies.get("token")?.value;   // ← 读登录态
  const res = await fetch(`${backend}/api/roadmap-node/${slug}/${nodeKey}`, {
    headers: token ? { Authorization: `Bearer ${token}` } : {},
    cache: "no-store",
  });
  return NextResponse.json(await res.json(), { status: res.status });
}

这就是第 7 课 BFF 转发模式的仓库实体:藏后端地址、透传 token、按需聚合。你可以对照第 7 课自己写的 /api/progress,结构几乎一样——教程代码和生产代码的差距比想象小。

步骤 5:读练习判题页的数据流

src/app/practice/[trackSlug]/page.tsx 负责练习入口;判题交互在组件里(src/components/ 下的练习相关组件)走「Client 组件提交答案 → 后端判题接口 → 返回结果渲染」。自己走一遍四步法:URL → 页面文件 → 组件 → lib/api 里的调用。走读完写一句话总结数据流,这一步没有标准答案,重要的是把方法用熟。

步骤 6:给自己出一个「改造任务」

模拟真实需求:「课时页给锁定课时加一个『开通会员』按钮」。定位路径:

  1. UI 改动 → lessons/[lessonSlug]/page.tsx 的 locked 分支附近
  2. 按钮点击行为 → 需要交互 → 新建 Client 组件放进 src/components/
  3. 样式 → 复用 gh-box 语义类 + Tailwind 工具类
  4. 数据 → lib/api.ts 加一个会员状态接口调用(token 透传、no-store)

这一个任务串起 4、5、9 三课知识。看完本课,你可以真去仓库提 PR。

讲清楚要点

  1. 页面薄、服务厚是本仓库的核心架构原则:page.tsx 只做「取数 + 组装」,逻辑都在 src/lib/,交互都在 src/components/。好处:页面可替换(重构路由不动业务)、服务层可测试、新人可按层阅读。
  2. 「个人化数据 no-store,公开数据可缓存」贯穿全仓库:凡是函数签名里带 token 的取数函数,清一色 no-store;公开目录数据才有缓存策略。这是第 5 课原则的生产级一致性执行。
  3. 门控是「标记 + 组件」双层:后端返回 locked: true(业务判断在 Java 侧),前端只负责按标记渲染 LoginGate/预览内容——判断逻辑不重复实现,前端永远不自己算「该不该锁」。
  4. Next 15 语法痕迹是判读版本的锚点:params: Promise<...>、await cookies() 说明是 15+ 语法。读任何 Next 仓库先扫一眼这两个写法,能立刻校准你的语法记忆。
  5. 读源码先读 lib/ 再读 components/:初学者爱从 UI 读起,但 UI 是结果不是原因;lib 层的 api.ts、serverAuth.ts 才是数据流的源头。先源头后展示,理解成本最低。

你可能会懵的地方

  • 为什么不把课程数据构建时静态化(SSG)? 因为课时页带登录态(锁定、进度、是否免费预览),响应因人而异——上一课的结论「用户级页面必须动态」。首页、课程列表这类公开页则可以走静态/ISR,各自选型,仓库里 dynamic = "force-dynamic" 只出现在个人化页面,印证了「按路由最小干预」。

  • lib/api.ts 里为什么返回 null 而不是 throw? 取数失败分两种:数据不存在(正常业务,渲染 404 页)和基础设施故障(异常,该报错)。课程不存在返回 null 让页面 notFound(),是语义化处理;全抛异常会让「查无此课」和「后端宕机」混为一谈。

  • 为什么组件不直接 fetch 后端,非要经过 lib? 三笔账:类型账(TS 接口集中定义)、缓存账(策略一处管理)、环境账(内网/公网地址分流只写一次)。绕过 lib 直接 fetch 的代码在这个仓库里应该被 code review 打回。

  • readCodeFiles 这种「服务端读文件」为什么安全? 它跑在 Server 组件里,读的是服务器上的路径;若等价逻辑放到 Client 组件,路径和文件内容都会暴露到浏览器。第 4 课「Server 组件能访问后端资源」的生产实例。

  • 我怎么确认某段交互是 Client 组件? 打开组件文件看第一行有没有 "use client";或看有没有 useState/onClick。快速判断法:能用键盘/鼠标产生「即时反应」的部分,几乎都是 Client。

错法对照

错法问题正解
只盯 JSX 读页面只看到皮,看不到数据流先读取数段(await 序列)再读 JSX
把教程语法硬套源码(如 params.slug)源码是 Next 15 Promise 语法,理解错位以仓库实际写法为准,await params
改 UI 时顺手在 page.tsx 里加 useStatepage 是 Server 组件,直接报错交互逻辑拆 Client 组件放 src/components
在组件里绕过 lib 直接 fetch类型、缓存、环境三处失控一律经 src/lib/api.ts
在前端复算 locked 逻辑(如 lessonCount > 3)与后端规则漂移,出现「前端解锁」以后端 locked 标记为唯一事实

卡点急救

  1. npm run dev 起不来或页面白屏:大概率是没配 .env.local(对照 .env.example 补 NEXT_PUBLIC_API_URL 等),或本地没有后端可连——先只浏览公开页面,登录相关功能连测试环境。
  2. 找不到某个 URL 对应的文件:记住动态段规则反向映射(/tracks/x/lessons/y → [slug]/lessons/[lessonSlug]);再不行全局搜页面里的独特文案定位文件。
  3. 看不懂某个 lib 函数被谁调用:编辑器里用「查找引用」(VS Code: Shift+F12)向上追;配合全局搜函数名,一般两三层就能追到页面。

面试常问

  • 介绍一下你读过的这个 Next.js 项目的架构。 参考回答:学习平台前端,Next.js 15 App Router + TypeScript + Tailwind。三层结构:src/app 承载路由与页面(薄组装层)、src/lib 承载数据获取与鉴权工具(api.ts 统一 fetch 封装、serverAuth.ts 读 Cookie、apiBase.ts 做公网/内网地址分流)、src/components 承载交互组件。个人化页面(课时页、练习页)force-dynamic 并对带 token 的请求 no-store;公开内容可缓存。后端 Spring Boot 提供 REST,Next 通过 BFF route.ts 转发并透传 token。

  • 这个项目里 RSC/Client 组件是怎么划分的? 页面主体全为 async Server 组件:服务端读 token、取课程数据、渲染 MDX 内容、判断锁定状态;只有测验答题、进度打钩、代码练习这类需要状态与事件的单元拆成 'use client' 组件。数据由 Server 取好后经 props 下传,凭证(token)从不进入客户端 JS。

  • 如果让你在这个项目加一个新页面,你的步骤? 先定路由与渲染策略(是否个人化 → 决定 dynamic 配置);在 lib/api.ts 补取数函数与 TS 类型(确定缓存策略);page.tsx 做组装(数据不存在走 notFound);可复用交互拆 Client 组件进 components;样式复用语义类 + Tailwind;最后用 build 输出核对新路由的渲染标记符合预期。

自测三问

  1. 从 URL /tracks/nextjs-essentials/lessons/next-intro 说出完整文件路径,并说出它为什么必须是动态渲染。
  2. getApiBase() 里 typeof window === "undefined" 判断的是什么?两个地址各在什么环境用?
  3. 课时锁定判断为什么放在后端?前端拿到 locked 标记后做哪两件事?

小结一句话

读真实仓库用固定四步:URL 定位文件、读页面骨架、沿 import 追 lib、对照后端;platform 的每一行代码都能和前 10 课的知识点对上号。

延伸

  • Next.js 官方文档:App Router
  • 下一课:收官——综合练习,把你的项目构建并部署上线。
加载本节练习…
← 上一课认证与登录门控下一课 →综合练习与部署