Next.js 新手容易犯的错误 _ 性能优化与安全实践(6)

1 忘记为 Suspense 设置 key 属性

问题是什么?

在 React 和 Next.js 中,Suspense 是一个用于处理异步数据加载的工具。当你用 Suspense 包裹一个组件时,如果组件的内容需要根据某些条件(如 URL 参数、搜索参数等)动态变化,你需要通过 key 属性告诉 React,这个组件的内容需要重新渲染。

如果忘记设置 key 属性:

  • React 会以为组件的内容没有变化,不会重新触发加载逻辑。
  • 用户在切换内容时看到的可能是旧的内容,导致体验变差。

为什么需要 **key** 属性?
  • key 是 React 用来标识组件的唯一标识符。
  • key 变化时,React 会认为这是一个全新的组件,从而重新初始化组件并触发加载逻辑。

简单比喻key 就像一个标签,用来告诉 React:"这是一份全新的数据,请重新加载。" 如果没有 key,React 会以为这份数据和之前的一样,就直接跳过更新。


错误示例:没有设置 **key** 属性
场景:

我们有一个商品详情页面,商品内容根据搜索参数中的 id 动态加载。页面用 Suspense 包裹商品详情组件,加载数据时显示"加载中..."。

typescript 复制代码
import { Suspense } from "react";

function ProductDetail({ id }: { id: string }) {
  // 模拟数据加载
  const product = fetchProductById(id); // 假设 fetchProductById 是一个数据加载函数
  return <div>商品名称:{product.name}</div>;
}

export default function ProductPage({ searchParams }: { searchParams: { id: string } }) {
  return (
    <div>
      <h1>商品详情</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <ProductDetail id={searchParams.id} />
      </Suspense>
    </div>
  );
}
用户交互流程:
  1. 用户进入页面,searchParams.id1
  2. 页面显示商品 ID 为 1 的内容。
  3. 用户点击链接,切换到商品 ID 为 2
  4. 问题 :React 没有意识到 ProductDetail 的内容需要更新,因此不会触发重新加载,显示的仍然是 ID 为 1 的商品内容。
为什么会出问题?

React 会重用组件实例。如果没有明确告诉 React,ProductDetail 需要更新,React 会直接跳过渲染,导致显示的内容不符合用户的期望。


正确示例:设置 **key** 属性
修改代码:

我们通过 key 属性告诉 React,当 id 变化时,这是一个全新的组件,需要重新加载。

typescript 复制代码
import { Suspense } from "react";

function ProductDetail({ id }: { id: string }) {
  // 模拟数据加载
  const product = fetchProductById(id); // 假设 fetchProductById 是一个数据加载函数
  return <div>商品名称:{product.name}</div>;
}

export default function ProductPage({ searchParams }: { searchParams: { id: string } }) {
  return (
    <div>
      <h1>商品详情</h1>
      <Suspense fallback={<div>加载中...</div>}>
        {/* 使用 key 属性 */}
        <ProductDetail key={searchParams.id} id={searchParams.id} />
      </Suspense>
    </div>
  );
}
用户交互流程:
  1. 用户进入页面,searchParams.id1
  2. 页面显示商品 ID 为 1 的内容。
  3. 用户点击链接,切换到商品 ID 为 2
  4. React 检测到 **key** 属性变化
    • 将旧的 ProductDetail 卸载。
    • 重新加载 ID 为 2 的商品数据。
  5. 页面显示商品 ID 为 2 的内容。
为什么解决了问题?

key 属性让 React 知道这是一个全新的组件实例,从而触发重新加载逻辑,确保显示的内容是最新的。


更直观的现实类比

假设你去图书馆查书:

  • 没有设置 **key** 属性:你说了书的编号,但图书管理员以为你还是找之前那本书,所以没有换书给你。
  • 设置了 **key** 属性:你明确告诉图书管理员"我要的是编号为 2 的新书",他会帮你找到正确的书。

  • Suspense 在处理动态内容时,需要依赖 key 属性重新加载。
  • 忘记设置 key 属性,React 会认为内容没有变化,导致页面显示的内容不正确。
  • 通过 key 属性,让 React 能够正确识别组件的变化,触发重新加载。
关键点:
  • 动态内容(如根据 URL 参数变化)必须通过 key 属性告知 React 变化。
  • 使用 key={唯一标识} 是确保组件正确更新的核心机制。

2 误将页面从静态渲染切换为动态渲染

问题是什么?

在 Next.js 中,页面可以通过两种方式渲染:

  1. 静态渲染(Static Rendering):页面在构建时生成静态 HTML,用户访问时直接使用现成的 HTML。加载速度快,非常适合内容较少变动的页面。
  2. 动态渲染(Dynamic Rendering):页面在每次请求时重新生成 HTML,内容可以根据请求的上下文动态生成。虽然灵活,但每次请求都需要耗费计算资源,加载速度较慢。

核心问题

如果你在静态页面中使用了某些动态特性(如 searchParamscookiesheaders),Next.js 会自动将页面切换为动态渲染。这可能导致性能下降,而你可能并不需要动态渲染。


为什么静态渲染更优?
  1. 性能更好:静态页面直接从 CDN 或服务器返回 HTML,无需重新计算,加载更快。
  2. 更便宜:动态渲染需要消耗服务器资源,尤其是流量大时,成本更高。
  3. 更稳定:静态页面已经生成好,不容易因为动态计算出错。

常见错误:无意中触发动态渲染
错误示例:
typescript 复制代码
export default function ProductPage({ searchParams }: { searchParams: { id?: string } }) {
  const productId = searchParams.id; // 从 URL 中获取动态参数
  return <div>商品 ID:{productId}</div>;
}
为什么这是一个问题?
  • 使用了 searchParams 属性,这属于 动态 API
  • Next.js 无法在构建时预先知道 URL 参数(id)的值,因此必须在每次请求时动态生成 HTML。
  • 页面会自动切换为动态渲染,失去了静态渲染的性能优势。

如何确认页面是否被动态渲染?
  1. 运行 npm run build
  2. 查看构建日志:
    • 静态页面会标注为 **Static**
    • 动态页面会标注为 **Dynamic**
  3. 如果页面被标记为动态,可能是因为使用了动态 API(如 searchParamscookiesheaders)。

正确示例:避免动态渲染

方法 1:使用静态生成(Static Generation) 如果页面不需要根据用户请求动态生成内容,可以用 getStaticProps 在构建时生成 HTML。

typescript 复制代码
export async function getStaticProps() {
  const data = await fetch("https://api.example.com/products").then((res) => res.json());
  return {
    props: { products: data },
  };
}

export default function ProductPage({ products }: { products: any[] }) {
  return (
    <div>
      <h1>商品列表</h1>
      <ul>
        {products.map((product) => (
      <li key={product.id}>{product.name}</li>
    ))}
      </ul>
    </div>
  );
}

解释

  1. 数据在构建时一次性获取,页面被静态生成。
  2. 用户访问时直接返回生成好的 HTML,无需每次请求都重新加载数据。

方法 2:增量静态生成(Incremental Static Regeneration, ISR) 如果数据需要定期更新,可以使用 ISR。

typescript 复制代码
export async function getStaticProps() {
  const data = await fetch("https://api.example.com/products").then((res) => res.json());
  return {
    props: { products: data },
    revalidate: 60, // 每 60 秒重新生成一次页面
  };
}

export default function ProductPage({ products }: { products: any[] }) {
  return (
    <div>
      <h1>商品列表</h1>
      <ul>
        {products.map((product) => (
      <li key={product.id}>{product.name}</li>
    ))}
      </ul>
    </div>
  );
}

解释

  1. 页面会在构建时生成静态内容。
  2. 每 60 秒重新验证一次数据,确保页面内容较为新鲜。

哪些情况需要动态渲染?

有些场景确实需要动态渲染,例如:

  1. 页面内容依赖于用户的请求上下文(如登录用户的偏好设置)。
  2. 需要使用动态数据(如购物车信息、用户权限)。

示例:动态渲染购物车页面

typescript 复制代码
export default function CartPage() {
  const cookies = headers().get("cookie"); // 获取用户的购物车数据
  const cartItems = parseCookies(cookies); // 假设 parseCookies 是解析购物车信息的函数

  return (
    <div>
      <h1>购物车</h1>
      {cartItems.length ? (
      <ul>
        {cartItems.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
      </ul>
    ) : (
      <p>购物车为空</p>
    )}
    </div>
  );
}

现实中的类比
  • 静态渲染:就像印刷一本书,你一次性把内容写好,印刷好后,无论有多少读者,直接分发现成的书即可。
  • 动态渲染:就像手写信件,每个读者来时都需要重新写一封信,根据读者的要求调整内容。虽然灵活,但耗时又费力。

  1. 核心问题:无意中触发动态渲染,导致页面性能下降。
  2. 如何避免 :尽量使用静态生成或增量静态生成,不要直接使用动态 API(如 searchParams)。
  3. 动态渲染适用场景:需要实时数据或依赖请求上下文时,才使用动态渲染。

3 将敏感信息硬编码到代码中

问题是什么?

在开发过程中,我们经常需要使用敏感信息,例如:

  • API 密钥
  • 数据库连接字符串
  • 第三方服务的认证令牌

如果直接把这些敏感信息写到代码中(俗称"硬编码"),可能会导致以下问题:

  1. 安全风险:敏感信息可能被意外暴露到浏览器,或者被泄露到公共的代码库(如 GitHub)。
  2. 代码管理困难:当你需要更新这些敏感信息时,必须修改代码并重新部署,非常麻烦。
  3. 不可扩展:如果多个环境(如开发环境、生产环境)使用不同的敏感信息,硬编码会让切换环境变得复杂。

为什么硬编码敏感信息是危险的?

敏感信息就像你的密码或银行卡 PIN,如果随意写在代码里:

  1. 一旦代码被泄露,敏感信息也会被泄露。
  2. 有些信息可能意外被发送到浏览器,用户可以通过开发者工具查看到。
  3. 如果代码托管在 GitHub 等平台上,被公开后风险更大。

错误示例:直接硬编码敏感信息
typescript 复制代码
// 错误示例:将 API 密钥直接写在代码中
export default function FetchData() {
  const apiKey = "my-secret-api-key"; // 硬编码的密钥
  const data = fetch(`https://api.example.com/data?key=${apiKey}`).then((res) => res.json());

  return (
    <div>
      <h1>数据</h1>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}
问题分析
  1. 敏感信息可能被暴露
    • 如果这个组件被用在客户端(use client),整个代码会被发送到浏览器,用户可以通过开发者工具轻松看到 apiKey
    • 即使代码只运行在服务器端,也可能被意外泄露到日志或其他地方。
  2. 管理困难:如果 API 密钥需要更改,你必须修改代码,重新部署应用。
  3. 不可扩展:当开发环境和生产环境使用不同的 API 密钥时,硬编码会让代码变得混乱且难以管理。

正确示例:使用环境变量管理敏感信息

Next.js 提供了一种安全且灵活的方式管理敏感信息------环境变量。环境变量存储在服务器上,默认不会被发送到浏览器,确保安全性。

文件结构:

plain 复制代码
.env.local
.env.production
示例代码:
  1. **.env.local** 文件中存储敏感信息
plain 复制代码
SECRET_API_KEY=my-secret-api-key
  1. 在代码中使用环境变量
typescript 复制代码
export default async function FetchData() {
  const apiKey = process.env.SECRET_API_KEY; // 从环境变量读取密钥
  const data = await fetch(`https://api.example.com/data?key=${apiKey}`).then((res) => res.json());

  return (
    <div>
      <h1>数据</h1>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}
解决了哪些问题?
  1. 更安全
    • process.env.SECRET_API_KEY 默认只在服务器端可用,浏览器端看不到这个密钥。
    • 如果需要公开某些环境变量,可以通过添加 NEXT_PUBLIC_ 前缀来显式声明。
  2. 更易管理
    • 环境变量存储在 .env 文件中,不需要修改代码。
    • 不同环境可以有不同的 .env 文件(例如 .env.local 用于本地开发,.env.production 用于生产)。
  3. 更灵活
    • 只需切换 .env 文件,就可以轻松调整应用的配置,无需修改代码。

如何避免敏感信息被泄露到浏览器?
  • 默认情况下 : 环境变量只在服务器端可用。如果你希望某些变量可以在客户端使用,必须以 NEXT_PUBLIC_ 为前缀声明。
  • 示例:让环境变量在客户端可用
plain 复制代码
NEXT_PUBLIC_API_URL=https://api.example.com
typescript 复制代码
export default function ClientComponent() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL; // 浏览器端可以访问
  return <p>API URL: {apiUrl}</p>;
}
  • 确保敏感信息不要以 **NEXT_PUBLIC_** 开头,否则它会被暴露到浏览器。

现实中的类比

假设你住在公寓里:

  1. 硬编码敏感信息:就像把你的门锁密码直接贴在门上,任何人都可以轻松看到。
  2. 使用环境变量:就像把密码藏在保险箱里,只有你自己能用的时候才取出来。

  • 核心问题:硬编码敏感信息容易导致泄露风险、管理困难和不灵活。
  • 解决方案:使用环境变量存储敏感信息,确保其只在服务器端可用。
  • 关键点
    1. 环境变量是管理敏感信息的最佳实践。
    2. 默认情况下,环境变量不会被发送到浏览器。
    3. 使用 NEXT_PUBLIC_ 显式声明需要暴露给客户端的环境变量。

4 错误区分客户端和服务器端功能

问题是什么?

在 Next.js 中,服务器端和客户端的运行环境是完全不同的:

  1. 服务器端 (Server-Side):
    • 可以访问数据库、文件系统、环境变量等敏感信息。
    • 运行在服务器上,不会把代码暴露给用户。
  2. 客户端 (Client-Side):
    • 运行在用户的浏览器中。
    • 无法直接访问服务器端资源(如环境变量、数据库)。
    • 所有代码都会被传递到浏览器,用户可以通过开发者工具查看。

问题核心

某些逻辑或功能(如读取环境变量或调用数据库)只能在服务器端运行。如果误把这些功能放在客户端组件中,可能会导致以下问题:

  1. 功能出错:客户端无法运行服务器端逻辑。
  2. 安全风险:敏感信息可能被意外暴露给用户。

为什么需要区分?
  • 服务器端功能:比如读取 API 密钥或查询数据库,这是非常敏感的操作,不能在客户端执行,否则敏感信息会暴露给用户。
  • 客户端功能:如交互逻辑、动态界面更新等,这些只能在浏览器中运行。

错误示例:在客户端组件中调用服务器端逻辑

假设我们有一个函数从服务器读取数据,这个函数使用了环境变量存储的 API 密钥。

代码:

typescript 复制代码
// 一个获取数据的函数,依赖于服务器端的环境变量
export async function getData() {
  const apiKey = process.env.SECRET_API_KEY; // 从环境变量读取敏感信息
  const response = await fetch(`https://api.example.com/data?key=${apiKey}`);
  return response.json();
}

// 客户端组件
"use client"; // 指定这是一个客户端组件

export default function ClientComponent() {
  const handleClick = async () => {
    const data = await getData(); // 在客户端调用服务器端逻辑
    console.log(data);
  };

  return <button onClick={handleClick}>获取数据</button>;
}
问题分析
  1. 客户端无法访问环境变量process.env.SECRET_API_KEY 是服务器端专用的,客户端无法使用。
  2. 功能出错 :因为 apiKey 不会被传递到客户端,getData 函数中的 API 请求会失败。
  3. 潜在的安全风险:如果错误处理不当,可能会无意中暴露敏感信息。

正确示例:区分服务器端和客户端功能

解决方案

将敏感逻辑保留在服务器端,只把处理后的结果传递给客户端。

重构代码:

  1. 服务器端函数:
typescript 复制代码
// 在服务器端运行的函数,用于获取数据
export async function getServerData() {
  const apiKey = process.env.SECRET_API_KEY; // 从环境变量读取敏感信息
  const response = await fetch(`https://api.example.com/data?key=${apiKey}`);
  return response.json();
}
  1. 客户端组件:
typescript 复制代码
"use client"; // 指定这是一个客户端组件

export default function ClientComponent() {
  const handleClick = async () => {
    // 客户端通过 API 获取数据,而不是直接调用服务器端逻辑
    const data = await fetch("/api/data").then((res) => res.json());
    console.log(data);
  };

  return <button onClick={handleClick}>获取数据</button>;
}
  1. API 路由: 在 Next.js 的 /pages/api 目录下创建一个 API 路由,让客户端通过 API 获取服务器端的数据。
typescript 复制代码
// 文件路径:/pages/api/data.js
import { getServerData } from "../../server-utils";

export default async function handler(req, res) {
  const data = await getServerData();
  res.status(200).json(data); // 将数据返回给客户端
}

解决了哪些问题?
  1. 安全性
    • API 密钥仍然保留在服务器端,客户端无法访问。
    • 客户端通过安全的 API 路由获取数据,避免敏感信息泄露。
  2. 正确运行
    • 客户端只负责展示和交互,不直接执行服务器端逻辑。
  3. 可扩展性
    • 如果将来需要调整数据逻辑,只需修改服务器端代码,而无需更改客户端逻辑。

现实中的类比

假设你去餐厅点餐:

  1. 错误示例:你走进厨房,直接让厨师给你做饭(客户端直接调用服务器端逻辑)。问题是,你不应该进入厨房,里面有厨房的秘密(敏感信息),并且你也可能不会正确使用厨房设备。
  2. 正确示例:你在前台点餐,服务员通过菜单传递信息给厨师(客户端通过 API 调用服务器端功能),厨师完成后再把结果送到你面前(客户端显示结果)。

如何避免这个问题?
  1. 明确区分服务器端和客户端功能
    • 服务器端逻辑:读取环境变量、调用数据库、处理敏感数据。
    • 客户端逻辑:界面交互、动态更新、发起 API 请求。
  2. 使用工具限制错误使用
    • 使用 server-only 包:
typescript 复制代码
import { serverOnly } from "server-only";

export function getData() {
  serverOnly(); // 确保此函数只能在服务器端运行
  // 服务器端逻辑
}
  1. 遵循最佳实践
    • 将服务器端逻辑封装到 API 路由中,客户端通过 API 调用。
    • 不要在客户端组件中直接访问 process.env 或其他服务器端资源。

  • 核心问题:客户端无法直接运行服务器端逻辑,误用会导致功能失效或安全问题。
  • 解决方法:将敏感逻辑保留在服务器端,客户端通过 API 获取结果。
  • 关键点
    1. 服务器端逻辑(如读取环境变量)只能运行在服务器上。
    2. 客户端只能调用服务器端的结果,而不能直接访问服务器端功能。
    3. 使用工具(如 server-only)来强制限制错误使用。

5 在 try-catch 中错误使用 redirect

问题是什么?

在 Next.js 中,redirect 是一个服务器端函数,用于跳转到另一个页面。当你想根据某些条件(如用户未登录或数据不存在)将用户重定向时,可以使用它。

但问题在于:
redirect 的工作原理是通过 抛出一个错误 来停止代码执行并触发跳转。如果把 redirect 放在 try-catch 中,catch 会捕获这个错误,从而阻止 redirect 的正常执行。最终结果是页面没有跳转。


为什么会出问题?

redirect 本质上是用 throw 实现的:

typescript 复制代码
// redirect 的内部实现
function redirect(url) {
  throw new Error(`Redirecting to ${url}`);
}

redirect 被放在 try-catch 中时:

  1. try 中的 redirect 会抛出一个错误。
  2. catch 捕获了这个错误,页面不会跳转。
  3. 接下来的代码会继续执行,导致意外行为。

**错误示例:在 **try-catch** 中使用 ****redirect**
假设场景:

我们有一个商品详情页面。如果商品不存在,我们想跳转到"商品未找到"页面。

错误代码:

typescript 复制代码
import { redirect } from "next/navigation";

export default async function ProductPage({ params }: { params: { id: string } }) {
  try {
    const product = await fetchProduct(params.id); // 获取商品数据

    if (!product) {
      redirect("/not-found"); // 如果商品不存在,跳转到 404 页面
    }

    return (
      <div>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
      </div>
    );
  } catch (error) {
    console.error(error); // 捕获错误
    return <div>加载出错</div>;
  }
}
问题分析:
  1. redirect("/not-found") 被调用时,它会抛出一个错误。
  2. 错误被 catch 捕获,跳转行为被阻止。
  3. 页面没有跳转,用户会看到"加载出错"的信息,而不是"商品未找到"页面。

**正确示例:不要在 **try-catch** 中使用 ****redirect**
修改代码:

redirect 放在 try-catch 外部,确保不会被 catch 捕获。

正确代码:

typescript 复制代码
import { redirect } from "next/navigation";

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProduct(params.id); // 获取商品数据

  if (!product) {
    redirect("/not-found"); // 商品不存在时,直接跳转
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
工作原理:
  1. 如果商品存在,正常返回商品信息。
  2. 如果商品不存在,redirect 会立即停止代码执行,并跳转到 /not-found 页面。
  3. 没有 try-catch 干扰,跳转逻辑可以正常执行。

进一步优化:分开错误处理和跳转逻辑
如果确实需要处理其他错误(例如 API 请求失败),可以将错误处理和跳转逻辑分开:
typescript 复制代码
import { redirect } from "next/navigation";

export default async function ProductPage({ params }: { params: { id: string } }) {
  let product;

  try {
    product = await fetchProduct(params.id); // 获取商品数据
  } catch (error) {
    console.error("API 请求失败:", error); // 处理 API 请求失败
    return <div>加载出错</div>;
  }

  if (!product) {
    redirect("/not-found"); // 商品不存在时,跳转
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
工作原理:
  1. try-catch 中处理 API 请求错误,保证即使请求失败,页面不会崩溃。
  2. 如果请求成功但商品不存在,则执行 redirect,正常跳转。

现实中的类比

想象你在排队取餐:

  1. 错误示例:排队时,如果你的订单出了问题(比如找不到订单信息),你直接去找服务员,但服务员把问题当成普通查询处理,结果你继续留在队伍中,无法正确离开。
  2. 正确示例:排队时,系统检测到你的订单不存在,直接把你引导到"订单未找到"窗口,不让你继续排队。

redirect 的跳转逻辑就像第二种情况,应该是直接把你带走,而不是被错误处理逻辑"拦住"。


  • 核心问题redirect 本质上是通过抛出错误来实现的。如果放在 try-catch 中,错误会被捕获,跳转逻辑会被阻止。
  • 解决方法
    1. redirect 放在 try-catch 外部。
    2. 如果需要同时处理错误和跳转,将跳转逻辑和错误处理逻辑分开。
  • 关键点
    • redirect 直接停止代码执行并跳转,不需要被捕获。
    • try-catch 应该只用于处理非跳转相关的错误。
相关推荐
GesLuck10 分钟前
C#控件开发4—仪表盘
前端·经验分享·c#
小林爱20 分钟前
【Compose multiplatform教程14】【组件】LazyColumn组件
android·前端·kotlin·android studio·框架·多平台
泯泷26 分钟前
JS代码混淆器:JavaScript obfuscator 让你的代码看起来让人痛苦
开发语言·javascript·ecmascript
网安kk35 分钟前
2025年三个月自学手册 网络安全(黑客技术)
linux·网络·python·安全·web安全·网络安全·密码学
dot.Net安全矩阵3 小时前
.NET | 剖析通过 TcpClient 实现内网端口转发
服务器·网络·tcp/ip·安全·.net
过往记忆6 小时前
告别 Shuffle!深入探索 Spark 的 SPJ 技术
大数据·前端·分布式·ajax·spark
高兴蛋炒饭7 小时前
RouYi-Vue框架,环境搭建以及使用
前端·javascript·vue.js
m0_748240448 小时前
《通义千问AI落地—中》:前端实现
前端·人工智能·状态模式
ᥬ 小月亮8 小时前
Vue中接入萤石等直播视频(更新中ing)
前端·javascript·vue.js
夜斗(dou)8 小时前
node.js文件压缩包解析,反馈解析进度,解析后的文件字节正常
开发语言·javascript·node.js