cover_image

Nextjs-现代网站的优选全栈框架简介

青州 拍码场
2025年02月19日 03:19

图片

简介

  • Next.js 是一个基于 React 的轻量级框架,它使得构建服务端渲染 (SSR) 和静态站点生成 (SSG) 的React程序变得简单和高效。
  • 它拥有一系列特性和优势,包括但不限于:

    • 服务器端渲染(SSR):Next.js 支持开箱即用的服务器端渲染,这有助于提高首次加载页面的速度和 SEO 效果。
    • 静态站点生成(SSG):可以提前生成静态文件,在请求时直接返回 HTML 文件,适合博客、文档等不经常变化的内容。
    • 增量静态再生(ISR):结合了 SSG 和 SSR 的优势,在构建时生成静态文件,之后可以根据需求在后台重新生成特定页面,保持内容更新的同时享受静态文件的性能优势。
    • 自动代码分割:根据路由自动分割代码,确保每个页面只加载必要的 JavaScript,减少初始加载时间。
    • API 路由:内置支持创建 API 路由,方便编写后端逻辑,实现全栈开发。
    • 简化配置:默认配置了许多最佳实践,如 Webpack 和 Babel 的配置,减少了项目搭建的时间。支持CSS Modules,及css 预处理器,如 Less 和 Sass。
    • 热模块替换(HMR):开发过程中修改组件代码后无需刷新整个页面即可看到效果,提高了开发效率。
    • TypeScript 支持:对 TypeScript 有很好的支持,可以直接使用 .ts 或 .tsx 文件编写代码。

背景

Next.js 首次发布于 2016 年,并在 2017 年 4 月 20 日发布 7.0 版本。它的诞生基于开发者和企业对构建更快、更优化的web应用程序的需求。

  • React 生态的扩展需求:随着 React 在前端开发中的广泛应用,开发者对基于 React 的应用提出了更多需求,如更好的 SEO 支持、更快的页面加载速度等。传统的客户端渲染(CSR)虽然适合交互丰富的单页应用,但在首次加载性能和 SEO 方面存在不足。

  • 服务端渲染的需求:为了解决上述问题,服务端渲染(SSR)成为一种流行的解决方案。然而,实现 SSR 需要处理许多复杂的问题,如路由管理、数据获取、状态同步等。Next.js 应运而生,它简化了这些复杂性,使得开发者可以更轻松地构建 SSR 应用。

  • 静态站点生成的趋势:随着 JAMstack 架构的兴起,静态站点生成(SSG)成为构建高性能网站的一种流行方式。Next.js 不仅支持 SSR,还引入了 SSG 和增量静态再生(ISR),使得开发者可以根据需求灵活选择最适合的渲染方式。

  • 全栈开发的需求:现代 Web 开发越来越倾向于全栈开发,即前后端一体化。Next.js 内置了 API 路由功能,允许开发者在同一项目中同时编写前后端逻辑,进一步简化了全栈开发的流程。

一、环境&安装与初始化项目

node环境(环境参考官网最低版本要求)

npm、yarn或pnpm等工具(依个人习惯选择)

初始化项目

  • 使用 npx create-next-app@latest 命令快速创建新项目。安装时,将有如下内容:

What is your project named? my-app  Would you like to use TypeScript? No / Yes    Would you like to use ESLint? No / YesWould you like to use Tailwind CSS? No / YesWould you like your code inside a `src/` directory? No / YesWould you like to use App Router? (recommended) No / YesWould you like to use Turbopack for `next dev`?  No / YesWould you like to customize the import alias (`@/*` by default)? No / YesWhat import alias would you like configured? @/*

create-next-app会创建一个以你的项目名称命名的文件夹,并安装所需的依赖项。

  • 进入项目目录后,可以使用 npm run dev 启动开发服务器。

二、项目结构

项目创建完成后,我们的项目结构如下:

├── README.md                    ├── src│   └──app                     相当于src               │      ├── favicon.ico      │      ├── globals.css         全局CSS│      ├── layout.tsx          项目入口文件,相当于main.js│      └── page.tsx            首页地址文件,访问 /├── next-env.d.ts ├── next.config.mjs         next 配置文件├── package-lock.json├── package.json├── postcss.config.mjs      postcss配置文件 ├── public                  静态文件目录,存放图片      │   ├── next.svg  │   └── vercel.svg   ├── tailwind.config.ts      tailwindcss 配置文件   └── tsconfig.json 

顶层文件夹

图片

顶层文件有两种可能,根据上一步的Would you like your code inside a 'src/' directory? No / Yes选择结果,yes为右边结构,app和pages直接置于顶层,反之则为左边结构

名称用途
appApp Router
pagesPages Router
public静态文件目录
src可选的应用程序源文件夹

三、开始

layout

app/layout.tsx 是整个应用的主布局文件,相当于React的main.ts或App.tsx,它主要做以下几个事情:

  • 项目 metadata
  • 加载全局样式 globals.css
  • 加载网络/本地字体
  • 定义应用顶级布局
  • 国际化
  • 第三方组件库 Provider Wrapper

同时在每个页面目录下,也可以定义自己的布局文件layout.tsx

总结:顶层 RootLayout 作用于所有页面,各个子 Layout 只作用于自己所属的目录下的所有页面

图片图片

图片
// app/layout.tsxexport default function RootLayout({  children,}: Readonly<{  children: React.ReactNode;}>) {  return (    <html lang="en">      <body className={inter.className}>        {/* header */}        <Header />        {/* 页面注入区 */}        {children}      </body>    </html>  );}
// app/overview/layout.tsxexport default function Layout({  children,}: Readonly<{  children: React.ReactNode;}>) {  return (    <section className="flex mt-4 space-x-6">      {/* Nav */}      <Nav />       {/* main */}      <main className="w-full border border-green-600 p-4">{children}</main>    </section>  );}

字体

Nextjs 使用 next/font 模块加载谷歌字体,而不像以前通过css去加载字体,这是next帮我们优化字体的加载

为什么要优化字体?

  • 浏览器加载字体的流程

图片

进行替换时,会产生字体大小、空隙、布局的偏移,即CLS (CLS:谷歌用于评估网站性能和用户体验的指标之一,用于衡量网页在加载过程中内容布局的稳定性)

使用 next/font 模块后,NextJs会自动优化字体,项目构建时,会自动下载字体文件和其他资源文件放在一起,提升页面访问性能

例如我们使用Google字体:

// app/layout.tsximport { Geist } from 'next/font/google' const geist = Geist({  subsets: ['latin'],})export default function RootLayout({  children,}: {  children: React.ReactNode}) {  return (    <html lang="en" className={geist.className}>      <body>{children}</body>    </html>  )}

这样会将我们字体包含在我们的部署中,当我们访问网站时,字体将由我们的部署提供,而不是向Google发起请求。

也可以使用next/font/local从本地加载我们自己的字体文件

import localFont from 'next/font/local' const myFont = localFont({  src: './my-font.woff2',})... <html lang="en" className={myFont.className}>   <body>{children}</body> </html>

四、文件系统路由

Nextjs 内置文件路由系统,分App Router和Page Router两种模式,从Nextjs 14开始默认使用App Router模式,所以我们在此只介绍App Router。

页面路由

在App Router模式下,会根据app文件夹下的目录自动生成路由,目录内的可访问页面,必须固定为page.tsx(或.jsx.js)例如:

访问路径App Router简介
/app/page.tsx根路由
/aboutapp/about/page.tsx嵌套路由:xxx/xxx
/blog/1app/blog/[id]/page.tsx动态路由: [xxx]
/test/1/aapp/test/[...slug]/page.tsx捕获所有路线: [...xxx]
/a, /bapp/(test)/a/page.tsx, app/(test)/b/page.tsx路由组,只用来组织文件,不生成实际路线段: (xxx)

app/_folder/page.tsx私人文件夹,该带有_的文件夹及其子项不生成路由

除此之外还有平行和拦截路线等内容,具体可以参考官网示例

API路由

App Router模式下,api要放置在app目录下,而且,必须命名为 route.ts

访问路径App Router
/api/productapp/api/product/route.ts
/api/product/1app/api/product/[id]/route.ts

API路由处理

export async function GET(request: NextRequest) {   // 获取数据逻辑   const data = await ...   return NextResponse.json({ data });}
export async function POST(request: NextRequest) { const body = await request.json(); // 插入数据逻辑 return NextResponse.json({ msg: 'Created success' });}
export async function DELETE(request: NextRequest) { const id = request.nextUrl.searchParams.get('id')!; if (!id) { return NextResponse.json({ msg: 'Delete Failed' }); } // 删除数据逻辑 return NextResponse.json({ msg: 'Deleted Success ' });}
export async function PUT(request: NextRequest) { const id = request.nextUrl.searchParams.get('id'); if (!id) { return NextResponse.json({ msg: 'Update Failed' }); } // 更新数据逻辑 return NextResponse.json({ msg: 'Updated Success' });}

五、Not Found

NextJs 会自动生成 404 页面,如果我们要使用自己的 404 页面,要遵循以下几个原则:

全局404页面

在 app 目录下,创建 not-found.tsx 文件,当访问路由不存在时,自动跳转到全局 not-found 页面

注意:全局 not-found.tsx 只能在app目录下,否则不生效, /app/not-found.tsx

局部404页面

  • 局部 404 多用于请求数据获取不到而出现的场景,而非无此页面的情况
  • 在某个页面或多个共享layout页面目录下,创建 not-found.tsx 文件,
  • 需要手动触发 next/navigationnotFound 方法 比如我们有一个 /app/overview/not-found.tsx,当访问 /app/overview/page2 时,page2页面获取不到接口数据时,出现局部404
// /app/overview/page2/page.tsximport { notFound } from 'next/navigation';
export default function Page1() { // 模拟获取数据 const data = null;
if (!data) { return notFound(); }
return <h1>I am page2...</h1>;}

图片


六、Error

全局错误页面

  • 全局错误页面,只能在 app 目录下,创建 global-error.tsx,否则不生效
  • 在 production 环境下,可以看到全局错误页面,开发环境不可见
'use client';
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return ( <html> <body> <h2>Something went wrong!</h2> <button onClick={() => reset()} >Try again</button> </body> </html> );}

局部错误页面

在某个页面或多个共享layout页面目录下,创建 error.tsx

//当发生错误时,error.tsx替换当前page.tsx内容展示,效果参考局部404效果export default async function Page1() { throw new Error('xxx');
return <h1>I am page2...</h1>;}

七、Loading

当访问 loading.tsx 所在目录下的任何页面时,浏览器会先自动加载并渲染 loading,当页面准备好后,会隐藏loading,再显示页面。

  • 全局loading:app/loading.tsx
  • 局部loading: app/xxxx/loading.tsx

图片

Loading 隔离

/app/overview  | page1/page.tsx  | page2/page.tsx  | page.tsx  | loading.tsx

局部loading 将作用于 overview 下的所有页面

/app/overview  | page1    | page.tsx    | loading.tsx  | page2/page.tsx  | page.tsx

局部loading 只有作用于 overview/page1

如果让局部 loading 只作用于某一个页面,而不作用于该目录下的其他页面的情况,该如何实现呢?

使用路由组包裹单独作用域的loading和page

/app/overview  |(overview)     | loading.tsx     | page.tsx  | page1/page.tsx  | page2/page.tsx

loading 将作用于 overview/(overview) 下的所有页面,但是实际上并没有(overview)这个路由,所以访问 /overview 时,出现局部loading。但访问 /overview/page1 或 /overview/page2,因为不会进入到(overview)文件夹, 会回退到全局 loading。

八、SSG与SSR

NextJS 14 默认SSG。看一个例子

// http://localhost:3000import { ArticleApi } from '@/app/api/article-api';
export default async function Home() { const articles = await ArticleApi.fetchToday();
return ( <> <section>{new Date().toLocaleTimeString()}</section> <section>{JSON.stringify(articles)}</section>; </> );}

默认情况下,当你 npm run build 后,再 npm run start,无论你刷新多少次浏览器,内容都是不变的,即使有 fetch 也不行, 显示的时间仍旧是不变的

SSG:静态渲染,在服务端构建部署时,数据重新生效,产生的静态页面可以被分发、缓存到全世界各地

  • 收益:访问更快、减轻服务器压力、利于SEO
  • 场景:没有变化的数据、多页面共享的数据

SSR:动态渲染,在服务端接收到每个用户请求时,重新请求数据,重新渲染页面

  • 收益:显示实时数据、特定用户的特定数据(用于区别对待)、可以获取到客户端请求的cookie和URL参数


Cache

SSG生效时间

当你需要给SSG固定一个缓存生效时间,可以在页面文件顶部增加一行

export const revalidate = 10; // 10秒
export default Page() {}

加入这行代码后,重新执行 npm run build -> npm start可以在浏览器观察到, 每10s 刷新后,时间才会发生变化,这是因为给整个页面设置了 revalidate 为 10s

SSR

如果你不想将页面预渲染缓存,可以在页面文件顶部加入一行,使每次访问时重新渲染

export const dynamic = 'force-dynamic';
export default Page() {}

SSG 预渲染动态参数的页面

使用generateStaticParams来获取生成页面的数据

// app/blog/[slug]/page.tsxexport async function generateStaticParams() {
const posts = await fetch('https://...').then((res) => res.json()) return posts.map((post) => ({ slug: post.slug, }))}
export default async function Page({ params,}: { params: Promise<{ slug: string }>}) { const { slug } = await params // ...}

Client Component vs Server Compoennt

NextJs 默认将所有组件都视为服务端组件

  • Client Component: 客户端组件,组件顶部有 'use client'标识,用户可以在浏览器进行页面交互,比如使用 useState、useEffuct、next/navigation等,「它既在服务器又在浏览器运行,先服务器,然后浏览器」
  • Server Component: 服务端组件,完全在服务端渲染,因此无法进行用户交互

如果组件使用了 useStateuseEffect 或 第三方库(用于交互的)等,而没有将文件标识为 'use client',系统将会报错

如果没有将一个文件手动标记为 'use server'(虽然NextJs默认将所有文件都视为在服务端运行),但是如果你在客户端组件'use client' 里面想直接调用这个服务端的函数,比如执行 form action、点击请求service, 一定要提前告诉浏览器,这个函数是运行在服务端的。如果不做标记,默认会视为服务端组件,但是你使用'use client'的组件,import 要调用的函数,浏览器会把它视为改组件的下级,当作在浏览器执行的。

这个时候我们有两种标记方式:

方式一:将客户端组件交互的函数内标记为 'use server'
// server, /app/actionss/user.tsexport async function createUser(body{}export async function test({}
// client, /app/user/create/page.tsx'use client';import { createUser, test } from '@/app/actions/user';export default function Page({ async function create(formData: FormData{ 'use server'// 标记 // 调用服务端 service await createUer(formData); } async function clickHandler({ 'use server'// 标记 await test() } return (<> <form action={create}>...</form> <section onClick={clickHandler}>test</section> </>);}
方式二:直接在服务器文件顶部标记 use server,表示这个文件在服务端运行,客户端组件上调用时,就不用在函数内部再标识 use server了
// server, /app/actionss/user.ts'use server';export async function createUser(body) {}export async function test() {}



// client, /app/user/create/page.tsx'use client';import { createUser, test } from '@/app/actions/user';export default function Page() {async function create(formData: FormData) { // 'use server'; // 不标记 // 调用服务端 service await createUer(formData);}async function clickHandler() { // 'use server'; // 不标记 await test()}return (<> <form action={create}>...</form> <section onClick={clickHandler}>test</section> </>);}

服务器组件的注意事项

当一个文件标识为 'use server',那么它 export 的函数必须为 async,export 对象不行,普通函数也不行,否则客户端组件 import 时,报错

"use server" file can only export async functions, found object. These functions are required to be defined as async, because "use server" marks them as Server Actions and they can be invoked directly from the client through a network request.

服务器组件中只能导出类型和异步函数

//❌ 以下写法会报错// api/service.ts'use server';export const QueryUserSchema = z    .object({      name: z.string(),      age: z.number().optional()    });    // api/route.tsimport { QueryUserSchema } from './service.ts';export async function GET(req: NextRequest) {    const filters =         getQueryParams<typeof QueryUserSchema._type>(req);
return NextResponse.json({});}
//✅ 这样不会// api/service.ts'use server';const QueryUserSchema = z    .object({      name: z.string(),      age: z.number().optional()    });    export type QueryUserParams = typeof    QueryUserSchema._type;    // api/route.tsimport { QueryUserParams } from './service.ts';export async function GET(req: NextRequest) {    const filters =         getQueryParams<QueryUserParams>(req);
return NextResponse.json({});}

九、SEO

  • 在 /app/layout.tsx中,设置顶级 metadata

  • 在页面中也可以设置专属页面 metadata,并与顶级 metadata 进行merge

    设置meta内容的方式有两种

  • 1. 在组件中导出metadata:

    //layout.tsx|page.tsximport type { Metadata } from 'next'export const metadata: Metadata = {title: '...',description: '...',}export default function Page() {}

    2. 在组件中导出generateMetadata函数(不能再layout中使用):

    //动态获取参数设置metaimport type { Metadata, ResolvingMetadata } from 'next'type Props = {  params: Promise<{ id: string }>  searchParams: Promise<{ [key: string]: string | string[] | undefined }>}export async function generateMetadata(  { params, searchParams }: Props,  parent: ResolvingMetadata): Promise<Metadata> {// read route paramsconst id = (await params).id// fetch dataconst product = await fetch(`https://.../${id}`).then((res) => res.json())// optionally access and extend (rather than replace) parent metadataconst previousImages = (await parent).openGraph?.images || []return {  title: product.title,  openGraph: {  images: ['/some-specific-page-image.jpg', ...previousImages],},  }}export default function Page({ params, searchParams }:Props) {}

十、Middleware 中间件

在 NextJS 中,中间件允许在请求完成之前(在缓存内容和路由匹配之前)运行代码,然后,根据传入的请求,可以通过重写、重定向、修改请求或响应标头或直接响应来修改响应
图片
在项目根目录中创建middleware.ts(或.js),要与app目录在同一级,app在src内部时,中间件也要在src内部


// /middleware.ts或/src/middleware.tsimport { NextResponse, NextRequest } from 'next/server' // 中间件处理逻辑,也可以是 async 函数export function middleware(request: NextRequest) {  return NextResponse.redirect(new URL('/home', request.url))}// 配置项export const config = {  matcher: '/about/:path*',}

该文件必须导出单个函数,可以是默认导出或命名的middleware,但是不支持导出多个中间件

export default function middleware(request) {  // Middleware logic}

配置项(可选)

在项目中,每个请求都会经过中间件,比如图片,api或者ico文件,实际上我们不需要每个请求都处理,我们可以在middleware.ts导出名为config的配置对象,这个对象包含matcher,可以指定适用于中间件的路径matcher可以指定固定的路径或者使用正则表达式,例如:

export const config = {  matcher: '/about',}// orexport const config = {   matcher: ['/about', '/contact'],}// orexport const config = {  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],}

matcher还可以是一个对象,包含以下属性

  • source:用于匹配请求路径的路径或模式。它可以是用于直接路径匹配的字符串,也可以是用于更复杂匹配的模式。
  • regexp(可选):根据源微调匹配的正则表达式字符串。它提供了对包含或排除哪些路径的额外控制。
  • locale(可选):布尔值,当设置为时false,将在路径匹配中忽略基于语言环境的路由。
  • has(可选):根据特定请求元素(例如标头、查询参数或 cookie)的存在指定条件。
  • missing(可选):关注某些请求元素缺失的情况,例如缺少标头或 cookie。
export const config = {  matcher: [    {      source: '/api/*',      regexp: '^/api/(.*)',      locale: false,      has: [        { type: 'header', key: 'Authorization', value: 'Bearer Token' },        { type: 'query', key: 'userId', value: '123' },      ],      missing: [{ type: 'cookie', key: 'session', value: 'active' }],    },  ],}

中间件的常用场景

  • 身份验证和授权:在授予对特定页面或 API 路由的访问权限之前,确保用户身份并检查会话 cookie
  • 服务器端重定向:根据特定条件(例如,区域设置、用户角色)在服务器级别重定向用户
  • 路径重写:支持 A/B 测试、功能部署或旧版路径,根据请求属性动态重写路径到 API 路由或页面。
  • 日志记录和分析:在页面或 API 处理之前捕获和分析请求数据以获取内容
  • 功能标记:动态启用或禁用功能,以实现无缝的功能推出或测试

不建议使用的场景

  • 复杂的数据获取和操作
  • 繁重的计算任务:中间件应该是轻量级的,响应速度快,否则可能会导致页面加载延迟
  • 大量的session处理:虽然中间件可以处理基本的session task,但大量的session处理应由专用的身份验证服务或路由处理程序管理
  • 不建议在中间件中执行直接数据库操作

操作 Cookie、Headers 和 CORS

Cookie

export default function middleware(req: NextRequest) { // 在request.cookies可以查看请求的cookie let cookie = req.cookies.get('xxx'); console.log(cookie); const allCookies = req.cookies.getAll(); console.log(allCookies); req.cookies.has('xxx'); // => boolean req.cookies.delete('xxx'); // `ResponseCookies` API来设置响应cookie const response = NextResponse.next(); response.cookies.set('vercel', 'fast'); response.cookies.set({            name: 'vercel',            value: 'fast',            path: '/', }); cookie = response.cookies.get('vercel'); console.log(cookie);        return response; // 会在浏览器中设置 cookie}

Headers

export default function middleware(req: NextRequest) { // Clone the request headers and set a new header `x-hello-from-middleware1` const requestHeaders = new Headers(req.headers); requestHeaders.set('x-hello-from-middleware1', 'hello'); // 可以在 api 请求中,req.headers 看到  'x-hello-from-middleware1' => { name: 'x-hello-from-middleware1', value: 'hello' },
// NextResponse.rewrite设置handers const response = NextResponse.next({ request: { // New request headers headers: requestHeaders, }, });
response.headers.set('x-hello-from-middleware2', 'hello'); return response;}

CORS

import { NextRequest, NextResponse } from 'next/server';
const allowedOrigins = ['https://acme.com', 'https://my-app.org'];
const corsOptions = { 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization',};
export function middleware(request: NextRequest) { const origin = request.headers.get('origin') ?? ''; const isAllowedOrigin = allowedOrigins.includes(origin);
const isPreflight = request.method === 'OPTIONS';
if (isPreflight) { const preflightHeaders = { ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }), ...corsOptions, }; return NextResponse.json({}, { headers: preflightHeaders }); }
const response = NextResponse.next();
if (isAllowedOrigin) { response.headers.set('Access-Control-Allow-Origin', origin); }
Object.entries(corsOptions).forEach(([key, value]) => { response.headers.set(key, value); });
return response;}
export const config = { matcher: '/api/:path*',};

结语

Nextjs拥有强大的的功能和灵活的特性,从基本的项目构建,到深入的数据获取,样式处理再到高级的性能优化,错误处理,SEO,以及最后的部署与运维,每一步都紧密相连,共同构建起了高效,稳定且用户体验良好的web应用程序。

 直播预告

 招聘信息



图片往期精彩内容指路


基于Dify工作流的AI查单助手实践

高并发场景性能优化-剖析接口超时解决方案

基于eBPF的可观测性建设

基于AI的智能测试助手

交互式营销系统:基于用户行为意图营销

WebSocket协议在作业系统的落地实践

继续滑动看下一个
拍码场
向上滑动看下一个