阅读视图

发现新文章,点击刷新页面。

Next.js 教程系列(六)API Routes 与全栈开发基础

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第六章:API Routes 与全栈开发基础

教程简介

本章将带你深入理解 Next.js 的 API Routes 功能,掌握如何在同一个项目中实现前后端一体化开发。你将学会如何编写高质量的 API 接口,处理数据请求、鉴权、错误处理、性能优化等企业级场景。我们还会结合移动端适配、接口安全、全栈开发最佳实践,帮助你构建健壮、可维护的全栈应用。


理论讲解

1.1 API Routes 概述与架构演进

  • API Routes 是 Next.js 提供的后端接口开发能力,允许你在 /pages/api/app/api 目录下直接编写 Node.js 风格的接口。
  • 支持 RESTful、GraphQL、Webhooks、BFF(Backend For Frontend)等多种接口风格。
  • 与前端页面共享同一项目、同一依赖、同一部署流程,极大提升开发效率。
  • 适合中小型全栈项目、BFF 模式、原型开发、企业级微服务网关等场景。
  • 推荐分层架构:API 层(路由)、服务层(业务逻辑)、数据访问层(DAO),便于维护和扩展。
  • 支持接口版本管理(如 /api/v1/),便于平滑升级。

1.2 API Routes 的基本用法与进阶

  • /pages/api 目录下创建任意 .ts/.js 文件,即可自动成为一个 API 路由。
  • 每个文件导出一个默认函数,接收 req(请求对象)和 res(响应对象)。
  • 支持 GET、POST、PUT、DELETE 等 HTTP 方法。
  • 支持中间件、Cookie、Session、文件上传、数据库操作、Edge API Routes(边缘计算)、Server Actions(App Router)。
  • 推荐按业务模块拆分目录,如 /api/user//api/order//api/admin/

1.3 API Routes 与全栈开发

  • 前端页面通过 fetchaxiosSWRReact Query 等方式请求本地 API。
  • API 层可集成数据库(如 Prisma、TypeORM)、第三方服务(如 Stripe、微信支付)、缓存(如 Redis)、消息队列等。
  • 支持 SSR/SSG/ISR 等多种渲染模式下的数据获取。
  • 可作为微服务网关,聚合/转发后端服务。

1.4 企业级安全与权限控制

  • 鉴权:结合 JWT、Session、OAuth2、API Key、第三方登录(如 GitHub、微信)实现用户身份校验。
  • 接口限流:防止恶意刷接口,可用 Redis、内存、第三方服务实现。
  • CSRF/XSS 防护:合理设置 CORS、校验 Referer、过滤输入。
  • 敏感信息保护:环境变量、加密存储、日志脱敏、接口审计。
  • 接口签名与幂等性:对关键接口请求参数签名校验,防止篡改和重复提交。
  • 多租户支持:通过租户ID、Token、Header 实现多租户隔离。

1.5 性能优化与高可用

  • 缓存:HTTP 缓存头、Redis、CDN、接口预热、缓存穿透防护。
  • 批量/合并请求:减少接口数量,提升移动端体验。
  • 异步处理:如队列、定时任务,避免接口阻塞主线程。
  • 边缘计算:利用 Vercel Edge Functions 实现低延迟接口。
  • 接口降级与容灾:主服务异常时自动降级到备用方案。
  • 日志与监控:接口需有日志、埋点、告警,便于排查问题。

1.6 Mock、自动化测试与文档

  • Mock Service Worker(MSW):前端可独立开发调试,后端未完成时模拟接口。
  • Jest/Supertest:为 API Routes 编写单元测试、集成测试。
  • 契约测试:保证前后端数据结构一致。
  • OpenAPI/Swagger:自动生成接口文档,支持在线调试。
  • 类型注释:结合 TypeScript 类型,提升文档准确性。
  • CI 持续集成:自动化测试覆盖,保障接口质量。

1.7 多端适配与国际化

  • 响应式接口:根据 UA/参数返回不同数据结构,适配 Web/移动/小程序/桌面端。
  • 国际化:接口支持多语言返回,结合 i18n、next-intl。
  • 图片/多媒体优化:返回合适尺寸的图片链接,支持 WebP、AVIF。
  • 网络异常处理:接口需返回明确错误码和提示,前端可友好降级。

1.8 错误处理与监控

  • 统一错误码与响应格式:便于前端处理和埋点。
  • Sentry/LogRocket:接入错误监控,自动上报异常。
  • 慢接口告警:接口超时自动告警,便于性能优化。
  • 日志采集:接口请求日志、用户行为日志、异常日志。

代码示例

2.1 创建基础 API 路由

// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: 'Hello, Next.js API!' });
}

2.2 支持多种 HTTP 方法

// pages/api/user.ts
import type { NextApiRequest, NextApiResponse } from 'next';

let users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' },
];

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    res.status(200).json(users);
  } else if (req.method === 'POST') {
    const { name } = req.body;
    const newUser = { id: Date.now(), name };
    users.push(newUser);
    res.status(201).json(newUser);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.3 接口鉴权与 JWT 校验

// pages/api/profile.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET || 'demo_secret';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: '未登录' });
  }
  try {
    const user = jwt.verify(token, SECRET);
    res.status(200).json({ user });
  } catch {
    res.status(401).json({ error: 'Token 无效' });
  }
}

2.4 接口限流中间件

// lib/rateLimit.ts
const rateLimitMap = new Map<string, { count: number; last: number }>();

export function rateLimit(ip: string, limit = 10, windowMs = 60_000) {
  const now = Date.now();
  const entry = rateLimitMap.get(ip) || { count: 0, last: now };
  if (now - entry.last > windowMs) {
    rateLimitMap.set(ip, { count: 1, last: now });
    return false;
  }
  if (entry.count >= limit) return true;
  rateLimitMap.set(ip, { count: entry.count + 1, last: entry.last });
  return false;
}
// pages/api/secure-data.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rateLimit } from '@/lib/rateLimit';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const ip = req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress || '';
  if (rateLimit(ip, 5, 60_000)) {
    return res.status(429).json({ error: '请求过于频繁,请稍后再试' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.5 文件上传与表单处理

// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';

export const config = {
  api: {
    bodyParser: false,
  },
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const form = new formidable.IncomingForm();
  form.parse(req, (err, fields, files) => {
    if (err) return res.status(500).json({ error: '上传失败' });
    // 假设保存到本地
    const file = files.file as formidable.File;
    fs.renameSync(file.filepath, `./public/uploads/${file.originalFilename}`);
    res.status(200).json({ url: `/uploads/${file.originalFilename}` });
  });
}

2.6 数据库操作(以 Prisma 为例)

// pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    const products = await prisma.product.findMany();
    res.status(200).json(products);
  } else if (req.method === 'POST') {
    const { name, price } = req.body;
    const product = await prisma.product.create({ data: { name, price } });
    res.status(201).json(product);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.7 移动端分页与懒加载接口

// pages/api/feed.ts
import type { NextApiRequest, NextApiResponse } from 'next';

const allItems = Array.from({ length: 100 }).map((_, i) => ({ id: i + 1, title: `Item ${i + 1}` }));

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;
  res.status(200).json({
    items: allItems.slice(start, end),
    total: allItems.length,
    page,
    limit,
  });
}

2.8 错误处理与统一响应格式

// lib/response.ts
export function success(data: any) {
  return { code: 0, data };
}
export function error(message: string, code = 1) {
  return { code, message };
}
// pages/api/unified.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { success, error } from '@/lib/response';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    // ...业务逻辑
    res.status(200).json(success({ msg: 'ok' }));
  } catch (e) {
    res.status(500).json(error('服务器异常'));
  }
}

2.9 GraphQL API 基础

// pages/api/graphql.ts
import { ApolloServer, gql } from 'apollo-server-micro';

const typeDefs = gql`
  type User { id: ID! name: String! }
  type Query { user(id: ID!): User }
`;
const resolvers = {
  Query: {
    user: (_: any, { id }: { id: string }) => ({ id, name: '小明' }),
  },
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
export const config = { api: { bodyParser: false } };
export default apolloServer.createHandler({ path: '/api/graphql' });

2.10 API Key 鉴权与签名校验

// pages/api/secure.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

const API_KEY = process.env.API_KEY || 'demo_key';
const SECRET = process.env.API_SECRET || 'demo_secret';

function verifySignature(req: NextApiRequest) {
  const signature = req.headers['x-signature'] as string;
  const timestamp = req.headers['x-timestamp'] as string;
  const raw = `${timestamp}${API_KEY}${SECRET}`;
  const expected = crypto.createHash('sha256').update(raw).digest('hex');
  return signature === expected;
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.headers['x-api-key'] !== API_KEY || !verifySignature(req)) {
    return res.status(401).json({ error: '无效签名' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.11 幂等性与防重复提交

// lib/idempotency.ts
const idempotencyMap = new Map<string, number>();
export function isDuplicate(id: string) {
  if (idempotencyMap.has(id)) return true;
  idempotencyMap.set(id, Date.now());
  setTimeout(() => idempotencyMap.delete(id), 60_000);
  return false;
}
// pages/api/order.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { isDuplicate } from '@/lib/idempotency';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const id = req.headers['x-idempotency-key'] as string;
  if (!id || isDuplicate(id)) {
    return res.status(409).json({ error: '重复提交' });
  }
  // ...创建订单逻辑
  res.status(201).json({ message: '订单创建成功' });
}

2.12 Mock 与自动化测试

// __tests__/api/user.test.ts
import handler from '../../pages/api/user';
import { createMocks } from 'node-mocks-http';

test('GET /api/user', async () => {
  const { req, res } = createMocks({ method: 'GET' });
  await handler(req, res);
  expect(res._getStatusCode()).toBe(200);
  expect(JSON.parse(res._getData())).toEqual([
    { id: 1, name: '小明' },
    { id: 2, name: '小红' },
  ]);
});

2.13 OpenAPI/Swagger 自动生成文档

// scripts/generate-openapi.js
// 使用 swagger-jsdoc 自动生成 openapi.json
const swaggerJSDoc = require('swagger-jsdoc');
const options = { ... };
const openapiSpec = swaggerJSDoc(options);
require('fs').writeFileSync('openapi.json', JSON.stringify(openapiSpec, null, 2));

2.14 多语言接口

// pages/api/i18n.ts
import type { NextApiRequest, NextApiResponse } from 'next';
const messages = {
  zh: { hello: '你好' },
  en: { hello: 'Hello' },
};
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const lang = req.query.lang || 'zh';
  res.status(200).json({ message: messages[lang as string]?.hello || messages.zh.hello });
}

2.15 Sentry 错误监控集成

// pages/api/_middleware.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.SENTRY_DSN });
export default function middleware(req, ev) {
  try {
    return NextResponse.next();
  } catch (e) {
    Sentry.captureException(e);
    throw e;
  }
}

实战项目

3.1 构建全栈商品管理系统

目标:实现一个支持商品增删改查、图片上传、用户鉴权、移动端适配的全栈商品管理系统。

主要功能:
  1. 商品列表页:支持分页、搜索、移动端自适应。
  2. 商品详情页:展示商品图片、价格、描述。
  3. 后台管理页:支持商品的新增、编辑、删除。
  4. 用户登录鉴权:JWT 登录、接口权限控制。
  5. 图片上传:支持多图上传、进度显示。
  6. API 接口:全部基于 Next.js API Routes 实现。
  7. 错误处理与统一响应格式。
  8. 性能优化:接口缓存、按需加载。
  9. 日志与监控:接口请求日志、错误告警。
  10. 国际化与移动端适配。
关键代码片段:
// pages/products/index.tsx
import useSWR from 'swr';
import { useState } from 'react';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function ProductsPage() {
  const [page, setPage] = useState(1);
  const { data, error } = useSWR(`/api/feed?page=${page}&limit=10`, fetcher);
  if (error) return <div>加载失败</div>;
  if (!data) return <div>加载中...</div>;
  return (
    <div>
      <ul>
        {data.items.map((item: any) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
      <button disabled={page === 1} onClick={() => setPage(page - 1)}>上一页</button>
      <button disabled={data.items.length < 10} onClick={() => setPage(page + 1)}>下一页</button>
    </div>
  );
}
// pages/admin/products.tsx
import { useState } from 'react';

export default function AdminProducts() {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [msg, setMsg] = useState('');
  const handleAdd = async () => {
    const res = await fetch('/api/products', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, price: Number(price) }),
    });
    const data = await res.json();
    setMsg(data.code === 0 ? '添加成功' : data.message);
  };
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="商品名" />
      <input value={price} onChange={e => setPrice(e.target.value)} placeholder="价格" type="number" />
      <button onClick={handleAdd}>添加商品</button>
      <div>{msg}</div>
    </div>
  );
}

3.2 多租户商城 API 设计

  • 支持多租户(如不同商家/品牌)数据隔离。
  • 每个租户有独立的商品、订单、用户数据。
  • 通过 Header、Token、子域名等区分租户。
  • 接口返回结构需兼容多端(Web/移动/小程序)。
  • 支持租户级别的权限、限流、定制化配置。

3.3 订单系统与支付回调

  • 订单创建接口需防止重复提交(幂等性)。
  • 支付回调接口需校验签名、防止伪造。
  • 订单状态流转需有日志、告警。
  • 支持异步通知、消息推送。

3.4 实时消息与 WebSocket

  • 使用 nextjs-websocket、socket.io 实现实时订单状态推送。
  • 支持移动端、桌面端多端同步。
  • 接口需有鉴权、限流、断线重连机制。

3.5 复杂参数校验与批量导入导出

// lib/validate.ts
import * as z from 'zod';
export const productSchema = z.object({
  name: z.string().min(2),
  price: z.number().positive(),
});
// pages/api/products.ts
import { productSchema } from '@/lib/validate';
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const parse = productSchema.safeParse(req.body);
    if (!parse.success) {
      return res.status(400).json({ error: '参数校验失败', details: parse.error.errors });
    }
    // ...创建逻辑
  }
}
  • 支持 Excel/CSV 批量导入商品,接口需校验格式、异步处理。
  • 导出接口支持大数据量分片导出,防止超时。

3.6 长轮询与 WebSocket

  • 长轮询接口需有超时、重试机制。
  • WebSocket 需支持断线重连、心跳包。

常见问题与最佳实践

  • API 目录混乱:建议按业务模块拆分,统一命名规范。
  • 环境变量泄漏:敏感信息只放 .env,不要暴露到前端。
  • SSR/CSR 下接口复用:建议统一用 API Routes,避免重复实现。
  • 接口 Mock 不一致:Mock 数据与真实接口保持同步,自动化测试覆盖。
  • 接口文档滞后:用 OpenAPI/Swagger 自动生成,CI 校验。
  • 多端适配遗漏:接口返回结构需兼容 Web/移动/小程序。
  • 接口安全被忽视:务必加鉴权、限流、签名、日志。
  • 性能瓶颈:接口加缓存、CDN、边缘计算,定期分析慢接口。
  • 错误处理不统一:统一响应格式,前端友好提示。

片尾

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

Next.js水合那些事儿

嘿,大家好!今天咱们来聊聊Next.js里一个挺核心但听起来有点玄乎的概念——水合(Hydration)。如果你用过Next.js,估计对这个词不陌生,但它到底是啥?为啥这么重要?别急,咱慢慢掰扯清楚,保准让你看完后心里亮堂堂的!

一、技术背景:Next.js和SSR/SSG的那些事儿

先说说背景,Next.js是个超级火的React框架,最大的卖点就是能轻松搞定服务器端渲染(SSR)静态站点生成(SSG) 。为啥要整这些?简单来说,传统的React应用(也就是纯客户端渲染,CSR)是先把一个空壳HTML扔给浏览器,然后靠JavaScript在浏览器里把页面内容“画”出来。这虽然灵活,但有两个问题:

  1. 首屏加载慢:用户得等JavaScript加载完、执行完,才能看到页面内容。
  2. SEO不友好:搜索引擎爬虫看到的是空HTML,抓不到啥有用的内容。

Next.js站出来说:“我来解决!”通过SSR或SSG,Next.js能在服务器上把页面渲染好,生成完整的HTML,直接发给浏览器。这样用户能秒看到内容,搜索引擎也能开心地抓到数据。听起来很美对吧?但这时候,JavaScript咋办?页面送过去是静态的HTML,咋让它“活”起来,响应用户的点击、输入啥的?这就得靠水合了!

二、名词解释:水合到底是啥?

“水合”这个词听起来像化学实验,其实在Next.js里,它是个很形象的说法。想象一下,服务器送来的HTML就像一块干巴巴的海绵,里面有页面的结构和内容,但它还不会“动”。水合就是把这块干海绵泡进水里,让它吸饱React的JavaScript“水分”,变成一个能互动的、活生生的React应用。

具体点说,水合是Next.js(或者React)在浏览器端把服务器渲染的静态HTML跟React组件“绑定”起来的过程。服务器送来的HTML是死的,React通过水合给它注入灵魂,让页面能响应用户操作,比如点击按钮、切换tab啥的。

三、实现原理:水合咋干的?

好了,来说说水合咋实现的。别怕,咱用大白话讲,尽量不整那些让人犯困的技术术语。

  1. 服务器干活
    你用Next.js的getServerSideProps(SSR)或者getStaticProps(SSG)写页面逻辑,服务器会先把页面渲染成HTML。这HTML包含了页面的完整DOM结构和初始数据(比如从API拉来的列表数据)。这时候,Next.js还会把页面的初始状态(props)序列化成JSON,塞进一个叫__NEXT_DATA__的script标签里,一起发给浏览器。
  2. 浏览器接手
    浏览器收到HTML后,先展示出来,用户立马能看到内容(这叫首屏渲染)。与此同时,Next.js的JavaScript(也就是React代码)开始加载。加载完后,React会干啥?它会读取__NEXT_DATA__里的JSON数据,用来初始化React组件树。
  3. 水合过程
    React会把服务器送来的HTML结构跟自己的虚拟DOM对比一遍,确认没啥问题后,就把事件监听器(比如onClick、onChange)“挂”到对应的DOM节点上。这个过程就像给HTML装上“遥控器”,让它能响应用户的操作。完成之后,页面就从静态的HTML变成了一个动态的React应用。
  4. 注意事项
    水合有个关键点——服务器和客户端渲染的HTML必须一致。如果不一致(比如服务器少渲染了个div,或者客户端代码改了结构),React会报错,提示“水合失败”(hydration mismatch)。这也是开发Next.js时常踩的坑,后面会讲咋避免。

四、应用实例:水合在哪儿发挥作用?

好了,讲了这么多原理,咱们来看看水合在实际开发里咋用。以下是几个常见的场景:

1. 博客网站(SSG + 水合)

假设你用Next.js搭了个博客,文章页用getStaticProps生成静态HTML。用户访问文章页时,浏览器立马显示服务器渲染好的文章内容(标题、正文、图片啥的)。与此同时,React在后台加载并水合,页面“活”过来后,用户就能点“喜欢”按钮、评论啥的了。这就是水合的典型应用:首屏快如闪电,交互丝滑如初

2. 电商网站(SSR + 水合)

想象一个电商首页,商品列表通过getServerSideProps从服务器拉数据,渲染成HTML。用户打开页面,立马看到商品卡片、价格啥的。React加载完后,水合让页面支持交互,比如点击“加入购物车”会触发状态更新,页面动态刷新。水合保证了服务器和客户端的无缝衔接。

3. 动态表单

有个表单页面,服务器渲染出初始的输入框和按钮。用户看到页面后,React水合让表单支持输入验证、动态提示等功能。比如你输入邮箱,React会实时校验格式,给你个绿勾或红叉。

五、避坑指南:水合常见问题咋整?

水合虽然好用,但开发时容易踩坑。以下是几个常见问题和解决办法:

  1. 水合不匹配(Hydration Mismatch)
    为啥会不匹配?可能是服务器和客户端渲染的HTML不一样。比如:

    • 你在客户端用了useEffect动态加了个元素,服务器没这逻辑。
    • 你用了Date.now()Math.random(),导致服务器和客户端生成的HTML不同。
      解决办法
    • 确保服务器和客户端逻辑一致,动态内容放useEffect里(只在客户端跑)。
    • 用Next.js的dynamic导入动态加载纯客户端组件,跳过服务器渲染。
  2. 水合太慢
    如果JavaScript文件太大,加载和水合会拖慢页面交互。
    解决办法

    • 优化JavaScript包体积,用Next.js的动态导入(next/dynamic)按需加载组件。
    • next/script优化第三方脚本加载。
  3. SEO和水合的平衡
    水合是为了交互,但过度依赖客户端逻辑可能让SEO变差。
    解决办法

    • 尽量把核心内容放服务器渲染,客户端只处理交互逻辑。
    • next/head优化SEO元数据。

六、总结:水合的魅力

水合是Next.js的杀手锏之一,它让服务器渲染的静态页面“活”过来,既保证了首屏加载的快,又保留了React的动态交互能力。理解水合的原理,能帮你更好地用Next.js开发出高性能、SEO友好的Web应用。

简单总结下:

  • 水合干啥:把服务器渲染的HTML变成动态React应用。
  • 咋实现的:服务器送HTML+JSON,客户端加载React后绑定事件。
  • 用在哪儿:博客、电商、表单,哪儿都需要!
  • 咋用好:避免不匹配,优化加载速度,平衡SEO和交互。

希望这篇文章让你对Next.js的水合有了更清楚的认识!有啥问题,随时问我哈~

❌