1. 为什么选择 Next.js?从 React 到全栈的平滑升级
如果你已经接触过 React,可能会觉得用它来构建一个现代网页应用已经足够好了。组件化开发、丰富的生态、灵活的架构,React 确实给了我们很多自由。但当你真正开始做一个需要被搜索引擎收录、需要极快首屏加载速度、或者需要处理复杂数据流的项目时,你可能会开始头疼:路由怎么配?服务端渲染怎么搞?代码分割、图片优化、API 路由……这些“基建”工作会消耗你大量的时间。
这就是 Next.js 登场的时候了。我把它理解为一个“开箱即用”的 React 框架,它帮你把那些繁琐但又必需的工程化配置都打包好了。你可以把它想象成,React 给了你一堆高质量的乐高积木,而 Next.js 不仅帮你把积木按颜色和形状分好类,还附赠了几本搭建城堡、飞船的详细说明书,甚至帮你预装好了几个炫酷的电动马达(比如服务端渲染、静态生成)。
我刚开始用 Next.js 的时候,最直观的感受就是“快”。这种快体现在两个方面:一是开发速度快,很多配置都是零配置或极简配置;二是最终产出的应用运行快,因为它内置的性能优化手段太多了。比如,你完全不用操心怎么实现“按需加载”,Next.js 的文件系统路由天然就伴随着自动代码拆分,用户访问哪个页面,就只加载那个页面的代码,首屏加载时间自然就降下来了。
另一个让我决定深入使用 Next.js 的关键点是它对 SEO 的友好。传统 React 单页应用(SPA)的内容是靠 JavaScript 在浏览器里动态生成的,搜索引擎爬虫有时候抓取起来比较费劲,可能会影响排名。而 Next.js 默认支持服务端渲染(SSR)和静态生成(SSG),服务器可以直接把渲染好的完整 HTML 页面发给爬虫,这对内容型网站(博客、电商、新闻站)来说简直是福音。我记得有个项目从纯 React 迁移到 Next.js 后,在搜索引擎的收录速度和数量上都有肉眼可见的提升。
所以,Next.js 适合谁呢?我觉得这几类开发者会特别受益:一是从 React 过来,想提升项目性能和开发体验的;二是需要做 SEO 优化的前端开发者;三是全栈开发者,或者想涉足后端逻辑的前端同学,因为 Next.js 的 API Routes 功能让你能在同一个项目里轻松写后端接口。接下来,我们就亲手从零开始,搭一个集成了现代工具链的 Next.js 项目,感受一下它的便利。
2. 手把手初始化你的第一个 Next.js 项目
理论说再多,不如动手敲一行命令。Next.js 的官方创建工具 create-next-app 已经非常成熟,我们不仅用它初始化项目,还要把一些最佳实践的选配项都带上。我个人的习惯是,新项目尽量上 TypeScript 和 Tailwind CSS,这俩一个保代码稳健,一个保样式高效。
2.1 环境准备:打好地基
首先,确保你的机器上安装了 Node.js。我推荐使用最新的 LTS(长期支持)版本,你可以在 Node.js 官网找到下载。安装好后,打开终端,用 node -v 和 npm -v 检查一下版本。这一步是基础,就像盖房子前得确认有砖头和水泥。
接下来是编辑器。我强烈推荐 VS Code,它和 Next.js 的配合度极高。为了提高开发效率,你可以安装几个必备插件:
- ES7+ React/Redux/React-Native snippets:输入几个字母就能生成 React 组件模板,节省大量时间。
- Tailwind CSS IntelliSense:如果你选择使用 Tailwind,这个插件能提供智能提示和预览,写样式行云流水。
- Prettier - Code formatter:代码自动格式化,保证团队代码风格统一。你可以在项目里配好,保存时自动格式化。
- Prisma(可选):如果你后续打算用 Prisma 这个优秀的 ORM 来操作数据库,提前装上它的插件会有很好的语法高亮。
这些插件不是必须,但能极大提升你的编码舒适度。就像木匠有好用的刨子和锯子,干活自然更顺手。
2.2 项目创建:一步到位的现代化配置
环境齐备,现在开始创建项目。打开终端,进入你想存放项目的目录,然后运行下面这个命令。我强烈建议你直接复制粘贴,并留意我后面解释的每一个选项:
npx create-next-app@latest my-next-app
运行后,你会进入一个交互式的命令行问答界面。别紧张,我们一步步来选:
- 项目名称(What is your project named?):输入你喜欢的名字,比如
my-blog或dashboard-app。这里我输入my-next-app。 - 是否使用 TypeScript(Would you like to use TypeScript?):毫不犹豫地选 Yes。TypeScript 能在你写代码时就发现潜在的类型错误,对于中大型项目是维护性的巨大保障。新手可能会觉得有点门槛,但 Next.js 对 TS 的支持是原生的,体验很好,值得投入学习。
- 是否使用 ESLint(Would you like to use ESLint?):选 Yes。ESLint 是代码质量守门员,能帮你检查出不好的代码习惯和潜在错误,保持代码整洁。
- 是否使用 Tailwind CSS(Would you like to use Tailwind CSS?):我推荐选 Yes。Tailwind 是一种实用优先的 CSS 框架,通过组合类名来快速构建样式。它和 Next.js 是黄金搭档,能让你几乎不写传统 CSS 就完成整个页面的样式。如果你更熟悉其他 CSS 方案(如 CSS Modules、Styled-components),也可以选 No,后续自己安装。
- 是否使用
src/目录(Would you like to usesrc/directory?):这个看个人喜好。选 Yes 会把应用源代码(如app,components)放到src文件夹里,让根目录更干净。选 No 则会直接放在项目根目录。我通常选 Yes,结构更清晰。 - 是否使用 App Router(Would you like to use App Router?):务必选 Yes。这是 Next.js 13 引入的基于 React Server Components 的新路由架构,代表了未来方向,功能更强大,逻辑更清晰。旧的
pages路由器虽然还能用,但新项目建议直接从 App Router 开始。 - 是否自定义导入别名(Would you like to customize the default import alias?):可以先选 No。导入别名(比如用
@/components代替../../components)在项目复杂后很有用,但初期我们可以用默认设置。
一路回车确认后,create-next-app 就会开始搭建项目骨架,并自动安装所有依赖。这个过程会花一两分钟,取决于你的网速。完成后,按照终端的提示,进入项目目录并启动开发服务器:
cd my-next-app
npm run dev
如果一切顺利,你会看到终端输出类似 http://localhost:3000 的地址。用浏览器打开它,一个简约的 Next.js 欢迎页面就出现了!这意味着你的本地开发环境已经成功跑起来了。这个页面是热更新的,你修改代码保存后,页面会几乎无感地自动刷新,开发体验非常流畅。
3. 理解 Next.js 的核心:文件系统路由与导航
项目跑起来了,我们来看看 Next.js 最让人称道的特性之一:基于文件系统的路由。在传统的 React 项目中,我们通常需要安装像 react-router-dom 这样的库,并手动配置一堆路由规则。在 Next.js 的 App Router 里,这一切都变得极其直观。
3.1 路由即文件夹:约定大于配置
在你的项目里,找到 app 目录(如果在 src 下,就是 src/app)。这个 app 目录就是所有页面的家。在 App Router 中,一个文件夹就代表一个路由段(route segment),而一个特殊的文件 page.tsx(或 page.jsx)则代表这个路由段对外公开的页面。
让我举个例子,你一看就懂。假设我们要做一个简单的博客,有首页和文章列表页。
- 首页(
/):它对应app/page.tsx文件。你刚才在浏览器看到的欢迎页,就是这个文件的内容。 - 关于页(
/about):你只需要在app目录下新建一个文件夹,命名为about,然后在这个about文件夹里创建一个page.tsx文件。那么,访问http://localhost:3000/about就会显示这个文件的内容。 - 博客文章页(
/blog):同样,创建app/blog/page.tsx,即可通过/blog访问。 - 嵌套路由(
/blog/first-post):这也很简单。创建app/blog/first-post/page.tsx即可。Next.js 会自动将文件夹的嵌套结构映射为 URL 的路径。
这种设计哲学就是“约定大于配置”。你不需要写任何路由配置文件,只要按照规则创建文件和文件夹,路由就自然生成了。这对于快速原型开发和维护都非常友好。我经常在构思页面结构时,就直接在 app 目录下创建对应的文件夹,思路特别清晰。
这里有一个非常重要的细节:在 app 目录下,只有命名为 page.tsx、page.jsx、page.js 的文件才会被当作页面组件并生成可访问的路由。其他文件,比如 component.tsx、layout.tsx、utils.ts,都是私有的,不能通过 URL 直接访问。这保证了路由的清晰和安全。
3.2 智能导航:使用 <Link> 组件
在 Next.js 应用中进行页面跳转,你不能直接用 HTML 的 <a> 标签。虽然 <a> 标签也能工作,但它会导致浏览器进行完整的页面刷新,丢失了 React 应用“单页”的流畅体验,所有资源(JS、CSS)都要重新加载,非常低效。
Next.js 提供了自己的 next/link 组件。它的用法和 <a> 标签很像,但背后做了大量的优化工作。我们来看个对比:
传统 <a> 标签(不推荐):
// 这会导致全页面刷新
<a href="/about">关于我们</a>
Next.js <Link> 组件(推荐):
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog">博客</Link>
</nav>
);
}
当你使用 <Link> 时,Next.js 会在后台智能地预加载链接页面的代码(当链接出现在视口中时)。当你点击链接时,它只会获取新页面必需的数据和代码块,然后由 React 在客户端进行平滑的更新,感觉就像应用内的瞬时切换,没有白屏闪烁。这极大地提升了用户体验和网站性能。
你可以打开浏览器的开发者工具,在“网络(Network)”选项卡里观察,点击 <Link> 跳转时,只会看到少量数据请求,而没有整页的文档(document)请求。这就是客户端导航的魅力。所以记住,在 Next.js 里做内部链接,总是优先使用 next/link。
4. 组件渲染的革新:服务器组件 vs 客户端组件
这是 Next.js(尤其是 App Router)最核心、也最具颠覆性的概念之一。理解它,你才能真正用好 Next.js 的性能优势。简单来说,Next.js 允许你明确指定一个组件是在服务器端渲染,还是在客户端渲染。默认情况下,所有组件都是服务器组件。
4.1 两种组件的本质区别
服务器组件(Server Components) 的代码只在服务器上运行。它们可以直接访问后端资源(如数据库、文件系统、内部 API),并且不会将代码打包发送到浏览器。这意味着:
- 优点:可以执行敏感操作(如读取数据库密钥),因为代码不会泄露到客户端。能减少发送给浏览器的 JavaScript 包大小,提升性能。支持更快的初始页面加载,因为服务器直接返回渲染好的 HTML。
- 限制:不能使用状态(
useState,useReducer)、生命周期效应(useEffect)和浏览器专属 API(如window,document,addEventListener)。也不能处理用户交互,比如onClick。
客户端组件(Client Components) 则和传统的 React 组件一样,它们的代码会被打包并发送到浏览器,在用户的设备上执行。它们可以处理交互、使用状态和效应。
- 优点:可交互,能使用所有 React Hooks 和浏览器 API。
- 代价:会增加 JavaScript 包体积,且不能直接访问服务器端资源。
那么,如何创建一个客户端组件呢?非常简单,只需要在组件文件的最顶部(在任何导入语句之前)加上 "use client"; 这个指令。
4.2 实践:如何正确划分组件
理论有点抽象,我们来看一个我实际项目中踩过的坑。假设我们有一个商品卡片组件 ProductCard,它需要显示商品信息(从数据库获取),并且有一个“加入购物车”按钮。
错误示范(在服务器组件里处理事件):
// app/product-card.tsx
// 这是一个默认的服务器组件
import React from 'react';
const ProductCard = () => {
// 假设这里通过某个函数从数据库获取了product数据
const product = { id: 1, name: 'Awesome T-Shirt' };
return (
<div className="border p-4 rounded-lg">
<h2>{product.name}</h2>
{/* 错误!onClick是浏览器事件,不能在服务器组件中使用 */}
<button onClick={() => console.log('Added!')}>
加入购物车
</button>
</div>
);
};
export default ProductCard;
这段代码会报错,因为 onClick 是客户端交互,而 ProductCard 是服务器组件。
正确做法一:将整个组件变为客户端组件
// app/product-card.tsx
"use client"; // 在顶部添加这行指令
import React from 'react';
const ProductCard = () => {
const product = { id: 1, name: 'Awesome T-Shirt' }; // 注意:现在数据需要从客户端获取了
return (
<div className="border p-4 rounded-lg">
<h2>{product.name}</h2>
<button onClick={() => console.log('Added!')}>
加入购物车
</button>
</div>
);
};
export default ProductCard;
这样做虽然解决了问题,但有个缺点:现在连显示商品名称的静态部分也变成了客户端渲染,增加了不必要的 JavaScript 下载和执行。
正确做法二(推荐):仅将交互部分拆分出去 这才是 Next.js 倡导的模式:尽可能保持组件为服务器组件,只将需要交互的叶子节点标记为客户端组件。
// app/product-card.tsx
// 这是一个服务器组件
import React from 'react';
import AddToCartButton from './add-to-cart-button'; // 导入客户端组件
// 服务器组件可以直接进行数据获取(后面会讲)
async function getProduct() {
const res = await fetch('https://api.example.com/product/1');
return res.json();
}
const ProductCard = async () => { // 注意:这是一个异步组件!
const product = await getProduct(); // 在服务器端获取数据
return (
<div className="border p-4 rounded-lg">
<h2>{product.name}</h2>
{/* 将交互部分委托给客户端子组件 */}
<AddToCartButton productId={product.id} />
</div>
);
};
export default ProductCard;
// app/add-to-cart-button.tsx
"use client"; // 只有这个按钮组件是客户端组件
import React from 'react';
const AddToCartButton = ({ productId }) => {
const handleClick = () => {
console.log(`Adding product ${productId} to cart`);
// 这里可以调用购物车状态管理等
};
return (
<button onClick={handleClick}>
加入购物车
</button>
);
};
export default AddToCartButton;
这种模式的优势非常明显:ProductCard 的大部分内容(商品信息)在服务器端就渲染成 HTML 发送给用户,速度快且对 SEO 友好。只有那个小小的按钮需要客户端 JavaScript。这极大地优化了性能。养成这种“组件拆分”的思维习惯,是写好 Next.js 应用的关键。
5. 数据获取:在正确的地方获取数据
理解了服务器组件和客户端组件的区别,数据获取的思路就清晰了。原则是:尽可能在服务器端获取数据。
5.1 在服务器组件中获取数据
在服务器组件中,你可以使用原生的 fetch API 或任何 Node.js 兼容的数据库客户端来直接获取数据。因为代码在服务器上运行,所以没有跨域问题,也可以安全地使用环境变量访问密钥。
Next.js 扩展了原生的 fetch,提供了便捷的缓存和重新验证配置。看一个简单的例子:
// app/users/page.tsx
type User = {
id: number;
name: string;
email: string;
};
export default async function UsersPage() {
// 在服务器组件中直接使用 async/await
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}
这个 UsersPage 组件会在服务器端请求数据,将数据渲染进 HTML,然后把完整的页面发送给浏览器。用户打开页面时,内容已经在了,无需等待客户端 JavaScript 再去获取数据。
5.2 在客户端组件中获取数据
如果你确实需要在客户端获取数据(例如,根据用户输入实时搜索),那么你可以使用传统的 React 方式,比如 useEffect 和 useState,或者使用更强大的库如 SWR 或 TanStack Query。
// app/search/client-search.tsx
"use client";
import { useState, useEffect } from 'react';
export default function ClientSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (query.length < 2) return; // 简单防抖
const timer = setTimeout(() => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
}, 300);
return () => clearTimeout(timer); // 清理副作用
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
记住,客户端数据获取适用于高度交互、数据频繁变化的场景。对于初始内容,优先考虑服务器端获取。
6. 性能关键:缓存、静态渲染与动态渲染
Next.js 在性能优化上做了很多幕后工作,其中缓存策略和渲染模式的选择直接影响应用的加载速度和服务器负载。
6.1 数据缓存(Data Cache)
还记得前面在服务器组件里用的 fetch 吗?Next.js 默认会缓存 fetch 请求的响应。这意味着,如果两个组件请求同一个 URL 的数据,或者同一个用户再次访问页面,Next.js 会直接从缓存中返回数据,而不是再次发起网络请求。这大大减少了数据库或外部 API 的负载,提升了响应速度。
但缓存并非总是好事。对于实时性要求高的数据(如股票价格、体育比赛比分),我们需要绕过缓存或设置一个较短的缓存时间。Next.js 的 fetch 提供了配置项:
// 选项一:完全不缓存(每次请求都获取最新数据)
const dynamicData = await fetch('https://api.example.com/live-score', {
cache: 'no-store',
});
// 选项二:定时重新验证(每10秒刷新一次缓存)
const revalidatedData = await fetch('https://api.example.com/news', {
next: { revalidate: 10 }, // 单位:秒
});
revalidate 选项非常有用,它实现了“增量静态再生”(ISR)。页面在构建时生成并缓存,然后每隔设定的时间(如10秒),如果有新请求,Next.js 会在后台重新获取数据并生成新页面,下次请求就得到更新后的内容。这完美平衡了性能和数据新鲜度。
6.2 静态渲染 vs 动态渲染
这两种渲染模式决定了页面在构建时(npm run build)的行为。
静态渲染(Static Rendering):如果一个路由中的所有数据获取都使用了缓存(或者没有数据获取),Next.js 会在构建时就将这个页面预渲染为静态 HTML 文件。这个文件可以被 CDN 缓存,访问速度极快,服务器零计算压力。这适用于内容不常变的页面,如博客文章、产品介绍页。
动态渲染(Dynamic Rendering):如果一个路由中包含了未缓存的请求(cache: 'no-store')、或者使用了需要根据请求信息(如 cookies, headers)才能确定的动态数据,Next.js 会在每次请求时在服务器上实时渲染这个页面。这保证了数据的实时性,但牺牲了一些性能。
你可以在构建输出中看到每个页面的渲染模式。运行 npm run build 后,观察终端输出:
Route (app) Size First Load JS
┌ ○ / 5.45 kB 91 kB
├ ○ /about 1.23 kB 87 kB
└ λ /dashboard 3.12 kB 89 kB
○符号 表示静态生成(Prerendered as static HTML)。λ符号 (Lambda) 表示动态渲染(Server-rendered on demand)。
一个常见的误区是认为“动态渲染=慢”。其实不然,动态渲染的页面依然享受服务器组件、流式传输等优化,速度仍然很快,只是无法被 CDN 全局缓存。你应该根据页面的数据特性来选择合适的模式。比如,用户个人资料页面(/profile)肯定是动态的,而公司介绍页面(/about)完全可以是静态的。
理解并运用好这些概念,你就能构建出既快又新鲜的应用。Next.js 把这些复杂的决策封装成了简单的配置,让我们开发者可以更专注于业务逻辑本身。在项目初始化阶段就建立起对这些核心概念的认知,就像在盖楼前看懂了建筑图纸,后续的“添砖加瓦”才会更加得心应手。

983

被折叠的 条评论
为什么被折叠?



