服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月3日 00:02

Docker 多阶段构建怎么用?减小镜像体积的最佳方法

多阶段构建让 Dockerfile 分多个阶段——编译阶段用完整环境构建产物,运行阶段只拷贝最终产物。结果:镜像从 1GB+ 缩小到 50MB 以下。问题:单阶段构建镜像太大node:20 镜像 1.1GB,加上 node_modules 几百 MB,最终镜像可能 1.5GB。但运行时只需要 dist/ 目录和 node 生产依赖。多阶段构建--from=builder 从第一阶段拷贝指定目录。node:20-slim 只有 200MB,最终镜像约 300MB——比单阶段小 5 倍。Go 应用:极致压缩Go 编译出单个二进制文件,运行时不需要 Go 环境:scratch 是空镜像——里面只有你拷贝的二进制文件。最终镜像可能只有 10-20MB。前端应用:Nginx 托管静态文件前端只需要构建后的 HTML/CSS/JS,不需要 node_modules。nginx:alpine 只有 25MB。COPY --from 的其他用法不限于同一 Dockerfile 的阶段,可以从其他镜像拷贝:从 Caddy 官方镜像里只拷贝二进制文件,不用自己安装。关键要点每个 FROM 开始一个新阶段,只有最后一个阶段的产物进入最终镜像用 AS 命名阶段,COPY --from=名称 引用运行阶段尽量用 slim/alpine 变体不要把源码、编译工具、dev 依赖带进运行镜像
服务端阅读 06月2日 23:47

Docker 容器数据怎么备份和恢复?Volume 备份和数据库导出实战

Docker 容器的数据在 Volume 里——备份 Volume 就是备份数据。两种方式:直接备份 Volume 文件,或从容器内导出。方法一:备份 Volume 目录简单粗暴,但要求停掉容器或确保数据一致性(数据库正在写入时备份可能损坏)。方法二:用临时容器备份不停容器,用 --volumes-from 挂载同一个 Volume:临时容器挂载 pg_data(只读)和宿主机 /backup 目录,把数据打包到宿主机。数据库导出(推荐)数据库不适合直接拷文件——文件可能处于不一致状态。用数据库的导出工具:在容器里执行:导出的是 SQL 文本,保证逻辑一致性,可以跨版本恢复。恢复数据库恢复用 psql/mysql 命令,不用拷文件。自动化备份备份文件要推到远程存储(S3、OSS),不要只存在本机——本机挂了备份也没了。Docker Volume 备份 vs 数据库导出Volume 备份:快,但不保证一致性,适合非数据库文件数据库导出:慢,但保证一致性,适合数据库两者配合:Volume 备份应用配置/上传文件,数据库导出业务数据
服务端阅读 06月2日 23:47

Docker 容器怎么监控?Prometheus + Grafana 和告警配置实战

Docker 监控分两层:容器级(CPU/内存/网络)和应用级(QPS/延迟/错误率)。容器级用 cAdvisor + Prometheus,应用级用代码埋点 + Prometheus。统一在 Grafana 看板和告警。最快上手:docker stats只适合临时查看,没有历史数据、没有告警、没有可视化。Prometheus + cAdvisor:容器级监控cAdvisor 采集容器的 CPU、内存、网络、磁盘 IO 指标,Prometheus 存储和查询,Grafana 可视化。cAdvisor 暴露 /metrics 端点,Prometheus 定时拉取。Grafana 导入 Docker dashboard 模板(ID 893)即可看到容器资源看板。关键监控指标| 指标 | 含义 | 告警阈值 ||------|------|----------|| containercpuusagesecondstotal | CPU 使用率 | > 80% 持续 5 分钟 || containermemoryusagebytes | 内存使用 | > 90% 限制值 || containernetworkreceivebytestotal | 网络接收 | 异常突增 || containeroom_events | OOM 次数 | > 0 立即告警 |OOM 事件是最严重的——容器被杀意味着应用中断,必须立即处理。告警配置Prometheus Alertmanager 配置告警规则和通知渠道:通知渠道支持邮件、Slack、钉钉、企业微信。日志监控监控 + 日志配合:告警触发后用 docker logs 查看对应容器的日志,定位问题。如果用了 Loki,直接在 Grafana 里查日志,不用跳到终端。
服务端阅读 06月2日 23:47

Docker 编排工具怎么选?Docker Compose、Swarm 和 Kubernetes 对比

Docker 编排工具解决的是多容器管理问题——手动 docker run 管几个容器还行,几十个就力不从心了。三个主流方案:Compose(开发)、Swarm(小团队)、Kubernetes(生产)。Docker Compose:开发环境首选一条 docker compose up -d 启动所有服务。适合本地开发、CI 测试、小型项目部署。局限:单机运行,不支持自动扩缩容,没有滚动更新,没有服务发现。服务挂了需要手动重启。Docker Swarm:轻量级集群Swarm 内置在 Docker 里,不需要额外安装。支持多节点集群、滚动更新、服务发现、内置负载均衡。局限:功能比 K8s 少很多——没有自动扩缩容(HPA)、没有自定义调度、没有 CRD 扩展。社区在萎缩,新项目不建议选 Swarm。Kubernetes:生产标准K8s 是容器编排的事实标准。功能完整:自动扩缩容、滚动更新、服务发现、配置管理、密钥管理、持久卷、网络策略、审计日志。K8s 的代价:学习曲线陡、运维复杂、需要专门的平台团队。小项目用 K8s 是杀鸡用牛刀。怎么选1-5 个服务:Docker Compose,简单够用5-20 个服务,单集群:Swarm 或 K8s(建议直接 K8s,Swarm 没有未来)20+ 个服务,多环境:Kubernetes云上部署:直接用云厂商的 K8s 托管服务(EKS/GKE/AKS),别自己搭一句话:开发用 Compose,生产用 K8s。Swarm 跳过。
服务端阅读 06月2日 23:45

Docker 容器权限怎么管?rootless 模式、用户映射和安全加固

Docker 容器默认以 root 运行——如果容器被攻破,攻击者获得容器内的 root 权限,可能逃逸到宿主机。安全的第一步:不要用 root 跑应用。不要用 root 跑应用USER appuser 之后的所有操作(CMD、ENTRYPOINT)都以 appuser 身份执行。即使应用有漏洞,攻击者只有普通用户权限。docker run 指定用户如果 Dockerfile 里没有 USER,运行时指定:覆盖 Dockerfile 里的 USER 指令。UID 1000 通常是宿主机的第一个普通用户。只读文件系统把容器文件系统挂载为只读,攻击者无法写入恶意文件。 给 /tmp 临时写入空间(很多应用需要)。限制能力(Capabilities)Linux capabilities 是细粒度的权限控制。Docker 默认给容器少量 capability,但还可以更严格:删掉所有能力, 只加回绑定 1024 以下端口的能力。按需添加,不给多余的权限。Rootless DockerDocker 默认以 root 运行 daemon——即使容器里不是 root,daemon 本身是 root。Rootless 模式让整个 Docker daemon 以普通用户运行:Rootless 模式的限制:不能绑定 80/443 端口(需要 1024 以上),没有 cgroup 资源限制,网络功能受限。适合 CI/CD 和开发环境,生产环境不成熟。资源限制防止恶意或失控的容器吃光宿主机资源:限制容器最多 100 个进程,防止 fork bomb。安全检查清单容器不以 root 运行(USER 指令或 --user 参数)文件系统只读(--read-only)最小 capability(--cap-drop ALL + 按需 cap-add)资源限制(memory、cpus、pids)不挂载 Docker socket(-v /var/run/docker.sock 是最危险的操作)镜像来自可信源(不用来历不明的镜像)
服务端阅读 06月2日 23:45

Docker 容器怎么更新?滚动更新、蓝绿部署和回滚策略

Docker 容器是不可变的——更新不是在容器里改代码,而是用新镜像替换旧容器。关键是怎么替换才能不停服。最简单的更新:停旧启新有停机时间。适合内部工具、非关键服务。停机时间取决于镜像拉取速度和启动时间。滚动更新:Docker SwarmSwarm 的滚动更新逐个替换容器,始终保持部分实例在线:更新过程中部分容器跑 v1,部分跑 v2,用户无感知。如果新版本有问题,回滚:蓝绿部署准备两套环境(蓝和绿),切换流量:蓝绿部署零停机,但需要双倍资源。适合关键服务的版本更新。Docker Compose 更新Compose 会检测镜像是否变化,只重建有变更的容器。但不是滚动更新——旧容器先停再启新容器,有短暂停机。健康检查确保更新安全更新后容器必须通过健康检查才算成功。连续失败 3 次,标记为 unhealthy。配合滚动更新,unhealthy 的新容器不会继续替换旧容器。回滚策略不管用什么更新方式,都要有回滚能力:保留旧版本镜像(不要 docker rmi 删掉)数据库变更要向后兼容(新版本能读旧 schema)配置文件版本化管理(.env 用 git 追踪)回滚就是 docker run 旧版本镜像。如果数据库迁移了且不兼容旧版本,回滚也救不了——所以数据库变更要分步做。
服务端阅读 06月2日 23:43

Docker 容器日志怎么查看和分析?日志驱动和集中化方案

Docker 日志分两类:容器标准输出(docker logs)和应用自己的日志文件。docker logs应用必须把日志输出到 stdout/stderr 才能用 docker logs 查看。日志轮转(防止磁盘爆满)每个容器最多 3 个文件,每个 10MB,超过自动轮转。集中化日志多容器环境下逐个 docker logs 不现实。用日志聚合:Loki + Grafana:轻量,推荐ELK Stack:功能全但重Fluentd:日志收集器排查技巧grep 过滤:docker logs my-container 2>&1 | grep ERROR多容器:docker compose logs -f启动失败:docker logs $(docker ps -lq)
服务端阅读 06月2日 23:43

Docker 容器网络不通怎么排查?DNS、端口和防火墙问题定位

容器网络问题分三类:容器间不通、容器访问不了外网、外部访问不了容器。按层级排查:DNS → 端口映射 → 防火墙。容器间通信问题不在同一个网络:docker network connect my-network container-b。Docker 内置 DNS 只在同网络内生效。用服务名(ping redis)而不是 IP。ping redis 失败但 IP 能通是 DNS 问题。端口映射问题常见错误:忘了 -p 参数、端口冲突、只绑定了 127.0.0.1。容器访问不了外网DNS 失败时手动指定:docker run --dns 8.8.8.8。防火墙拦截Docker 修改 iptables 实现端口映射。ufw 开启但没放行 Docker 链会拦截流量。确保 DEFAULTFORWARDPOLICY="ACCEPT"。网络模式bridge(默认):需要端口映射和 DNShost:直接用宿主机网络,性能好但隔离差none:无网络生产环境推荐 bridge + 自定义网络。
服务端阅读 06月2日 23:43

Docker 容器内存泄漏怎么排查?OOM 和内存增长定位实战

容器内存持续增长最终被 OOM Kill 是 Docker 最常见的问题之一。排查步骤:确认是不是真的泄漏 → 定位哪个容器 → 找到代码里的泄漏点。确认问题内存持续增长不回落大概率是泄漏。短暂增长后稳定不一定是泄漏——可能是 JVM/Node.js 堆还没触发 GC。检查 OOM 事件容器内定位语言级分析Node.js:用 v8.writeHeapSnapshot() 生成堆快照Java:用 jmap -dump 生成堆 dumpPython:用 tracemalloc 或 objgraph用 Chrome DevTools 或 MAT 分析堆快照,找 Retained Size 最大的对象。常见原因全局列表/缓存只加不删(最常见)事件监听器没有 removeEventListener数据库连接没有释放临时缓解只治标不治本,排查期间防止 OOM Kill。
服务端阅读 06月2日 23:43

Docker 怎么配合微服务架构?一个服务一个容器的实践方法

微服务架构的核心是一个服务一个进程——Docker 的容器天然就是为此设计的。每个微服务打包成独立镜像,独立部署、独立扩缩容。一个服务一个容器每个服务有自己的 Dockerfile,独立构建和部署。服务间通信同一 Docker 网络内,容器用服务名互相访问(http://user-service:3000),Docker 内置 DNS 自动解析。跨主机通信需要额外方案:Docker Swarm 用 Overlay 网络,Kubernetes 用 Service。扩缩容Compose 的 --scale 适合开发测试。生产环境用 K8s 的 HPA 自动扩缩容。配置管理不要把配置写死在镜像里。用环境变量注入,不同环境用不同 .env 文件。Docker 微服务的局限Docker Compose 适合 5-10 个服务的项目。超过 10 个服务应该用 Kubernetes——它提供自动扩缩容、滚动更新、服务发现、健康检查等编排能力。
服务端阅读 06月2日 23:40

Docker 跑数据库怎么做?数据持久化、备份和生产环境注意事项

Docker 跑数据库能快速搭建开发环境,但生产环境要格外注意数据持久化、性能和备份。一句话:开发环境放心用,生产环境谨慎用。数据持久化:第一优先级容器删了数据就没了——这是 Docker 跑数据库最大的风险。必须用 Volume 挂载数据目录:# MySQL:把 /var/lib/mysql 挂到宿主机docker run -d -v mysql_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=secret mysql:8# PostgreSQL:把 /var/lib/postgresql/data 挂出来docker run -d -v pg_data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret postgres:16-v mysql_data:/var/lib/mysql 用 Docker Volume(推荐)。也可以用宿主机目录 -v /data/mysql:/var/lib/mysql,但权限管理更麻烦。验证持久化:docker rm -f 删掉容器后重新 docker run(同一个 volume),数据还在。开发环境:docker-compose 一键搭建# docker-compose.ymlservices: db: image: postgres:16 environment: POSTGRES_DB: myapp POSTGRES_PASSWORD: dev123 ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7 ports: - "6379:6379"volumes: pg_data:docker compose up -d 一键启动 PostgreSQL + Redis。ports 映射到宿主机,本地开发工具(psql、DBeaver)直连。生产环境的注意事项1. 不要把数据库密码放环境变量环境变量对容器内所有进程可见,docker inspect 也能看到。用 Docker Secrets(Swarm)或文件挂载:docker run -d -v /secrets/db_password.txt:/run/secrets/db_password:ro mysql:82. 设置资源限制数据库不加限制会吃光宿主机内存:docker run -d --memory=4g --memory-swap=4g --cpus=2 mysql:8同时在数据库配置里调整缓存大小(MySQL 的 innodb_buffer_pool_size,PG 的 shared_buffers),让它和容器内存限制匹配。3. 健康检查healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 3容器"运行中"不代表数据库"可用"——MySQL 可能还在做崩溃恢复。健康检查确保只有真正可用的数据库接收连接。备份# MySQL 备份docker exec mysql_container mysqldump -u root -p secret mydb > backup.sql# PostgreSQL 备份docker exec pg_container pg_dump -U postgres mydb > backup.sql定时备份:用 cron + 上面的命令,把 SQL 文件推到 S3 或其他存储。不要把备份文件存在容器里。生产环境该不该用 Docker 跑数据库?适合:中小规模、团队能力有限、需要快速部署。用 Docker 统一运维比手动管理多台数据库服务器简单。不适合:高性能场景(I/O 密集型数据库)、需要极致调优、合规要求禁止容器化。Docker 的网络和存储有一层抽象,理论上比裸机慢 5-10%。大多数场景这 5% 可以忽略,但对 TPS 要求极高的系统可能不行。折中方案:数据库跑在宿主机或托管数据库(RDS),只把应用放在 Docker 里。
服务端阅读 06月2日 23:16

Next.js App Router 怎么定义路由?动态路由、布局嵌套和 loading 详解

Next.js App Router 用文件系统定义路由——文件夹结构就是 URL 结构。每个路由三要素:page.tsx(页面)、layout.tsx(布局)、loading.tsx(加载状态)。基本路由映射app/├── page.tsx → /├── about/│ └── page.tsx → /about├── blog/│ ├── page.tsx → /blog│ └── [slug]/│ └── page.tsx → /blog/:slug(动态路由)├── dashboard/│ ├── layout.tsx → dashboard 共享布局│ ├── page.tsx → /dashboard│ └── settings/│ └── page.tsx → /dashboard/settingspage.tsx 是必须的——没有 page.tsx 的文件夹不构成路由。其他文件(layout、loading、error)都是可选的。动态路由// app/blog/[slug]/page.tsxexport default function BlogPost({ params }: { params: { slug: string } }) { return <h1>文章: {params.slug}</h1>;}访问 /blog/hello-world 时,params.slug = "hello-world"。Catch-all 路由用 [...slug]:匹配 /shop/clothes/shirts 这样的多级路径,params.slug = ["clothes", "shirts"]。路由组用 (groupName) 创建不反映在 URL 中的分组:app/├── (marketing)/│ ├── about/page.tsx → /about(URL 里没有 marketing)│ └── pricing/page.tsx → /pricing├── (shop)/│ ├── products/page.tsx → /products│ └── cart/page.tsx → /cart路由组的用途:给一组页面共享 layout 而不影响 URL。比如 (marketing) 组用营销页布局,(shop) 组用商店布局。布局嵌套layout.tsx 会嵌套——子路由的 layout 包在父 layout 里面:// app/layout.tsx — 根布局(必有,包含 html/body)export default function RootLayout({ children }) { return ( <html> <body> <nav>全局导航</nav> {children} </body> </html> );}// app/dashboard/layout.tsx — dashboard 布局export default function DashboardLayout({ children }) { return ( <div className="flex"> <aside>侧边栏</aside> <main>{children}</main> </div> );}从 /dashboard 切换到 /dashboard/settings 时,根布局和 dashboard 布局都不会重新渲染。只有 children 对应的 page.tsx 更新。loading.tsx:自动 Suspense// app/dashboard/loading.tsxexport default function Loading() { return <div className="animate-pulse">加载中...</div>;}Next.js 自动用 Suspense 包裹 page.tsx,加载时显示 loading.tsx。不需要手写 useState 管理加载状态。error.tsx:错误边界// app/error.tsx — 必须是 Client Component'use client'export default function Error({ error, reset }) { return ( <div> <p>出错了: {error.message}</p> <button onClick={reset}>重试</button> </div> );}error.tsx 捕获子组件的运行时错误,显示错误界面。reset 函数重新渲染出错的组件。程序化导航'use client'import { useRouter } from 'next/navigation';function LoginButton() { const router = useRouter(); return <button onClick={() => router.push('/dashboard')}>登录</button>;}router.push() 客户端跳转,router.replace() 替换当前历史记录(不能回退),router.back() 返回上一页。Server Component 里用 redirect():import { redirect } from 'next/navigation';export default async function Page() { const session = await getSession(); if (!session) redirect('/login'); // ...}redirect 在服务端执行,用户看不到中间页面。
服务端阅读 06月2日 23:15

Next.js SSR、SSG 和 ISR 有什么区别?怎么选?

SSR、SSG、ISR 是三种不同的页面渲染策略,区别在于 HTML 什么时候生成。选哪个取决于数据的更新频率和页面的实时性要求。三种策略对比| 策略 | HTML 生成时机 | 适合场景 | 性能 ||------|-------------|----------|------|| SSG | 构建时 | 博客、文档、营销页 | 最快(CDN 缓存) || SSR | 每次请求时 | 仪表盘、个人主页 | 中等(服务端计算) || ISR | 构建时 + 定时更新 | 商品列表、新闻 | 接近 SSG 的速度 |SSG(Static Site Generation)构建时生成 HTML,部署后不变化。速度最快——CDN 直接返回静态文件,零服务端计算。// App Router 默认就是 SSG// 没有 dynamic 数据获取的页面自动静态生成export default function AboutPage() { return <h1>关于我们</h1>;}// 带数据的 SSG:构建时获取async function BlogList() { const posts = await db.post.findMany(); // 构建时执行 return posts.map(p => <article key={p.id}>{p.title}</article>);}局限:数据变化后需要重新构建部署。适合不常变的内容。SSR(Server-Side Rendering)每次请求时生成 HTML。数据始终最新,但每次请求都有服务端计算开销。// App Router: 使用动态数据获取自动触发 SSRasync function Dashboard() { const stats = await fetch('https://api.example.com/stats', { cache: 'no-store' // 不缓存,每次请求重新获取 }).then(r => r.json()); return <div>{stats.users} 用户</div>;}cache: 'no-store' 告诉 Next.js 这个请求不能缓存,必须每次执行。适合实时数据。ISR(Incremental Static Regeneration)SSG 的升级版——静态生成 HTML,但后台定时重新生成。兼具 SSG 的速度和数据的新鲜度。async function ProductList() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // 每 3600 秒(1 小时)重新验证 }).then(r => r.json()); return products.map(p => <div key={p.id}>{p.name}</div>);}ISR 的工作流程:第一个用户请求 → 返回缓存的静态 HTML(快)后台检查 revalidate 时间是否到期到期后重新生成 HTML,替换旧缓存下一个用户请求 → 返回新生成的 HTML关键点:用户永远看到的是缓存的页面(快),后台异步更新。最坏情况数据延迟 revalidate 秒。怎么选选 SSG:内容几乎不变(文档、博客、营销页)选 ISR:内容定期更新(商品列表、新闻、排行榜),能接受短暂延迟选 SSR:内容必须实时(仪表盘、用户个人页、搜索结果)常见错误:所有页面都用 SSR。大部分页面用 ISR 就够了——1-5 分钟的数据延迟用户感知不到,但性能提升显著。App Router 中的 revalidate 策略// 定时重新验证(ISR)fetch(url, { next: { revalidate: 60 } }); // 60 秒// 按需重新验证:修改数据后手动触发import { revalidateTag, revalidatePath } from 'next/cache';// 在 Server Action 里触发async function updateProduct(formData: FormData) { await db.product.update({ ... }); revalidateTag('products'); // 刷新所有 products 标记的缓存 revalidatePath('/products'); // 刷新 /products 页面}按需重新验证(On-Demand Revalidation)比定时更精准——数据变了立即刷新,没变就不浪费资源。
服务端阅读 06月2日 23:14

Next.js Pages Router 和 App Router 有什么区别?该不该迁移?

Pages Router 是 Next.js 的原始路由系统,App Router 是 13+ 引入的新系统。核心区别:App Router 基于 React Server Components,默认在服务端渲染;Pages Router 默认在客户端渲染。新项目用 App Router,老项目不急迁移。架构对比| 维度 | Pages Router | App Router ||------|-------------|------------|| 目录 | pages/ | app/ || 默认渲染 | 客户端 | 服务端(RSC) || 数据获取 | getServerSideProps / getStaticProps | async 组件 + fetch || 布局 | _app.tsx + 全局 Layout | 嵌套 layout.tsx || 路由 | 文件即路由 | 文件夹 + page.tsx || API | pages/api/ | app/ + route.ts || Loading | 手动管理 | loading.tsx 自动 |数据获取的变化Pages Router 用特殊函数获取数据:// Pages Routerexport async function getServerSideProps() { const data = await fetchData(); return { props: { data } };}export default function Page({ data }) { return <div>{data}</div>;}App Router 直接在组件里 async/await:// App Routerexport default async function Page() { const data = await fetchData(); // 服务端直接执行 return <div>{data}</div>;}App Router 的方式更直观——不需要记特殊函数名,数据获取就是普通的函数调用。布局系统Pages Router 的布局是全局的(_app.tsx),切换页面时整个布局重新渲染。App Router 支持嵌套布局——每个目录可以有 layout.tsx,子路由切换时父布局不重新渲染:app/├── layout.tsx # 根布局(导航栏、页脚)— 永远不重新渲染├── dashboard/│ ├── layout.tsx # 仪表盘布局(侧边栏)— 路由切换不重新渲染│ ├── page.tsx # /dashboard│ └── settings/│ └── page.tsx # /dashboard/settings用户从 /dashboard 切换到 /dashboard/settings 时,根布局和仪表盘布局都保持不变,只有 settings 的 page.tsx 重新渲染。这在 Pages Router 里做不到。何时迁移不急迁移的情况:项目稳定运行,没有性能问题团队不熟悉 RSC,迁移风险高大量自定义 _document.tsx / _app.tsx 逻辑应该迁移的情况:需要更好的首屏性能(RSC 显著减少 JS 体积)嵌套布局能解决当前的布局闪烁问题项目刚开始或处于早期迁移可以渐进式——两个路由可以共存。先把新页面用 App Router 写,老页面逐步迁移。两者可以共存pages/├── index.tsx # Pages Router 处理├── about.tsx # Pages Router 处理app/├── dashboard/│ └── page.tsx # App Router 处理Next.js 13+ 同时支持两套路由,同一个项目里渐进迁移。但要注意:同一路径不能两边都定义(pages/dashboard.tsx 和 app/dashboard/page.tsx 会冲突)。
服务端阅读 06月2日 23:13

Next.js 认证怎么做?NextAuth.js 配置 OAuth 和凭证登录实战

Next.js 认证最主流的方案是 NextAuth.js(v5 改名 Auth.js)。它处理了 OAuth、JWT、Session 管理等所有细节,30 分钟就能搭好 Google/GitHub 登录。最快上手:NextAuth.jsnpm install next-auth@beta// app/api/auth/[...nextauth]/route.tsimport NextAuth from 'next-auth';import GitHub from 'next-auth/providers/github';export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), ], pages: { signIn: '/login', // 自定义登录页 },});export const { GET, POST } = handlers;// middleware.ts — 保护路由export { auth as middleware } from './app/api/auth/[...nextauth]/route';export const config = { matcher: ['/dashboard/:path*'],};三步:配置 provider → 创建 API 路由 → 加中间件保护。用户访问 /dashboard 时如果没有登录,自动跳转到 /login。在 Server Component 里获取 Sessionimport { auth } from './app/api/auth/[...nextauth]/route';export default async function Dashboard() { const session = await auth(); if (!session) { redirect('/login'); } return <h1>欢迎, {session.user.name}</h1>;}auth() 在服务端获取 session,不需要客户端 JavaScript。这是 RSC 的优势——认证逻辑完全在服务端,客户端零开销。在 Client Component 里获取 Session'use client'import { useSession } from 'next-auth/react';export default function Profile() { const { data: session, status } = useSession(); if (status === 'loading') return <p>加载中...</p>; if (!session) return <p>请先登录</p>; return <p>{session.user.name}</p>;}useSession 通过轮询 /api/auth/session 获取 session。更高效的方式是用 SessionProvider:// app/providers.tsx'use client'import { SessionProvider } from 'next-auth/react';export function Providers({ children }) { return <SessionProvider>{children}</SessionProvider>;}包在 layout 里后,useSession 不再轮询,而是通过 Context 共享 session 数据。自定义登录页默认登录页太简陋。自定义页面:// app/login/page.tsximport { signIn } from '@/auth';export default function LoginPage() { return ( <form action={async () => { 'use server' await signIn('github'); }}> <button type="submit">用 GitHub 登录</button> </form> );}用 Server Action 触发 signIn,比客户端 signIn() 更简洁。凭证登录(用户名密码)OAuth 不够时,加 Credentials provider:import Credentials from 'next-auth/providers/credentials';import bcrypt from 'bcryptjs';export const { handlers, auth } = NextAuth({ providers: [ Credentials({ credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { const user = await db.user.findUnique({ where: { email: credentials.email }, }); if (!user) return null; const valid = await bcrypt.compare(credentials.password, user.passwordHash); if (!valid) return null; return { id: user.id, name: user.name, email: user.email }; }, }), ],});authorize 返回 null 表示认证失败,返回 user 对象表示成功。密码必须用 bcrypt 哈希,不要存明文。常见问题Session 过期后页面不刷新:用 SessionProvider 的 refetchInterval 定时刷新:<SessionProvider refetchInterval={300}> 每 5 分钟检查一次。OAuth 回调 404:确保回调 URL 在 OAuth provider 里配置正确。GitHub 在 Settings > Developer > OAuth Apps 里配。部署后 Cookie 不工作:NEXTAUTH_URL 环境变量必须设为生产域名。Vercel 部署时自动设置,其他平台需要手动配。
服务端阅读 06月2日 23:11

React Server Components 是什么?和 Client Components 怎么配合?

React Server Components(RSC)是只在服务端渲染的组件——它们的代码不会发送到浏览器。这是 React 架构的根本变化:组件不再默认跑在客户端,而是默认跑在服务端。RSC 解决什么问题传统 React 应用把所有组件代码打包成 JS 发给浏览器。一个列表页可能有 200KB 的 JS,但大部分是数据获取和渲染逻辑,用户交互只占一小部分。用户要等 JS 下载、解析、执行完才能看到页面。RSC 的解决方案:数据获取和渲染在服务端完成,只把 HTML 和少量交互代码发给浏览器。结果:更快的首屏、更小的 JS 包、更简单的数据获取。Server Component vs Client Component// Server Component(默认)— 不发 JS 给浏览器async function ArticleList() { const articles = await db.article.findMany(); // 直接查数据库 return ( <ul> {articles.map(a => <li key={a.id}>{a.title}</li>)} </ul> );}// Client Component — JS 会发给浏览器'use client'import { useState } from 'react';function SearchBox() { const [query, setQuery] = useState(''); // 需要状态 return <input value={query} onChange={e => setQuery(e.target.value)} />;}判断标准很简单:需要 useState、useEffect、onClick 等 React hooks/事件的就是 Client Component,否则就是 Server Component。数据获取方式的变化Pages Router 时代,客户端获取数据:// Pages Router — 客户端获取useEffect(() => { fetch('/api/articles').then(r => r.json()).then(setArticles);}, []);App Router + RSC,服务端直接获取:// App Router — 服务端获取async function Page() { const articles = await db.article.findMany(); return <ArticleList articles={articles} />;}不需要 API 路由,不需要 loading 状态管理,不需要客户端缓存。服务端拿到数据直接渲染成 HTML。组合模式Server Component 可以渲染 Client Component,但反过来不行:// Server Componentimport SearchBox from './SearchBox'; // Client Componentasync function Page() { const data = await fetchData(); return ( <div> <SearchBox /> {/* Client Component:交互 */} <ArticleList data={data} /> {/* Server Component:展示 */} </div> );}关键规则:Server Component 可以 import 和渲染 Client ComponentClient Component 不能 import Server ComponentServer Component 可以通过 props 把数据传给 Client Component(必须是可序列化的数据)什么时候用 Client Component只有这四种情况需要 'use client':需要交互(onClick、onChange)需要状态(useState、useReducer)需要生命周期(useEffect)需要浏览器 API(window、localStorage)其他都用 Server Component。一个常见错误:因为不熟悉 RSC 而给所有组件加 'use client'——这样 App Router 就退化成了 Pages Router,失去了 RSC 的性能优势。RSC 的局限Server Component 不能用 hooks(useState、useEffect)Server Component 不能用浏览器 API(window、document)Server Component 传给 Client Component 的 props 必须可序列化(不能传函数、类实例)调试更难——错误堆栈跨服务端和客户端RSC 仍然在快速演进,API 可能在未来版本变化。但方向是明确的:服务端渲染更多,客户端 JS 更少。
服务端阅读 06月2日 23:10

Next.js Server Actions 怎么用?表单提交、状态管理和安全验证

Server Actions 是 Next.js 的服务端函数——在服务端执行,客户端直接调用,不需要手写 API 路由。底层是 POST 请求 + 加密签名,比传统 fetch + API Route 更简洁。基本用法// app/actions.ts'use server'export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; await db.post.create({ data: { title, content } }); revalidatePath('/posts'); // 刷新缓存}// app/posts/page.tsximport { createPost } from './actions';export default function NewPost() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">发布</button> </form> );}没有 API 路由,没有 fetch,没有 JSON 序列化。表单提交直接触发服务端函数。Server Action 的本质Server Action 编译后变成一个 POST 请求:POST /_next/data/... HTTP/1.1Content-Type: text/x-componentNext-Action: hashed-action-idFormData bodyNext.js 自动给 action 生成加密 ID,客户端调用时带这个 ID。请求到达服务端后,Next.js 根据 ID 找到对应函数执行。所以即使有人猜到 URL,没有正确的 action ID 也无法调用。useActionState 管理状态'use client'import { useActionState } from 'react';import { createPost } from './actions';export default function NewPost() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { const result = await createPost(formData); return result; }, null ); return ( <form action={formAction}> <input name="title" /> <button type="submit" disabled={isPending}> {isPending ? '提交中...' : '发布'} </button> {state?.error && <p>{state.error}</p>} </form> );}isPending 在请求期间为 true,可以显示加载状态。state 保存上一次 action 的返回值。输入验证永远不要信任客户端数据。Server Action 里必须验证:'use server'import { z } from 'zod';const schema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10),});export async function createPost(formData: FormData) { const parsed = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), }); if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors }; } await db.post.create({ data: parsed.data }); revalidatePath('/posts');}Zod 验证比手写 if-else 更可靠,错误信息也结构化。revalidatePath 和 revalidateTagServer Action 修改数据后,需要告诉 Next.js 刷新缓存:revalidatePath('/posts'):刷新指定路径的缓存revalidateTag('posts'):刷新所有带 fetch(..., { next: { tags: ['posts'] } }) 标记的请求缓存revalidateTag 更灵活——一个 tag 可以对应多个页面,改一次全部刷新。安全注意事项Server Action 虽然有加密 ID 保护,但仍然是公开的 HTTP 端点:必须在 action 内做认证检查(getServerSession())必须验证输入(Zod)不要在 action 里返回敏感信息(返回值会发给客户端)'use server'export async function deleteAccount(formData: FormData) { const session = await getServerSession(); if (!session) throw new Error('Unauthorized'); // ...}和 API Route 的区别Server Action:适合表单提交、数据变更,自动处理 loading/error 状态API Route:适合第三方回调(Webhook)、文件上传、需要自定义响应头简单场景用 Server Action 更简洁。需要精细控制 HTTP 响应时用 API Route。
服务端阅读 06月2日 23:10

Next.js 中间件怎么用?认证重定向、A/B 测试和 Edge Runtime 限制

Next.js 中间件在请求到达页面之前执行,适合做认证检查、路由重写、A/B 测试等。它跑在 Edge Runtime 上,冷启动快但功能有限——不能用 Node.js API。基本用法在项目根目录创建 middleware.ts:import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) { // 请求到达页面之前执行 console.log(request.nextUrl.pathname); return NextResponse.next(); // 放行,继续处理请求}// 限制中间件只对匹配的路径生效export const config = { matcher: ['/dashboard/:path*', '/api/:path*'],};matcher 很重要——不设的话每个请求(包括静态资源)都经过中间件,拖慢性能。认证重定向最常见的用例:未登录用户跳转到登录页:export function middleware(request: NextRequest) { const token = request.cookies.get('session-token'); if (!token) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next();}export const config = { matcher: ['/dashboard/:path*'],};修改请求头中间件可以给请求加 header,页面里用 headers() 读取:export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); requestHeaders.set('x-pathname', request.nextUrl.pathname); return NextResponse.next({ request: { headers: requestHeaders }, });}页面里:import { headers } from 'next/headers';export default function Page() { const pathname = headers().get('x-pathname'); // ...}A/B 测试根据 Cookie 或随机分配给用户不同版本:export function middleware(request: NextRequest) { let variant = request.cookies.get('ab-variant')?.value; if (!variant) { variant = Math.random() > 0.5 ? 'A' : 'B'; } const response = NextResponse.next(); response.cookies.set('ab-variant', variant); // 重写到不同页面 if (variant === 'B' && request.nextUrl.pathname === '/pricing') { return NextResponse.rewrite(new URL('/pricing-b', request.url)); } return response;}NextResponse.rewrite 在服务端切换到另一个页面,用户看到的 URL 不变。限制:Edge Runtime中间件跑在 Edge Runtime,不是 Node.js。这意味着:不能用 fs、path、crypto(Node.js 内置模块)不能用 prisma、mongoose(数据库客户端)不能用 jsonwebtoken(需要 crypto)执行时间限制 30 秒(Vercel 上是 25 秒)简单的 token 验证、Cookie 操作没问题。复杂的认证逻辑(查数据库验证 token)不应该放中间件——放在页面组件或 API 路由里。常见问题中间件死循环:中间件重定向到 /login,但 /login 也匹配了 matcher。解决:matcher 排除 /login,或在中间件里判断路径。cookies 在 Server Component 里读不到:中间件 response.cookies.set() 设置的 Cookie 在同一请求的 Server Component 里读不到——因为中间件的 response 还没返回给浏览器。需要通过 request headers 传递。
服务端阅读 06月2日 23:08

Next.js 性能怎么优化?Server Components、图片和缓存策略实战

Next.js 性能优化从三个方向入手:减少客户端 JavaScript 体积、加快页面加载速度、优化数据获取策略。App Router 的 Server Components 天然比 Pages Router 快——大部分代码不发给浏览器。1. Server Components 优先App Router 默认所有组件都是 Server Component。只在需要交互(useState、useEffect、onClick)时才加 'use client':// Server Component(默认)— 不发 JS 给浏览器export default function Page() { return ( <div> <h1>标题</h1> {/* 静态内容 → Server Component */} <SearchBox /> {/* 需要交互 → Client Component */} <ArticleList articles={articles} /> {/* 纯展示 → Server Component */} </div> );}常见错误:整个页面都标 'use client'。正确做法是把交互部分抽成小的 Client Component,外层保持 Server Component。2. 图片优化next/image 自动做三件事:懒加载、按设备尺寸返回合适分辨率、WebP 格式转换。import Image from 'next/image';<Image src="/hero.jpg" width={1200} height={600} alt="描述" priority // 首屏图片加这个,跳过懒加载 placeholder="blur" // 模糊占位,加载时不会闪白/>必须填 width 和 height——防止布局偏移(CLS)。priority 只给首屏可见图片用,多了反而拖慢 LCP。外部图片需要配置域名白名单:// next.config.jsimages: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com' }, ],},3. 字体优化next/font 自动内联字体文件,消除 FOUT(字体闪烁):import { Inter } from 'next/font/google';const inter = Inter({ subsets: ['latin'] });export default function Layout({ children }) { return <body className={inter.className}>{children}</body>;}不要用 CSS @import 加载 Google Fonts——它会阻塞渲染。next/font 在构建时下载字体文件,零网络请求。4. 动态导入减少首屏 JS非首屏需要的组件用 dynamic 懒加载:import dynamic from 'next/dynamic';const HeavyChart = dynamic(() => import('./HeavyChart'), { loading: () => <div>加载中...</div>, ssr: false, // 纯客户端组件不需要 SSR});ssr: false 跳过服务端渲染——适合图表、编辑器这类重交互、不需要 SEO 的组件。5. 数据获取策略App Router 的 fetch 默认有缓存:// 缓存(默认)— 适合不常变的数据const data = await fetch('https://api.example.com/data');// 不缓存 — 每次请求都重新获取const data = await fetch('https://api.example.com/data', { cache: 'no-store'});// 定时重新验证 — 适合有一定延迟容忍的数据const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } // 1 小时后重新验证});ISR(Incremental Static Regeneration)是 Next.js 的杀手锏:静态页面生成后定时更新,不需要每次请求都渲染。revalidate 的时间根据数据变化频率设置——新闻列表 60 秒,配置数据 3600 秒。6. Layout 防止重复渲染App Router 的 layout.tsx 在导航时不会重新渲染。把不会变的 UI(导航栏、页脚)放在 layout 里:// app/layout.tsx — 只渲染一次export default function RootLayout({ children }) { return ( <html> <body> <nav>导航栏</nav> {children} {/* 只有这部分会随路由变化 */} <footer>页脚</footer> </body> </html> );}7. 分析打包体积npx @next/bundle-analyzer生成可视化报告,找出最大的包。常见问题:整个 lodash 被 import(改用 lodash-es 的按需导入)、moment.js 太大(改用 dayjs)、客户端不必要的包。性能优化检查清单Server Component 优先,'use client' 只在需要交互时用图片用 next/image + width/height + priority(首屏)字体用 next/font,不用 CSS @import非首屏组件 dynamic 导入fetch 设置合理的 cache/revalidate 策略静态 UI 放 layout,不放 pagebundle-analyzer 检查大包
服务端阅读 06月2日 23:07

Next.js 应用有哪些安全风险?环境变量泄露、XSS 和 CSRF 防护实战

Next.js 应用的安全风险主要来自三方面:服务端渲染(SSR)泄露敏感数据、API 路由缺乏认证、客户端代码暴露过多信息。逐个堵住就行。1. 环境变量:服务端 vs 客户端Next.js 的环境变量默认只在服务端可用。以 NEXT_PUBLIC_ 开头的才会暴露给浏览器。最常见的错误:把数据库密码、API Key 加了 NEXT_PUBLIC_ 前缀。# .env.local — 只在服务端可用DATABASE_URL=postgresql://...STRIPE_SECRET_KEY=sk_live_...# .env.local — 暴露给浏览器(谨慎使用)NEXT_PUBLIC_API_URL=https://api.example.comNEXT_PUBLIC_GA_ID=G-XXXXXXX检查方法:浏览器 F12 > Sources > 搜索你的密钥。如果搜到了,说明 NEXT_PUBLIC_ 用错了。2. API 路由必须加认证App Router 的 Route Handlers 默认没有任何认证:// app/api/users/route.ts — 裸奔的 API,任何人都能访问export async function GET() { const users = await db.user.findMany(); return Response.json(users);}加上认证中间件:import { getServerSession } from 'next-auth';export async function GET(request: Request) { const session = await getServerSession(); if (!session) { return new Response('Unauthorized', { status: 401 }); } const users = await db.user.findMany(); return Response.json(users);}更高效的方式:用 Next.js 中间件统一拦截,不用每个路由单独写:// middleware.tsimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) { const token = request.cookies.get('session-token'); if (!token && request.nextUrl.pathname.startsWith('/api/')) { return new Response('Unauthorized', { status: 401 }); } return NextResponse.next();}export const config = { matcher: '/api/:path*',};3. 防止 XSSReact 默认转义 HTML,XSS 风险不大。但 dangerouslySetInnerHTML 是例外:// 危险:用户输入的 HTML 直接渲染<div dangerouslySetInnerHTML={{ __html: userContent }} />// 安全:先用 DOMPurify 清洗import DOMPurify from 'isomorphic-dompurify';<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />另一个 XSS 来源:URL 参数直接插入页面。Next.js 的 useSearchParams() 读取的值如果不转义就渲染,可能被注入。4. CSRF 防护SameSite Cookie 是最简单的 CSRF 防护:// 设置 Cookie 时加 SameSitecookies().set('session-token', token, { httpOnly: true, secure: true, sameSite: 'lax', // 阻止跨站请求携带 Cookie path: '/', maxAge: 60 * 60 * 24 * 7,});sameSite: 'lax' 允许顶层导航携带 Cookie(用户点链接跳转正常),但阻止跨站 POST 请求携带。对大部分应用够用。5. Content Security PolicyCSP 限制页面能加载哪些外部资源,防止恶意脚本注入:// middleware.tsexport function middleware(request: NextRequest) { const response = NextResponse.next(); response.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-eval' https://cdn.example.com; style-src 'self' 'unsafe-inline';" ); return response;}CSP 配置比较繁琐,建议用 next-safe 库简化。先从 default-src 'self' 开始,逐步放宽需要的域名。6. 依赖安全审计npm audit # 检查已知漏洞npm audit fix # 自动修复npx better-npm-audit audit # 更详细的审计定期跑 npm audit,高危漏洞必须修。Next.js 自身也有安全更新——保持版本最新。安全检查清单环境变量没有敏感信息泄露到客户端API 路由都有认证Cookie 设置了 httpOnly + secure + sameSitedangerouslySetInnerHTML 用了 DOMPurify有 CSP 头npm audit 无高危漏洞