Next.js框架入门
Next.js — Web开发领域最高效、便捷的全栈框架
渲染模式
client side rendering:浏览器下载一个最小的 HTML 页面和页面所需的 JavaScript。然后使用 JavaScript 更新 DOM 并渲染页面。当应用首次加载时,用户可能会注意到在看到完整页面之前有轻微的延迟,这是因为在所有 JavaScript 下载、解析和执行之前,页面不会完全渲染。首次加载页面后,导航到同一网站的其他页面通常会更快,因为只需获取必要的数据,JavaScript 可以重新渲染页面的部分内容,而无需完全刷新页面。
server side rendering:在服务器端将网页完整渲染并生成 HTML,再将该完整 HTML 发送到浏览器,客户端接收HTML后,执行JavaScript为DOM附加交互能力。
static site generation:页面的 HTML 将在构建时生成。这意味着在生产环境中,页面 HTML是在执行next build时生成的。这个 HTML 将在每次请求时重复使用。它可以由CDN 缓存。
incremental static regeneration:增量静态再生可以实现:
● 无需重新构建整个网站就能更新静态内容;
● 通过对大多数请求提供预渲染的静态页面来减少服务器负载;
● 确保将正确的 cache-control 标头自动添加到页面;
● 在不增加 next build 时间的情况下处理大量内容页面;
| 渲染模式 | 渲染位置 | 优势 | 限制 |
|---|---|---|---|
| CSR | 浏览器 | 前端交互更灵活,服务器压力小 | 初始时加载慢,SEO 不佳 |
| SSR | 服务器 | 首次加载快,SEO 好,社交媒体友好 | 服务器成本高,架构复杂 |
| SSG | 构建时 | 渲染速度快,成本低,简单 | 内容无法实时更新 |
| ISR | 构建+更新 | 综合 SSR 与 SSG 优点 | 需要支持的框架和机制(如Next.js) |
渲染模式对比
CSR的不足
1. 嵌套组件获取远程数据时,造成昂贵的客户端-服务器瀑布;
2. 白屏时间长,JavaScript bundle size过大,这会增加页面的 FCP(首次内容绘制)和 TTI(可交互时间),FCP 和 TTI 被推迟后。这意味着在 FP(首次绘制)和 FCP 之间,用户将看到空白屏幕。
3. SEO困难,据悉目前百度、Google虽然能够爬取SPA,但效果不如SSR。
Next.js
起步
本教程将基于Next.js v15.x App Router逐步搭建一个包含注册、登录/认证、数据列表、增删改查等功能的服务端渲染Web App,并将其成功部署到vercel。
├── app
| ├── _components // 私有文件夹,不会渲染成路由
| ├── dashboard // 一级路由
| | ├── (overview) // 分组路由,不会渲染成路由
| | | ├── loading.tsx // 路由/dashboard的page加载中的UI
| | ├── invoices // 嵌套路由 /dashboard/invoices
| | | ├── error // 错误UI — React 错误边界
| | | ├── [id]
| | | | ├── edit // 动态路由 /dashboard/invoices/[id]/edit
| | | | | ├── not-found.tsx // 未找到页面显示的UI
| | └── layout.tsx // /dashboard路由的布局
| ├── favicon.ico // 元数据—网站图标
| ├── layout.tsx // 根布局
| ├── lib // 公共方法/字段
| ├── login
| | └── page.tsx // 路由 /login的页面
| ├── page.tsx // 根路径/的页面
| ├── seed
| | └── route.ts // 初始化数据库
| └── ui // ui组件库
├── auth.config.ts
├── auth.ts // 身份认证
├── middleware.ts // 请求中间件
├── next.config.ts // Next.js 的配置文件
├── postcss.config.js
├── public // 静态资源 可使用
├── .env // 环境变量
├── .env.local/production/development // 特定环境变量
├── tailwind.config.ts // tailwind配置
└── tsconfig.json // TS配置,各个字段的配置详见https://www.typescriptlang.org/tsconfig/#incremental
项目目录
当前的项目结构策略是将项目文件存储在app内的顶层文件夹中,在特殊文件中定义的组件按特定层次结构渲染,组件在嵌套路由中递归渲染,这意味着路由段的组件将嵌套在其父段的组件中。
更多项目结构参考:Next.js项目结构
配置开发环境
请确保您的系统满足以下要求:Node.js 18.18 或更高版本
npx create-next-app@latest
npx:npx会下载create-next-app@latest放在临时文件中,过一段时间会自动清除,免去了本地全局安装的步骤。
next dev:// 启动开发服务器。
next build:// 为生产构建应用程序。
next start:// 启动生产服务器。
next lint:// 运行 ESLint。
核心概念
Next.js使用基于文件系统的路由,可以使用文件夹和文件来定义路由。路由可以包括layout(布局)和page(页面),路由直接通过Link链接,也可以在客户端组件以编程方式(useRouter)更改路由。
文件系统约定
Next.js项目文件系统约定,预定义了多种文件名对应的作用,下面列出一些开发常用的文件约定:
布局
layout 文件用于在你的 Next.js 应用中定义布局,app/layout.tsx是根布局(根布局必须定义 和 标签),类似app/dashboard/layout.tsx是dashboard路由的布局,接收children和params组成的props
页面
page.tsx文件允许定义对于路由唯一的 UI,接收searchParams和params组成的props,
可以通过从文件中默认导出一个组件来创建页面,和route互斥(一个文件夹下不能同时存在page.tsx和route.ts)。
package.json
项目配置文件,scripts:可运行的脚本;dependencies:项目依赖;更多参数详见:package.json | npm DocsonlyBuiltDependencies告诉pnpm:对于列表中的包,只下载和使用已经编译好的二进制文件,跳过任何源码编译步骤。因为bcrypt和sharp包含c++代码,编译过程中可能出错(缺少编译工具、环境不兼容等原因),通过直接配置onlyBuiltDependencies下载包维护者提供的预编译的二进制文件,安装速度更快,不容易出错
"pnpm": {
"onlyBuiltDependencies": [
"bcrypt",
"sharp"
]
}
路由处理程序
/api/static-page/route.ts 这个路由是对外暴露请求接口,可通过fetch('${origin}/static_page/api')调用,见代码:使用路由API。在App Router可以使用路由处理程序或中间件实现BFF层。
中间件
middleware.ts 文件用于在请求完成之前写入 中间件 并在服务器上运行代码。然后,根据传入的请求,你可以通过重写、重定向、修改请求或响应标头或直接响应来修改响应。
中间件在渲染路由之前执行。它对于实现自定义服务器端逻辑(例如身份验证、日志记录或处理重定向)特别有用。
使用项目根目录中的文件 middleware.ts(或 .js)来定义中间件。
更多文件系统约定说明参考:文件系统约定
样式
● CSS Modules
CSS Modules 通过生成唯一的类名来局部作用域化 CSS。这允许你在不同文件中使用相同的类名而不用担心命名冲突,本教程结合使用了scss和css模块。
● 全局CSS
使用全局 CSS 在整个应用程序中应用样式:创建一个 app/global.css 文件并在根布局(app/layout.tsx)中导入,以将样式应用于应用程序中的每个路由
● Tailwind CSS
包含多个实用工具类的css框架,可以直接在HTML中应用样式,而无需编写自定义的CSS,且高度可定制。
更多方式详见:Getting Started: CSS
服务器/客户端组件
服务器组件:
Next.js中布局(layout.tsx)和页面(page.tsx)默认是服务器组件。
客户端组件:
use client将模块及其传递的依赖项指定在客户端渲染,当创建需要客户端 JavaScript 功能(如状态管理、事件处理、自定义hooks和访问浏览器 API)的交互式用户界面时应该使用它。会被打包到 JavaScript bundle 并发送到浏览器,而服务器组件不会。
详细说明见:服务器和客户端组件
获取数据
服务器组件:
使用fetch API调用接口 或者使用ORM框架,或者调用数据库。见代码:latest-invoices.tsx,这个组件默认是服务端组件,可以通过fetchLatestInvoices
直接调用数据库获取数据。
我们以这个组件为例,将其改造成客户端组件,并添加onClick事件处理程序。见代码:latest-invoices-client.tsx
客户端组件:
● React 的 use
本质还是数据在服务器组件获取,通过props传给作为子组件的客户端,浏览器 Network/XHR 不会暴露http请求;
● 使用像 SWR 或react-query这样的社区库(底层也是使用fetch API,封装了缓存、自动刷新、乐观更新、自动重试、全局状态管理等强大功能),定义API Route后,在客户端组件使用库或原生fetch调用接口,浏览器 Network能看到xhr请求。
● LatestInvoicesClient作为客户端组件,默认不能被Suspense,但是启用了 Suspense 的数据源会激活 Suspense 组件,它们包括:
支持 Suspense 的框架如 Relay 和 Next.js。
使用 lazy 懒加载组件代码。客户端组件
使用 use 读取缓存的 Promise 值。客户端组件
更新数据
使用服务器函数,代码见:服务器函数
服务器组件:
可以通过将 "use server" 指令添加到函数主体顶部,在服务器组件中内联服务器函数;
可以直接导入已经定义的服务器函数。
客户端组件:
无法在客户端组件中定义服务器函数。但是,你可以通过从顶部包含 "use server" 指令的文件中导入它们来在客户端组件中调用它们。
指令
use server
指定要在服务器端执行的函数或文件。它可以用在文件顶部,以指示文件中的所有函数都是服务器端的,或者内联在函数顶部以将该函数标记为服务器功能,这是一个 React 特性。
渲染策略
静态渲染
使用静态渲染时,HTML 会提前生成 - 无论是在构建时还是通过 revalidation。结果将被缓存并在用户和请求之间共享。
在部分预渲染中,Next.js 会为路由预渲染一个静态 shell。这可能包括布局和任何其他不依赖于请求时间数据的组件。
动态渲染
使用动态渲染时,HTML 会在请求时生成。这允许你根据请求时间数据提供个性化内容
在app/dashboard/(overview)/page.tsx页面toggle对cookie的引用可在devIndicators验证。
● 开发环境本地启动,通过dev提示器查看路由是静态渲染还是动态渲染。
● next build会给出路由是什么渲染模式。
可以在一个路由内结合动态渲染和静态渲染-部分预渲染(partial-prerendering),只能在在canary版本试用。
路由组
路由组(Route Groups) 允许你将文件组织成逻辑上的分组,而不会影响 URL 路径的结构。当你使用括号 () 创建一个新文件夹时,该文件夹的名称不会出现在 URL 路径中 。例如,/dashboard/(overview)/page.tsx 对应的路径是 /dashboard,overview只是开发者用于自己组织代码的文件夹。
身份认证
Next.js推荐使用next-auth,结合,auth.ts 是认证核心,导出 signIn、signOut 等方法,使用 zod 校验登录参数,通过 next-auth 实现邮箱/用户名+密码登录,安全校验用户信息,集中管理认证。逻辑见代码:/auth.ts
调试
1. 新建并配置.vscode/launch.json
2. Ctrl+Shift+D on Windows/Linux, ⇧+⌘+D on macOS,选择调试模式,按F5,即可开启调试模式。
部署
● 使用Docker
● 使用阿里云的serverless function
● 使用Node.js server方式
Next.js 可部署到任何支持 Node.js的云服务器。运行 npm run build 来构建应用程序,运行 npm run start 来启动 Node.js 服务器。该服务器支持所有 Next.js 功能
● Vercel:官方集成部署方案
1. 根据教程最终搭建完成项目核心功能,pnpm dev启动开发环境,访问 http://localhost:3000,登录成功后,应该能在浏览器看到dashboard页面。
2. 在vercel通过github/gitlab导入项目,部署后能看到项目后台,包括数据库、资源使用情况、应用状态监控、应用指标等。修改代码git push到远程分支(如 main)后,vercel会自动重新部署。
配置数据库
启动pnpm dev后,访问路由 localhost:3000/seed ,会执行建表语句,并使用placeholder-data.ts中的初始化数据给表填充数据。
环境变量
1. 把本项目的.env直接导入到vercel环境变量后台。
2. nextauth_url:如果使用了next-auth认证库,Vercel部署时还需要加线上域名作为NEXTAUTH_URL 环境变量的值用于认证成功后的跳转。
3. 主域名、本次部署预览域名、发布分支预览域名
vercel后台
性能指标
在app/layout.tsx安装并使用SpeedInsights,后在vercel后台可以看到具体的页面性能指标。
详细部署方案见:Next.js on Vercel
QA
1. 何时使用服务器组件或客户端组件,他们怎么组合使用。
服务器组件是默认选择,适用于:
● 从靠近源的数据库或 API 获取数据。
● 使用 API 密钥、令牌和其他密钥信息,但不要将它们暴露给客户端;
● 减少发送到浏览器的 JavaScript 数量
● 改进 首次内容绘制 (FCP)指标,并将内容逐步流式传输到客户端从靠近源的数据库或 API 获取数据
客户端组件,需要添加 "use client" 指令,适用于:
● 管理状态 和 响应事件。例如onClick, onChange
● 反应式副作用的生命周期。例如useEffect
● 仅限浏览器的 API。例如localStorage、window、Navigator.geolocation 等
组合使用
最佳实践是将客户端组件尽可能推到组件树的叶子节点,服务器组件可以导入客户端组件,但客户端组件不能直接导入服务器组件。如果需要在客户端组件中使用服务器组件,可以通过 children传递,这种架构让你既能享受服务器渲染的性能优势,又能提供丰富的客户端交互体验。详见代码:服务器组件的客户端字组件:nav-links客户端组件内渲染服务器组件:static_page
2. 什么场景需要直接查询数据库?
服务器组件中的数据获取:
● 页面初始渲染需要的数据
● 静态内容生成
● 服务端预处理的复杂查询
API Routes 中的数据操作:
● CRUD 操作
● 复杂的业务逻辑
● 需要身份验证的数据访问
Server Actions 中的数据变更
● 表单提交处理
● 用户交互触发的数据更新
3. 使用ORM(Object Relational Mapping)框架/ODM(Object Document Modeling)库和直接写SQL语句有什么区别?
| 特性 | ORM | ODM | 原生SQL |
|---|---|---|---|
| 目标存储 | 关系型数据库(MySQL、PostgreSQL、OceanBase等),部分 ORM 也支持 MongoDB | 文档型数据库(MongoDB、CouchDB 等) | 关系型数据库 |
| 抽象层级 | 将数据库表映射为类,将行映射为对象;通过对象方法操作数据 | 将文档集合映射为模型,将文档映射为对象;通过模型方法操作文档 | 无框架抽象层,直接编写和执行原生查询(SQL 或数据库自身的查询语言) |
| 模式定义 | 通常需要定义模型(实体)和字段类型 | 定义 Schema(可选/灵活),并可带验证规则 | 模式由数据库控制,不在代码层面定义 |
| 查询语言 | 通过框架 API 或查询构建器生成并执行 SQL | 通过框架 API(BSON/JSON 风格)生成底层文档查询 | 关系型数据库使用标准 SQL;文档型数据库使用其原生 CRUD 或聚合查询 API |
| 关联处理 | 支持外键、一对多、多对多等,通过 JOIN 自动完成 | 通过嵌套文档或引用实现;无 JOIN,关联查询需手动组合或多次查询 | 手动编写 JOIN、子查询或多次独立查询 |
| 事务支持 | 支持 | MongoDB 自 v4.0 起支持多文档事务,但使用上不如关系库直观 | 关系型数据库使用原生事务(BEGIN/COMMIT/ROLLBACK);文档库事务视驱动支持而定 |
| 安全性 | 默认参数化查询,防止注入风险 | 默认参数化或基于驱动的查询,防止注入 | 需手动使用预编译/参数化或转义以防注入 |
| 性能调优 | 可执行原生 SQL 或调用底层接口,以缓存、懒加载等方式优化 | 支持原生查询、聚合管道等,但低级优化选项较少 | 完全可控,最易做极致优化 |
| 简单示例 | import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();async function main() {const users = await prisma.user.findMany({ where: { age: { gt: 18 } } });console.log(users);}main(); | User.find({ age: { $gt: 18 } }).then(users => console.log(users)) .catch(err => console.error(err)); | const sql = "SELECT * FROM users WHERE age > 18";db.query(sql, [], (err, rows) => { … });// 文档库 原生查询collection.find({ age: { $gt: 18 } }).toArray(); |
4. webpack的define和.env里面的环境变量有什么区别?
.env文件:webpack或Vite等打包工具在Node.js运行时使用dotenv解析.env .[mode]["local"]文件,并将.env中的内容注入到process.env中,客户端是无法访问process.env的。
DefinePlugin:在webpack等打包工具在编译时进行字符串替换,直接嵌入到打包后的代码中,改变打包产物,适用于将需要的变量注入到前端代码中。
tips: Vite的.env通过import.meta.env获取.env中设置的变量,本质上是DefinePlugin的机制,而不是真正的环境变量,Vite内部使用了类似的替换机制。可以在Vite的源码中看到,它会:
1. 读取.env文件
2. 过滤出VITE_前缀的变量
3. 在编译时将import.meta.env.VITE_XXX替换为对应的字面值
troubleshooting
1. 初次登录数据库后台时,可以会遇到Tables报错,并显示Database is not set错误,经验证,在第三方client连接Postgre数据库后,Tables会正常显示数据表。
2. 客户端组件中直接使用不确定的内容如Math.random()、new Date().toLocaleString()等时,会报错水合(Hydration 是 React 将 事件处理程序 附加到 DOM 的过程,以使静态 HTML 具有交互性)失败;
具体原因:客户端组件在服务端渲染时,new Date().toLocaleString() 会生成服务端时间的字符串;而在客户端水合时,new Date().toLocaleString() 会生成客户端时间的字符串。这两个值很可能不一样(时区、时间点、locale等),导致 React 发现 HTML 不一致,报出 hydration error。
解决方案一:用 useEffect 在客户端生成时间,初始渲染时不要输出时间字符串;见代码:app/ui/dashboard/latest-invoices-client.tsx
解决方案二:用 dynamic 并 ssr: false 包裹整个组件,这样组件只在客户端渲染,服务端不会输出 HTML,自然不会有 hydration 问题,ssr: false只能用在客户端组件('use client'),可以再包装一层。
// app/ui/dashboard/LatestInvoicesClientWrapper.tsx
"use client";
import dynamic from "next/dynamic";
const LatestInvoicesClient = dynamic(
() => import("@/app/ui/dashboard/latest-invoices-client"),
{ ssr: false }
);
export default function LatestInvoicesClientWrapper() {
return <LatestInvoicesClient />;
}
// app/dashboard/page.tsx
import LatestInvoicesClientWrapper from "@/app/ui/dashboard/latest-invoices-client-wrapper";
参考资料
● 流式 HTML