全栈框架next.js入手指南

作为基于react.js的全栈框架next.js在现在不可谓不热门,我个人也上手用了一段时间,体验上面来说还是不错的。所以在这里,给大家分享一下next.js的入手指南!

如果有理解不到位的地方,还请指正!

基本介绍

该篇落简单介绍next.js是什么,和react.js的关系,以及怎么创建一个next.js的项目。

认识next.js

这一篇落,我们来认识一下什么是next.js,为什么要使用它来进行开发。

与react.js的关系

首先,我们要了解next.js这个框架,就要知道一个点,那就是next.js是基于react.js之上构建的一个全栈框架,也可以说是react.js的框架。

并且,在react.js的基础上,next.js增加了更多的附加功能和其他优化。

为什么要使用next.js

既然next.js也是基于react.js的,那为啥用next.js而不是直接用react.js呢?

这是因为next.js它本身就有几个react.js默认没有的优点:

  1. 默认支持SSRSSG,有着更好的SEO和首屏加载速度;
  2. 内置路由系统,搭配模版和页面的使用,可以不用再去配置react-router
  3. 内置API系统,例如博客这类简单的后端功能甚至直接可以用next.js完成;

这是我认为next.js相比react.js下,体现出来的优点,当然也还有其他的地方。

SSR和SSG

这是我们经常能听到的两个名词:SSRSSG,他们对应的中文翻译叫做:服务端渲染和静态站点渲染。区别就是在于一个是实时渲染,一个是构建时预渲染,具体区别如下:

方式 SSR SSG
渲染时机 请求的时候 构建的时候
响应速度 很快
服务器压力
适合场景 需展示最新的数据 固定显示的内容

项目初始化

这一篇落,我们来说一下如何用next.js官方的脚手架创建一个项目并且简单介绍目录结构的功能。

create-next-app

创建next.js的项目,我们需要使用create-next-app来进行项目搭建,打开终端,输入以下命令:

shell 复制代码
npx create-next-app@latest demo

此时,可以看到界面询问我们创建next.js需要选择的功能,这个按个人需求来选择,选择完成后最终效果如下图:

此时,项目已经创建完毕,使用VSCode打开该项目。默认情况下,目录结构下的内容并不多,这里我们需要了解的是几个地方:

  • public/ 用于存放静态资源的目录
  • src/app 用于存放页面的目录(这里是AppRouter,是官方目前推荐的,对应的还有老版本PageRouter,不过不再推荐)
  • next.config.ts 这个是next.js的框架配置文件,功能很多也很重要
  • eslint.config.mjs 这个是eslint的配置文件

现在,我们着重关注的应该是src目录,之后的许多工作如业务代码编写,都会在该目录下进行。

从这一篇落起,就一直在提到目录,是因为在next.js项目中,目录非常重要。

next.js中,有着默认的约定,例如这里:src/app目录下就对应页面目录,每一个以page.tsx命令的文件就是一个页面。

我们要严格遵循next.js的目录约定,否则会有意想不到的问题产生!

目录结构

目录结构是next.js的一个重点,因为next.js的目录命名是约定式的,即不同的目录命令可能对应着不同的功能,若使用错误,则会导致意想不到的问题产生。

next.js的目录中,包括布局、页面、中间件等等功能的命名约定...后面我们说的理论内容,也基本都和每个目录或文件有关。

public公共目录

对于静态文件,通常在public目录下进行存放,例如图片这一类的资源,当需要访问时,直接通过/即可。

例如:在public目录下有一张图片logo.png,访问方式如下:

tsx 复制代码
<Image src="/logo.png" alt="logo" width={32} height={32} />

布局

next.js中,命名为layout.tsx的文件就叫布局。他的功能在于,定义公共部分的UI,该部分UI不会受路由的切换而更新,通常用于导航栏、侧边栏或者底部。

例如下面的布局中,当我们切换路由时,变化的是main标签里面的内容,而headerfooter标签内容不会改变。

tsx 复制代码
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <header>Header</header>
        <main>
          {/* 当路由切换,这里的内容将进行更新 */}
          {children}
        </main>
        <footer>Footer</footer>
      </body>
    </html>
  );
}

另外,除了默认的根布局以外,我们还可以在app下每一个页面目录重新创建layout.tsx来定义子布局,以此来创建出更多的布局效果。

如果需要创建多个根布局,那就需要用到"组"概念,我们后续来谈。

页面

页面是next.js的第二个核心功能,每一个页面我们都由page.tsx来命名。在next.js中,默认已经存在首页,即src/app/page.tsx文件。通过访问/根路径,我们将看到此页面。

如果想创建新的页面,那就需要新建一个新的目录,并且添加page.tsx文件。例如,这里我们在src/app目录下面,新建hello/page.tsx文件,编写如下代码:

tsx 复制代码
function HelloPage() {
  return (
    <div>
      HelloPage
    </div>
  );
}

export default HelloPage;

此时,我们就已经成功创建了一个页面,如何访问这个页面呢?还记得前面说的next.js重在约定嘛,在app目录下面的每一个目录命名,即代表页面路由的命名。

所以这里的hello目录名对应的路由即为/hello,此时访问/hello可以看到如下内容:

动态路由

通过上面的例子可以实现页面和路由创建,那假如此时有这样一个新需求:通过访问hello/a或者hello/b或者hello/xxxx都能匹配到同一个页面,并根据匹配不同的路径来显示不同的内容。

这里就需要使用到next.js的动态路由功能了,动态路由以中括号[...]来命名,根据上面例子我们则需要更改hello目录为hello/[slug]/page.tsx

这里的[slug]就表示动态匹配(slug不是必须这个格式,但是获取参数需要根据对应名称来获取)。

此时修改hello/[slug]/page.tsx代码如下:

tsx 复制代码
async function HelloPage({ params }: { params: Promise<{ slug: string }>}) {
  const { slug } = await params;
  return (
    <div>
      Hello:{ slug }
    </div>
  );
}

export default HelloPage;

此时再访问/hello/a时,将会看到如下内容:

如何匹配多层动态路由

当需要捕获多层动态路由时,例如:/hello/a/b/c,此时就需要通过[...slug]这种方式命名目录,更改hello/[slug]/page.tsxhello/[...slug]/page.tsx,修改为如下代码:

tsx 复制代码
async function HelloPage({ params }: { params: Promise<{ slug: string[] }>}) {
  const { slug } = await params;
  return (
    <div>
      Hello:{...slug}
    </div>
  );
}

export default HelloPage;

此时再访问/hello/a/b/c将会看到如下页面:

这里的params是固定的写法,与searchParams不同,通过params来获取的是动态路由上面的参数!

...slug\[...slug]

动态路由的命名方式有两种,一种就是刚刚使用的[...slug],还有一种就是[[...slug]]。他们的区别就是在于,是否有匹配的动态参数。

例如当访问/hello时,[...slug]会出现404,而[[...slug]]依然呈现页面,只是没有参数。

参数获取

除了动态路由的方式能获取参数以外,还可以通过路径后面的?追加参数并获取,例如当访问hello/a?name=cola时,此时可以通过下面方式获取参数:

tsx 复制代码
async function HelloPage({
  params,
  searchParams
}: {
  params: Promise<{ slug: string[]}>,
  searchParams: Promise<Record<string, string>>
}) {
  const { slug } = await params;
  const { name } = await searchParams;
  return (
    <div>
      Hello:{...slug},{name}
    </div>
  );
}

export default HelloPage;

此时页面将会更新为:

服务端渲染

默认创建的页面都是服务端渲染 的页面,即不能使用react.jsuseState或者useEffect这类依赖浏览器类的api,需要定义变量直接在函数里面定义即可,也不需理会生命周期等元素,例如:

tsx 复制代码
async function HelloPage({
  params,
  searchParams
}: {
  params: Promise<{ slug: string[]}>,
  searchParams: Promise<Record<string, string>>
}) {
  const { slug } = await params;
  const { name } = await searchParams;
  const animals = ['松鼠', '大象', '老虎'];
  // const [animals] = useState(['松鼠', '大象', '老虎'];); 不能使用,会报错

  return (
    <div>
      <p>Hello:{...slug},{name}</p>
      <p>{animals}</p>
    </div>
  );
}

export default HelloPage;

客户端渲染

倘若我们需要页面进行客户端渲染 而不是服务端渲染 ,此时则需要再page.tsx文件顶部添加'use client;'来进行标记,此时next.js将对该文件进行客户端渲染

当使用客户端渲染 时,此时就要用到react.js里面的useState或者useEffect这类钩子函数,而不是像服务端组件那样直接定义变量。

另外,在客户端组件 中,因为不能使用async来定义函数,所以获取参数的方式也有变化,其改变如下代码所示:

tsx 复制代码
'use client';

import { useSearchParams } from "next/navigation";
import { use } from "react";

function HelloPage({
  params,
}: {
  params: Promise<{ slug: string[]}>,
  searchParams: Promise<Record<string, string>>
}) {
  const { slug } = use(params);
  const name = useSearchParams().get('name');
  const animals = ['松鼠', '大象', '老虎'];

  return (
    <div>
      <p>Hello:{...slug},{name}</p>
      <p>{animals}</p>
    </div>
  );
}

export default HelloPage;

动态路由的参数params通过React.use来获取,而路径参数则通过useSearchParams方法获取之后,再通过get或者getAll获取参数。

注意事项

即使是使用了客户端渲染的页面,也可能出现window is not defined这类问题产生,这可能是由于引入的第三库直接就使用了window的原因。而next.js的页面呈现,也会在node环境下进行,所以导致该类问题产生。

解决方案:考虑在useEffect钩子函数里面动态引入。

404页面

next.js目录中,我们可以用not-found.tsx文件来命名404页面,不过情况又分为两种。

路径404页面

路径404页面是指我们访问不存在的路由时,展示出来的页面。一般来说,当访问不存在的路由时,都会返回app目录下的not-found.tsx文件。

逻辑404页面

逻辑404页面是指,在页面中可能遇到不存在的情况时,需要通过代码来跳转到404页面。例如:动态路由需要传递[id],但此时获得了不为数字的id时,就可以通过执行notFound方法来跳转。

逻辑跳转会从当前目录的not-found.tsx文件开始查找,直到根目录下的not-found.tsx文件。

加载页面

加载页面即loading.tsx是在数据还在请求或者组件正在挂载时,展示的页面。它也可以放在根目录或者其他页面目录下,其原理就等同于Suspense组件。

vscode 复制代码
app/
  dashboard/
    page.tsx
    loading.tsx
tsx 复制代码
// 框架内部伪代码
<Suspense fallback={<DashboardLoading />}>
  <DashboardPage />
</Suspense>

错误页面

组与私有目录

组和私有目录都是在app目录下,但不会被next.js识别为页面的两种命名形式。

通过(...)来命名,可以用于多级根布局的实现,例如有模块A模块B两个模块,需要不同的布局。此时,创建两个目录(A)(B),然后分别在其目录下创建新的layout.tsx布局文件。

私有目录

私有目录 通过_来命名,可以用于表示存放组件的目录,例如页面pageA下需要创建一些只用于该页面的组件时,就可以在pageA目录下创建_components目录来存放。

并行路由和拦截路由

并行路由和拦截路由都是不被next.js识别为页面的两种命名方式,拦截路由依托于并行路由来实现效果。

并行路由

先来说一下并行路由,并行路由是在页面目录下通过@xxx/page.tsx命名的文件,该页面也可以像children一样通过layout.tsx展示,方式如下:

tsx 复制代码
// pageB/@top/page.tsx
function PageBTop() {
  return (
    <div>
      PageBTop
    </div>
  );
}

export default PageBTop;
tsx 复制代码
// pageB/layout.tsx
import { ReactNode } from "react";

function LayoutB({
  children,
  top,
}: {
  children: ReactNode,
  top: ReactNode,
}) {
  return (
    <div className="border border-red-400">
      <div>LayoutB-Header</div>
      <div>{top}</div>
      <div>{children}</div>
      <div>LayoutB-Footer</div>
    </div>
  );
}

export default LayoutB;

其展示的页面如下:

对比写到一个page.tsx文件的好处就是,并行路由可以单独的写其他逻辑,例如loading.tsxnot-found.tsx等文件,甚至也可以在并行路由下创建新的路由。

注意:创建并行路由没有生效时,删除.next文件重新启动。

拦截路由

拦截路由是用于,当点击某个会进行路由跳转的UI(图片、按钮等)时,不进行页面跳转,而是在当前页面中进行显示,写法为:

  • (.)匹配同一级别的段
  • (..)匹配上一级的段
  • (..)(..)匹配上两级的段
  • (...)匹配 app目录中的段

现在修改pageB目录,将@top/目录为@top/(..)pageA/page.tsx,此时当在pageB目录下的进行点击跳转到/pageA路由时,将会弹出@top/(..)pageA/page.tsx下的文件,如下:

tsx 复制代码
// @top/(..)pageA/page.tsx
function PageA() {
  return (
    <div className="fixed left-1/2 top-1/2 -translate-1/2 w-lg aspect-[3/2] border border-gray-100 bg-white shadow-md p-3 rounded-md">
      PageA
    </div>
  );
}

export default PageA;

提示

上面记得要在@top目录下创建一个default.tsx文件,否则会出现404问题,内容如下即可:

javascript 复制代码
// @top/default.tsx
function Page() {
  return null;
}

export default Page;
tsx 复制代码
// pageB/layout.tsx
import Link from "next/link";
import { ReactNode } from "react";

function LayoutB({
  children,
  top,
}: {
  children: ReactNode,
  top: ReactNode,
}) {
  return (
    <div className="border border-red-400">
      <div>LayoutB-Header</div>
      <div>
        {children}
        <Link href="/pageA">拦截路由</Link>
      </div>
      <div>LayoutB-Footer</div>
      {top}
    </div>
  );
}

export default LayoutB;

点击拦截路由跳转时,页面效果如下:

提示

拦截路由不会影响直接在浏览器中输入路由的操作,意味着输入/pageA路由时依然展示为pageA页面。

建议

如果不是非得使用拦截路由并行路由 的话,暂时不建议。我在一些论坛上看到挺多人提出问题的,自己在学习这部分时也有问题,有时需要删除.next重新启动服务才能解决。

中间件

next.js中,中间件一般是用来做路由拦截、响应或者鉴权来使用的。通过在src或者根目录下创建文件middleware.ts来使用,并且需要默认导出一个函数,如下:

ts 复制代码
// src/middleware
export default function middleware() {
  console.log('middleware.');
}

当刷新页面后,可以看到终端打印如下,输入了很多'middleware'语句:

这是因为middleware处理的不仅仅只是路由请求,还有其他资源请求,修改代码如下,再刷新页面可以看到:

tsx 复制代码
// src/middleware
import { NextRequest } from "next/server";

export default function middleware(req: NextRequest) {
  console.log(req.url + ':middleware.');
}

如果需要对指定路径进行处理的话,就需要使用匹配器,其使用方法如下:

ts 复制代码
// src/middleware
import { NextRequest } from "next/server";

export default function middleware(req: NextRequest) {
  console.log(req.url + ':middleware.');
}

export const config = {
  matcher: ['/pageB'],
}

此时只有当请求路径为/pageB的路径才会被中间件处理,刷新页面后打印效果如下:

所以,通过中间件功能我们也可以实现鉴权等功能,例如:

ts 复制代码
// src/middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('token');
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
}

export const config = { matcher: ['/dashboard/:path*'] };

中间件的功能还是比较丰富的,就不一一赘述了,具体可以看文档:next.js中间件

服务端API

next.js中可以通过在app目录下创建api目录来作为后端接口,进行响应。

目录名称api 是固定写法,可以通过next.config.ts配置文件进行修改。

api目录下,可以新建新的目录进行api接口命名,类似页面那样,例如在api目录新建hello/route.ts文件,就代表接口为:/api/hello

route.ts

现在已经知道怎么创建一个api了,如何创建对应的响应呢?通过next.js提供的写法来实现,如下:

ts 复制代码
// src/app/hello/route.ts
import { NextResponse } from "next/server"

export const GET = () => {
  return NextResponse.json({
    code: 0,
    data: 'this is data',
  }, { status: 200 });
}

上面的GET方法就表示为/api/helloGet请求处理逻辑,其他请求类型写法也一样。此时,打开浏览器访问http://localhost:3000/api/hello,可以看到显示如下:

元数据

next.js中,元数据其实就是通过js对象来管理head标签的一种方式,其作用包含对SEO的优化等,分为两种情况。

静态元数据

静态元数据,即固定显示的,无需动态修改。通过导出一个metadata对象即可,例如:

tsx 复制代码
import { Metadata } from "next";

export const metadata: Metadata = {
  title: 'PageC',
  description: 'PageC description.'
}

function PageC() {
  return (
    <div>
      PageC
    </div>
  );
}

export default PageC;

上面我们定义了titledescription两个元数据,此时再看页面可以发现:

动态元数据

当元数据需要动态添加时,此时就不能直接用对象的方式定义,而是通过函数generateMetadata来实现,如下:

ts 复制代码
import { Metadata } from "next";

export const generateMetadata = async (): Promise<Metadata> => {
  const getMetaData = () => new Promise<{ title: string, description: string }>(resolve => setTimeout(() => {
    resolve({
      title: 'Async PageC',
      description: 'Async PageC description.'
    })
  }, 1e3));

  const { title, description } = await getMetaData();

  return {
    title,
    description
  }
}

function PageC() {
  return (
    <div>
      PageC
    </div>
  );
}

export default PageC;

此时在页面上可以看到动态添加的属性,如下图:

元数据的配置还有很多,具体可以看:next.js元数据

图像

next.js中内置了经过优化的Image图像组件,使用该组件时需满足以下任意一条:

  • 给定明确的widthheight属性;
  • 给定fill属性;

例如:

tsx 复制代码
function PageC() {
  return (
    <div>
      PageC
      <Image src="/cover.jpg" alt="img" width={80} height={120} />
      <div className="w-[80px] h-[120px] relative">
          <Image src="/cover.jpg" alt="img" fill />
      </div>
    </div>
  );
}

export default PageC;

倘若使用远程图片的话,则需要在next.config.ts中进行配置,否则构建阶段可能出现意想不到的问题,具体配置可以看:next.js内置图像组件

缓存

next.js中,请求接口一般采用fetch方法,并在next.js对于fetch方法进行了扩展,增加了缓存的功能,而且缓存不仅与请求,对于页面来说next.js也进行了缓存功能的实现。

fetch缓存

在使用fetch时,当我们添加了如下参数,即进行了缓存:

ts 复制代码
// 此时在一分钟内的请求都会返回缓存的数据
fetch('https://...', { next: { revalidate: 60 } });

如果说不需要缓存功能的话,则用如下方式请求:

ts 复制代码
fetch('https://...', { cache: 'no-store' });

另外,在扩展的fetch请求中还可以通过打上tag来强制刷新,例如:

ts 复制代码
fetch('https://...', { next: { tags: ['test'] } });

当执行revalidateTag('test')后,下一次带有test标签的请求将会获取最新的数据。

页面缓存

在页面中,可以通过定义revalidate属性来配置缓存效果,例如:

tsx 复制代码
export const revalidate = 60;

function PageC() {
  return (
    <div>
      PageC
    </div>
  );
}

export default PageC;

此时,当通过next build构建时,将会预渲染这个页面,并且能缓存60s的时间,当过了缓存时间后再请求新的页面数据,该配置有3种:

  • false 强制为SSG模式,不再对页面进行动态渲染;
  • 大于0的数字 页面的缓存时间;
  • 0 不进行页面缓存,每次请求直接通过SSR来渲染页面;

默认来说,在next.js中是通过静态生成+SSR方式来进行构建的,也就是所谓的ISR增量静态再生。

样式

对于样式来说,第一个推荐直接使用css文件,然后在页面或者组件中直接引入,类似默认next.js项目引入global.css一样。

第二个就是推荐创建next.js时,可选安装的tailwindcss工具,这个工具是一个原子化CSS的写法,用起来非常方便和简洁。

ESLint

在创建next.js时,也可选用eslint工具,此时会在根目录生成eslint.config.mjs文件,内容如下:

js 复制代码
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    ignores: [
      "node_modules/**",
      ".next/**",
      "out/**",
      "build/**",
      "next-env.d.ts",
    ],
  },
];

export default eslintConfig;

如果需要自定义规则的话,可以修改上述代码,例如:

js 复制代码
const eslintConfig = [
  ...compat.config({
    extends: ['next/core-web-vitals', 'next/typescript'],
    // 自定义插件
    plugins: ['simple-import-sort'],
    // 自定义规则
    rules: {
      'semi': ['warn', 'always'],
      'quotes': ['error', 'single'],
      'simple-import-sort/imports': 'error',
      'simple-import-sort/exports': 'error'
    }
  }),
  {
    ignores: [
      'node_modules/**',
      '.next/**',
      'out/**',
      'build/**',
      'next-env.d.ts',
      'src/components/ui/**'
    ],
  },
];

ShadCN

这里推荐一个工具:shadcn,他不是单纯的UI库,而是将radix-uitailwindcssthemeicon等整合的一个工具平台,安装也很简单,如下:

shell 复制代码
npx shadcn@latest init

具体细节可以看:shadcn文档

结束

以上就是next.js的常用基础概念,因为其概念比较多,不能都详细说明,当然这也是优秀强大的框架带来的学习成本。其生态技术远不止这些,感兴趣的话可以通过论坛、视频等再进行学习!

相关推荐
weixin_3823952315 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__15 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.16 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~17 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华17 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子20 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅20 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni20 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学21 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端