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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/综合练习与部署
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综合练习与部署

综合练习与部署

迁移 Course Explorer 到 Next;docker-essentials 部署。

45 分钟

综合练习与部署

这一课你将能做到

  1. 独立完成一个端到端小功能:「学习笔记」——动态路由 + Server 取数 + Server Action 写入 + 门控,把 12 课知识串成一条线
  2. 跑通 npm run build 并读懂路由输出表,修掉构建期暴露的类型与环境问题
  3. 完成一次真实部署(Vercel 或 Node 服务器 + Nginx),并掌握上线前检查清单

为什么要学

课程学到这里,你已经有了完整的技术地图:路由、RSC、取数缓存、渲染模式、接口、MDX、样式、认证,还走读过生产仓库。但「知道」和「做出来」之间隔着一个构建与部署的深渊——大量项目死在最后一公里:npm run build 时类型报错、部署后环境变量丢失、页面白屏、后端跨域 403。本课把最后这一公里走完。

综合练习的题目刻意选得小而全:「学习笔记」功能 200 行代码以内,却同时用到动态路由(第 3 课)、Server 取数(第 5 课)、Server Action 写入、门控(第 10 课)、Tailwind(第 9 课)。做完它,你就有了一个能写进简历、能当面演示、能被面试官追问细节的完整作品。

部署的意义再强调一次:面试官对「本地能跑」和「线上能访问」的项目,评估完全不同。一个有公网 URL 的作品集,胜过十份「代码在我电脑上」的简历。

先建立图像

本课的完整交付物架构:

┌─ 你的 Next.js 项目(生产构建产物)
│    /notes            列表页(Server:读当前用户笔记)
│    /notes/new        新建页(Server Action 提交)
│    /notes/[id]       详情页(动态段)
│    /api/auth/*       登录/登出 BFF(第 10 课代码复用)
└─ 数据来源(三选一,练习难度递增)
     ① 本地 JSON 文件(fs 读写,纯前端自洽)
     ② 内存 Map(重启即失,最快验证)
     ③ Spring Boot REST(真实架构,对接第 5 课封装)

部署形态对比:
  Vercel:git push 即部署,免费额度,零运维 ← 练习推荐
  Node 服务器:npm run build && npm run start,Nginx 反代 ← 企业常见
  Docker 容器:环境一致,本站 platform 就是 Dockerfile 部署

上线前后行为差异速查(踩坑高发区):

环节dev 行为生产行为
环境变量读 .env.local读部署平台配置(构建时注入)
未捕获错误红屏详细堆栈白屏/error.tsx,细节进日志
路由渲染一律动态、即时生效按 build 判定,静态页冻结
localhost指向你的机器指向服务器自己,注意 API 地址

跟着做

步骤 1:搭建笔记模块骨架

src/app/notes/
├── page.tsx          # 列表
├── new/page.tsx      # 新建
├── [id]/page.tsx     # 详情
└── actions.ts        # Server Action(写操作)

数据层用内存 Map(步骤最简,真实项目换成 fetch Spring Boot):

// src/lib/notes.ts
export interface Note {
  id: string;
  title: string;
  content: string;
  createdAt: string;
}

const notes = new Map<string, Note>();

export async function listNotes(): Promise<Note[]> {
  return [...notes.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

export async function getNote(id: string): Promise<Note | null> {
  return notes.get(id) ?? null;
}

export async function createNote(title: string, content: string): Promise<Note> {
  const note: Note = {
    id: crypto.randomUUID(),
    title,
    content,
    createdAt: new Date().toISOString(),
  };
  notes.set(note.id, note);
  return note;
}

步骤 2:列表页与详情页(Server 组件)

// src/app/notes/page.tsx
import Link from "next/link";
import { listNotes } from "@/lib/notes";

export const dynamic = "force-dynamic"; // 数据在内存里,必须动态

export default async function NotesPage() {
  const notes = await listNotes();
  return (
    <main className="mx-auto max-w-2xl p-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">我的学习笔记</h1>
        <Link className="rounded bg-blue-600 px-3 py-1.5 text-sm text-white" href="/notes/new">
          新建
        </Link>
      </div>
      <ul className="mt-4 space-y-2">
        {notes.map((n) => (
          <li key={n.id} className="rounded-lg border p-3 hover:bg-gray-50">
            <Link href={`/notes/${n.id}`} className="font-medium">
              {n.title}
            </Link>
            <p className="mt-1 text-xs text-gray-500">{n.createdAt}</p>
          </li>
        ))}
        {notes.length === 0 && <p className="text-sm text-gray-500">还没有笔记。</p>}
      </ul>
    </main>
  );
}
// src/app/notes/[id]/page.tsx
import { notFound } from "next/navigation";
import { getNote } from "@/lib/notes";

export default async function NotePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const note = await getNote(id);
  if (!note) notFound();
  return (
    <article className="mx-auto max-w-2xl p-6">
      <h1 className="text-2xl font-bold">{note!.title}</h1>
      <time className="mt-1 block text-xs text-gray-500">{note!.createdAt}</time>
      <div className="mt-4 whitespace-pre-wrap">{note!.content}</div>
    </article>
  );
}

步骤 3:Server Action 写入(本课新知识点)

// src/app/notes/actions.ts
"use server"; // 文件级声明:所有导出函数都是 Server Action

import { redirect } from "next/navigation";
import { createNote } from "@/lib/notes";

export async function addNote(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  const content = String(formData.get("content") ?? "").trim();
  if (!title) return { error: "标题不能为空" };

  const note = await createNote(title, content);
  redirect(`/notes/${note.id}`); // 成功后跳详情
}
// src/app/notes/new/page.tsx
import { addNote } from "../actions";

export default function NewNotePage() {
  return (
    <form action={addNote} className="mx-auto max-w-2xl space-y-3 p-6">
      <h1 className="text-2xl font-bold">新建笔记</h1>
      <input name="title" placeholder="标题" className="w-full rounded border p-2" />
      <textarea name="content" rows={8} placeholder="内容" className="w-full rounded border p-2" />
      <button className="rounded bg-blue-600 px-4 py-2 text-white">保存</button>
    </form>
  );
}

看这个 <form action={addNote}>:没有 onSubmit、没有 fetch、没有 'use client'——表单直接绑定服务端函数,提交时框架自动序列化 FormData 并在服务器执行。这就是 Server Action 的威力(渐进增强:JS 未加载完也能提交)。校验失败时返回 { error } 配合 useActionState 可在客户端展示,进阶自己扩展。

步骤 4:构建体检

npm run build

预期输出(读懂每一行):

Route (app)                    Size   Revalidate
○ /                            5 kB         ─
ƒ /api/login                   0 B          ─    ← 登录 BFF,动态
ƒ /notes                       …            ─    ← force-dynamic
ƒ /notes/[id]                  …            ─
● /notes/new                   …            ─    ← 纯表单页,静态

构建期常见报错与修法(都是真实高频):

  • Type error: Property 'x' does not exist:TS 类型问题,build 比 dev 严格,按提示补类型
  • Error occurred prerendering page:静态页在构建时被渲染了,但里面有服务端专用调用(如读环境变量缺失的接口)——给该页加 dynamic = "force-dynamic" 或修数据层
  • 内存 Map 的数据在静态页构建时被清空——注意我们给列表页标了动态,就是为了绕开「构建时快照」陷阱

步骤 5:部署到 Vercel(最快路径)

npm i -g vercel
vercel          # 首次会引导登录与项目关联
vercel --prod   # 部署生产

或推到 GitHub 后在 Vercel 控制台导入仓库(推荐,之后 push 即自动部署)。部署完成后必须配置:

Vercel 控制台 → Settings → Environment Variables:
  NEXT_PUBLIC_API_URL = https://你的后端域名     (浏览器要用,必须公网可达)
  API_INTERNAL_URL  = (如后端在别的平台,可留空回退公网)
配置后 Redeploy 一次才生效(环境变量是构建时注入的!)

对接真实 Spring Boot 后端时补两件事:后端给 Vercel 域名配 CORS 允许来源;或全部走 BFF 转发(服务端到服务端无 CORS 问题,本站选的就是后者)。

步骤 6:部署到自己的服务器(企业形态)

npm run build
npm run start -- -p 3000        # 服务器上跑 3000 端口

Nginx 反代配置要点:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;             # Next 需要正确 Host
        proxy_set_header X-Forwarded-Proto $scheme; # 让其知道外层是 http/https
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

进程守护用 pm2:pm2 start "npm run start" --name my-app。HTTPS 用 certbot 一条命令配 Let's Encrypt。本站 platform 的 Dockerfile 做的事同构:构建 → 生产依赖 → npm run start,理解了这一步就看懂了那个文件。

讲清楚要点

  1. Server Action 补齐了「写操作」的拼图:读用 async Server 组件,写用 "use server" 函数;表单直接 action={fn} 绑定,省掉整条「onSubmit → fetch → 处理响应」链路。它和 Route Handler 的分工在第 7 课已述:页面内变更用 Action,公网接口用 route.ts。
  2. build 是第一道生产安检:TS 全量类型检查、ESLint、静态页预渲染试跑,三关都在这里。dev 里藏的所有偷懒(any 满天飞、Server API 误用)都会在 build 暴露——所以「本地 build 一次再提交」是纪律。
  3. 环境变量是部署期问题,不是代码问题:代码里 process.env.X 写对只是及格线,生产环境「值在哪配、何时注入、能否公网可达」才是重点。Vercel 改变量必须 Redeploy;服务器部署用启动环境变量或 .env.production。排查线上白屏第一步永远是「打开部署平台的环境变量页」。
  4. 动态与静态的最终判定以 build 输出为准:你以为静态的页面可能因读了 cookies 变成 ƒ,你以为动态的页面可能被 Next 静态化。上线路前把路由表过一遍,和产品预期逐行核对。
  5. 上线检查清单(背下来):① build 零报错 ② 环境变量配齐且密钥未用 NEXT_PUBLIC_ 前缀 ③ 路由表逐行核对渲染模式 ④ HTTPS 生效 ⑤ 后端 CORS/内网地址正确 ⑥ 错误页(not-found/error)存在 ⑦ 登录 Cookie 的 secure 在生产为 true ⑧ 移动端过一遍核心流程。

你可能会懵的地方

  • 部署到 Vercel 后接口全 404 / 跨域报错? 分两查:Route Handler 的 404 → 确认 build 输出里有对应 ƒ 路由;后端请求报 CORS → 浏览器直连第三方域名被拦,把请求改走你的 BFF(route.ts 转发),服务端请求不存在 CORS。

  • 内存 Map 重启数据就没了,生产怎么存? 练习用内存是为了聚焦流程。生产三选一:对接 Spring Boot(本课架构推荐)、托管数据库(Vercel Postgres/PlanetScale)、或 Serverless 环境下用带持久层的 BaaS。注意 Vercel 的 Serverless 函数没有常驻内存,Map 在生产必然失效——这本身就是个好的面试谈资。

  • 为什么 dev 没问题,build 报一堆类型错? dev 按需编译且容错高;build 跑全量 tsc。说明代码里一直有类型隐患,只是 dev 没走到那些分支。正确心态:build 报错是帮你排雷,逐个修完代码质量上一个台阶。

  • Server Action 安全吗?等于把后端逻辑暴露了? Action 会编译成一个可被 POST 调用的端点,所以它就是公开接口,必须像对待 API 一样做鉴权与输入校验(本课笔记 Action 在真实项目里要先验登录态再写库)。误解「Action 是内部私有函数」是常见漏洞来源。

  • 静态导出(output: "export")能不能部署我的项目? 只有全部页面纯静态(无 route.ts、无 Server Action、无动态读取)才行。有门控、有接口的项目必须 Node 服务器或 Vercel 全功能运行时。判断法:build 输出里有 ƒ 就别想纯静态托管。

错法对照

错法问题正解
改完代码不 build 直接部署类型错/预渲染错在线上才炸提交前本地 npm run build 过一遍
生产忘配环境变量或没 Redeploy接口请求打到 localhost 全挂部署平台配置 + 触发重新构建
Server Action 里不校验登录直接写库任何人可调用的写接口Action 内先 getServerToken 验证再操作
Vercel 上 NEXT_PUBLIC_API_URL 填内网地址浏览器无法访问内网公开变量公网地址,服务端专用走内网变量
Nginx 反代后重定向/表单异常Next 拿不到真实 Host/协议配 proxy_set_header Host 与 X-Forwarded-Proto

卡点急救

  1. 线上页面 500:看部署平台日志(Vercel Logs / pm2 logs)。高频原因:环境变量缺失、后端不可达、Server Action 里抛异常。本课的 error.tsx 兜底 + 日志是唯一真相。
  2. 部署后样式丢失/类名没生效:Tailwind 构建没扫到文件(检查扫描配置包含 src 全目录)或线上加载了旧缓存(强刷 + 检查资源版本号)。
  3. Vercel 免费版函数冷启动慢:首请求 1-2s 属正常。缓解:公开数据走静态/ISR 减少 Server 调用;对延迟敏感的把后端部署到同区域。

面试常问

  • 你的 Next.js 项目是怎么部署的?上线前做了哪些检查? 参考回答:开发期本地 build 先过全量类型检查与预渲染安检;生产部署两条路径都实践过——Vercel(git push 自动构建,环境变量在平台配置、变更需 Redeploy)与自建服务器(npm run build + npm run start,pm2 守护、Nginx 反代并透传 Host/X-Forwarded 头、certbot 上 HTTPS)。上线清单:路由表核对渲染模式、环境变量齐备且密钥不分发到客户端、错误页兜底、Cookie secure、后端 CORS/内网直连验证。

  • Server Action 和传统表单提交相比改变了什么?要注意什么? 传统链路是 onSubmit 拦截 → fetch → 手动处理响应与重定向;Server Action 让表单 action 直接绑定服务端函数,框架负责序列化、调用与返回,天然渐进增强(无 JS 也可提交)。安全上它是公网可达的 POST 端点,必须做鉴权与输入校验;重定向用 redirect 抛出式 API;配合 useActionState 可回传校验错误。

  • 如果项目流量涨了,你会怎么扩展这个 Next.js 服务? 先分层看:静态资产(SSG/ISR 页面)CDN 承接,扩容压力集中在动态路由;动态页可加缓存层(Data Cache + revalidate 策略、或前端加 Redis/Nginx 缓存);Node 实例水平扩容 + 负载均衡(注意无状态化,会话数据外置);后端 API 独立扩展;观测 TTFB 与错误率定位下一瓶颈。本站的 Docker + Nginx 架构就是这套思路的落地。

自测三问

  1. Server Action 写数据比 Client fetch + 接口少了哪些代码?什么场景仍必须用 route.ts?
  2. build 输出里出现意外的 ƒ 标记,你的排查步骤是什么?
  3. 列出上线检查清单中你认为最容易漏的三项,并说明漏掉的后果。

小结一句话

从 build 体检到环境变量注入,从 Server Action 写操作到 Vercel/服务器两种部署形态,走完这最后一公里,你就完成了从「会写 Next.js」到「能交付 Next.js 产品」的全部闭环。

延伸

  • Next.js 官方文档:Deploying
  • 下一步:把综合练习部署上线后,回到 platform 仓库挑一个真实需求提交 PR——用本 track 学到的一切,参与你每天都在用的平台。
加载本节练习…
← 上一课读 platform 源码