Givepo
博客

使用 Next.js、React 和 TypeScript 进行现代 Web 开发:最佳实践

Server Components、渲染策略、类型安全的数据与 Core Web Vitals。一份实用指南,教你用当今大多数团队选择的技术栈构建快速、易维护的 Web 应用。

Givepo 团队
阅读约 6 分钟
Illustration of a code editor showing page.tsx next to a React component tree and a TypeScript badge网站开发

React、Next.js 和 TypeScript 已经成为专业 Web 应用的默认技术栈。React 提供组件模型,Next.js 在此之上加入路由、渲染和数据获取,TypeScript 则能在代码运行之前就捕获一整类错误。用得好,它们能做出对访客来说很快、对开发者来说易于维护的网站。

用得不好,同样的技术栈也会做出塞满数 MB JavaScript 的缓慢页面。本文介绍决定成败的关键理念,并提供可直接用于 Next.js App Router 项目的代码。

为什么这套技术栈成为默认选择

  • React 把界面描述为一棵可复用组件的树。按钮、卡片乃至整个页面都是其数据的函数,让界面可预测、易测试。
  • Next.js 把 React 变成完整的框架:基于文件的路由、服务端渲染、缓存、图片和字体优化,以及生产构建流程。
  • TypeScript 为 JavaScript 加上静态类型。编辑器知道每个对象的结构,重构更安全,许多运行时错误变成了编译期错误。

三者结合,能覆盖从营销网站到复杂仪表盘的各种需求,前端和服务端使用同一种语言。

Server Components 改变了默认方式

在 Next.js App Router 中,除非你主动选择,否则每个组件都是 React Server Component。Server Components 只在服务端运行,可以直接读取数据库或调用内部 API,而且它们的 JavaScript 永远不会发送到浏览器,发送的只有渲染结果。

只有需要交互的组件才需要加上 'use client' 指令:状态、副作用、事件处理或浏览器 API。最佳实践是让这些客户端组件保持小巧,并放在组件树的叶子节点上:

TSX
// app/blog/page.tsx: a Server Component (the default in the App Router)
import { getPosts } from '@/lib/posts'
import { LikeButton } from './like-button'

export default async function BlogPage() {
  const posts = await getPosts() // runs on the server, so no extra API route is needed

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
          <LikeButton />
        </li>
      ))}
    </ul>
  )
}
TSX
// app/blog/like-button.tsx: only this small component ships JavaScript
'use client'

import { useState } from 'react'

export function LikeButton() {
  const [liked, setLiked] = useState(false)

  return (
    <button type="button" aria-pressed={liked} onClick={() => setLiked((value) => !value)}>
      {liked ? 'Liked' : 'Like'}
    </button>
  )
}

列表、数据访问和页面结构不会给浏览器带来任何负担,只有按钮需要 hydration。在内容为主的页面上,这通常能去掉大部分客户端 JavaScript。

选择合适的渲染策略

Next.js 允许你按路由决定 HTML 在何时生成。选对方案是你所做的最重要的性能决策:

  • 静态渲染:HTML 在构建时生成,从缓存或 CDN 提供。最适合营销页面和文档。
  • 增量静态再生成(ISR):静态页面在一段时间后或通过 revalidatePath() 按需刷新。最适合博客、商品目录和由 CMS 驱动的页面。
  • 动态渲染:每个请求都生成 HTML。个性化页面、仪表盘以及依赖 cookie 的内容必须使用它。
  • 流式渲染:把页面中较慢的部分包裹在 <Suspense> 中,页面框架立即显示,慢数据准备好后再流式传入。

下面是一个带缓存、每五分钟刷新一次、并为每篇文章生成 SEO 元数据的动态博客路由。注意,在当前的 Next.js 版本中 params 是一个 Promise:

TSX
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPost } from '@/lib/posts'

type Props = { params: Promise<{ slug: string }> }

// Serve the cached page, regenerating it at most every five minutes
export const revalidate = 300

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { type: 'article', title: post.title, images: [post.coverUrl] },
  }
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound() // renders the 404 page with the correct status code

  return (
    <article>
      <h1>{post.title}</h1>
      {/* … */}
    </article>
  )
}

物有所值的 TypeScript

从第一次提交就开启 strict 模式,之后再补开会非常痛苦。noUncheckedIndexedAccess 也值得开启,因为它会迫使你处理数组元素或映射项不存在的情况:

JSON
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}

记住,类型在运行时会消失。跨越边界的数据(表单输入、API 响应、URL 参数、环境变量)必须经过校验,而不仅仅是标注类型。像 zod 这样的 schema 库可以用一个定义同时提供两者:

TypeScript
import { z } from 'zod'

export const PostSchema = z.object({
  slug: z.string().regex(/^[a-z0-9-]+$/),
  title: z.string().min(1).max(200),
  publishedAt: z.coerce.date(),
  tags: z.array(z.string()).default([]),
})

// One source of truth for the runtime check and the compile-time type
export type Post = z.infer<typeof PostSchema>

export function parsePost(input: unknown): Post {
  return PostSchema.parse(input) // throws a descriptive error for invalid data
}

用可辨识联合类型(discriminated union)显式地建模状态,例如 { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; data: Post[] }。这样编译器会强制每个组件处理加载和错误状态,而不是假设一切顺利。

使用 Server Actions 修改数据

Server Actions 让表单可以直接调用服务端函数,无需手写 API 路由。它们很方便,但底层仍然是公开的 HTTP 端点。请像对待 API 一样对待每个 action:校验输入,在服务端检查身份认证和权限,并返回表单可以展示的结构化错误。在界面上隐藏一个按钮并不等于安全。

性能与无障碍检查清单

  • 使用 next/image 并设置准确的 sizes 属性,让手机永远不会下载桌面尺寸的图片;只把首屏的主图标记为 priority。
  • 使用 next/font 加载字体,实现自托管并避免布局偏移。
  • 用 Promise.all 并行获取相互独立的数据,而不是一连串顺序执行的 await。
  • 让客户端组件保持小巧并放在叶子节点,延迟加载地图、图表和编辑器等重量级组件。
  • 在生产环境中跟踪 Core Web Vitals。“良好”意味着在第 75 百分位上,Largest Contentful Paint 低于 2.5 秒、Interaction to Next Paint 低于 200 毫秒、Cumulative Layout Shift 低于 0.1。
  • 使用语义化 HTML(<nav>、<main>、<article>、真正的 <button>),为每个表单字段添加标签,保持焦点状态可见,并检查颜色对比度。

要点回顾

  • 默认使用 Server Components,只在需要交互的地方添加 'use client'。
  • 按路由选择渲染策略:能静态就静态,内容页用 ISR,只有必要时才用动态渲染。
  • 将严格的 TypeScript 与每个边界上的运行时校验结合起来。
  • 衡量真实用户的性能,并把无障碍视为“完成”定义的一部分。

总结

现代 React 技术栈会回报那些清楚自己代码在哪里运行的团队。默认在服务端渲染,只为真正的交互发送 JavaScript,并在边界处强制类型和校验,你的应用就能加载迅速、排名靠前,并在不断成长中依然易于修改。

延伸阅读

  • #Next.js
  • #React
  • #TypeScript
  • #Web Performance
作者
Givepo 团队

Givepo 是一家 IT 公司,专注于开发网站、移动应用和定制软件,助力您的业务持续发展。

分享本文

免费 30 分钟咨询

与我们的团队沟通您的想法、需求范围和预算。

预约咨询

更多文章

博客
网站开发
·阅读约 1 分钟

网站还是 Web 应用:您的企业需要哪一种?

这个答案会影响预算、周期和技术选型。在开发开始之前,先看看哪一种更适合您的企业。

查看详情
后端与 API
·阅读约 7 分钟

后端架构详解:REST API、微服务与可扩展数据库

如何设计开发者乐于使用的 REST API,微服务何时值得付出代价,如何设计能承受高负载的数据库,以及让后端系统保持快速和可靠的模式。

查看详情
软件工程
·阅读约 7 分钟

整洁架构与 SOLID 原则:编写长期易维护的软件

为什么代码库越大越难改,以及整洁架构、依赖规则和 SOLID 原则如何让业务逻辑易于测试和修改,附完整的 TypeScript 示例。

查看详情