分类: web前端 | 标签: Nextjs

Nextjs入门笔记

发表于: 2023-12-11 16:20:28 | 字数统计: 3.7k | 阅读时长预计: 18分钟

Nextjs英文官网

简介

nextjs是基于react语法来实现SSR(服务器渲染)的框架,解决单页应用不利于SEO的问题

安装

环境说明

  • node v18
  • next v14

执行下面的命令

npx create-next-app@latest

然后依次选择

H:\study\next-study>npx create-next-app@latest
Need to install the following packages:
  create-next-app@latest
Ok to proceed? (y) y
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'create-next-app@14.0.3',
npm WARN EBADENGINE   required: { node: '>=18.17.0' },
npm WARN EBADENGINE   current: { node: 'v16.15.0', npm: '8.5.5' }
npm WARN EBADENGINE }
√ What is your project named? ... my-app
√ Would you like to use TypeScript? ... No / Yes #使用ts
√ Would you like to use ESLint? ... No / Yes  #使用eslint校验
√ Would you like to use Tailwind CSS? ... No / Yes #使用Tailwind CSS样式库
√ Would you like to use `src/` directory? ... No / Yes #使用src目录
√ Would you like to use App Router? (recommended) ... No / Yes #使用app路由
√ Would you like to customize the default import alias (@/*)? ... No / Yes  #使用@相对路径
√ What import alias would you like configured? ... @/*

启动

npm run dev

访问:http://localhost:3000/

目录说明

- public 静态资源目录
- src 
  - app 页面目录(重点)
    - favicon.ico 网站favicon.ico图标
    - globals.css 全局css文件
    - layout.tsx 根布局组件(重点)
    - page.tsx 默认页面(重点)
- next.config.js 核心配置文件
- postcss.config.js postcss配置文件
- tailwind.config.ts tailwindcss配置文件
- tsconfig.json ts配置文件

路由

定义路由

Next.js使用基于文件系统的路由器,其中文件夹用于定义路由。 文件夹中的page.jsx/page.tsx/page.js文件代表的就是页面。

【案例:定义路由】

创建a页面:src/app/a/page.tsx 访问a页面:http://localhost:3000/a

import React from 'react'
const page = () => {
  return (
    <div>a页面</div>
  )
}
export default page

创建b页面:src/app/a/b/page.tsx 访问b页面:http://localhost:3000/a/b

import React from 'react'
const page = () => {
  return (
    <div>b页面</div>
  )
}
export default page

可以看到创建了两个嵌套的文件夹,文件夹可以无限嵌套。

页面使用page.tsx导出一个react组件,其中http://localhost:3000 访问的就是src/app/page.tsx

layout布局

布局是在多个页面之间共享的用户界面。在导航中,布局保留状态,保持交互,并且不重新呈现。布局也可以嵌套。

【案例:定义布局】

创建根布局:src/app/layout.tsx

1.根布局是所有页面共享的布局,并且是必须的

2.只有根布局可以包含< html >和< body >标记。

import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = { //定义网站的seo
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({children}: {children: React.ReactNode}) {
  return (
    <html lang="en">
      <body className="bg-slate-400">根布局:{children}</body>
    </html>
  )
}

创建a页面的布局:src/app/a/layout.tsx

import React from 'react'
const layout = ({children}:{children:React.ReactNode}) => {
  return (
    <div>layoutA:{children}</div>
  )
}
export default layout

访问http://localhost:3000/a

1701851198502

Link导航组件

Link组件可以使用页面跳转c

创建c页面:src/app/c/page.tsx

import React from 'react'
import Link from 'next/link'
const page = () => {
  return (
    <div>
      c组件<br />
      <Link className='text-blue-600' href="/a">去a组件</Link>
      </div>
  )
}
export default page

usePathname

这个hook可以获取路由地址

创建d页面:src/app/d/page.tsx

'use client' //表示这个是一个客户端组件
import React from 'react'
import { usePathname } from 'next/navigation'
const page = () => {
    const pathname = usePathname()
    return (
        <div>pathname: {pathname}</div>
    )
}
export default page

useRouter

这个hook可以实现编程式导航

创建e页面:src/app/d/page.tsx

'use client'
import React from 'react'
import { useRouter } from 'next/navigation'
const page = () => {
  const router = useRouter()
  return (
    <div>编程式导航:<br />
        <button onClick={() => router.push('/d')}>跳转到d页面</button>
    </div>
  )
}
export default page

路由组

路由组的命名除了对组织而言没有特殊意义。它们不影响URL路径。

如下用括号进行分组,不会影响URL路径。

1701852815965

http://localhost:3000/book
http://localhost:3000/user
http://localhost:3000/admin
http://localhost:3000/shop

动态路由

一般用于一些详情页面,比如商品详情、新闻详情等

创建product页面:src/app/product/[id]/page.tsx

import React from 'react'
const page = ({params}: {params: {id: number}}) => {
  return (
    <div>product: {params.id}</div>
  )
}
export default page

访问:http://localhost:3000/product/123

loading组件

用户展示loading效果

新增loading.tsx组件

import React from 'react'
import './style.css'
const loading = () => {
  return (
    <div className='text-red-500'>loading...</div>
  )
}
export default loading

新增layout.tsx布局

import { Suspense } from 'react'
import Loading from './loading'
export default function Layout({ children }: { children: React.ReactNode }) {
    return (
    <Suspense fallback={<Loading />}>
        <div>layoutE:{children}</div>
    </Suspense>
    )
}

新增page.tsx布局

'use client'
import React from 'react'
const page = async () => {
  const router = useRouter()
  //模拟服务器请求数据渲染
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('服务器渲染ok')
    }, 2000);
  })
  return (
    <div>我来了</div>
  )
}
export default page

参考:Next.js 13 如何使用loading.js

error组件

创建一个和page.tsx同级的error.tsx

'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({ error,reset}: {error: Error & { digest?: string },reset: () => void}) {
    
  useEffect(() => {
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={() => reset()}
      >
        Try again
      </button>
    </div>
  )
}

page.tsx中抛出错误

'use client'
import React from 'react'
const page = async () => {
  const router = useRouter()
  //模拟服务器请求数据渲染
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('服务器渲染error')
    }, 2000);
  })
  return (
    <div>我来了</div>
  )
}
export default page

404组件

app/not-found.tsx ,当页面找不到的时候就会展示这个组件。(名字固定)

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </div>
  )
}

特殊情况:当有动态路由时,那么上面的not-found组件无效,需要使用[…not_found]

app/[lang]/[…not_found]/page.tsx

export default function NotFound(props) {
    return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </div>
  )
}

参考:https://stackoverflow.com/questions/75302340/not-found-page-does-not-work-in-next-js-13

并行路由

并行路由允许在同一布局中同时或有条件地呈现一个或多个页面。对于应用程序的高度动态部分,并行路由可用于实现复杂的路由模式。

1.并行路由是使用命名文件夹创建的。文件夹名是用 @文件名 约定定义的,并作为props传递给同一级别的布局。

2.并行路由文件夹中也支持error.tsx和loading.tsx

创建src/app/@analytics/page.tsx页面

import React from 'react'
const page = () => {
  return (
    <>analytics page</>
  )
}
export default page

创建src/app/@team/page.tsx页面

import React from 'react'
const page = () => {
  return (
    <>team page</>
  )
}
export default page

修改src/app/layout.tsx

import type { Metadata } from 'next'
import './globals.css'
import React from 'react'

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

// export default function RootLayout({children}: {children: React.ReactNode}) {
export default function RootLayout(props: {
  children: React.ReactNode,
  team: React.ReactNode, //接受并行路由
  analytics: React.ReactNode,//接受并行路由
}) {
  return (
    <html lang="en">
      <body className="bg-slate-400">
        根布局:{props.children}
        <div className='text-blue-500'>team: {props.team}</div>
        <div className='text-green-400'>analytics: {props.analytics}</div>
        </body>
    </html>
  )
}

此时访问http://localhost:3000/可以并行路由可以正常展示,而访问其它级别比如http://localhost:3000/a,那么并行路由无法展示,因为并行路由作为`props`只传递给**同一级别**的布局

中间件

中间件允许在请求完成之前运行代码。然后,根据传入的请求,可以通过重写、重定向、修改请求或响应头或直接响应来修改响应。 (类似拦截器)

新建文件middleware.ts(或js,文件名固定)来定义中间件。与pages或app处于同一级别,或者在src内部。

比如下面定义的一个用于国际化的中间件

import { NextResponse } from 'next/server'
 
let locales = ['en', 'zh']
 
// Get the preferred locale, similar to above or using a library
function getLocale(request) {  //设置默认语言
    return 'zh'
}
 
export function middleware(request) {
  const pathname = request.nextUrl.pathname
  //判断不是/en 或者/zh开头
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  )
 
  // 如果不是/en 或者/zh开头 那么重定向到/zh或者/en
  if (pathnameIsMissingLocale) {
    const locale = getLocale(request)
    return NextResponse.redirect(
      new URL(`/${locale}/${pathname}`, request.url)
    )
  }
}
//不需要拦截的路径
export const config = {
  matcher: [
    // Skip all internal paths (_next)
    //'/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
    '/((?!api|_next/static|_next/image|favicon.ico|resources).*)'
  ],
}

组件

组件有客户端组件(由浏览器渲染的组件)、服务端组件(由服务器渲染的组件)、内置组件

客户端组件和服务端组件需要自己编写,默认是服务端组件,内置组件由next提供。

服务端组件

默认就是服务器组件,按照react组件的写法即可

客户端组件

需要在组件的第一行加上use client

内置组件

Image组件

用法和img标签类似,只不过做了性能优化

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="Picture of the author"
    />
  )
}

Image组件可配置的属性如下:

PropExampleTypeStatus
srcsrc="/profile.png"StringRequired
widthwidth={500}Integer (px)Required
heightheight={500}Integer (px)Required
altalt="Picture of the author"StringRequired
loaderloader={imageLoader}Function-
fillfill={true}Boolean-
sizessizes="(max-width: 768px) 100vw, 33vw"String-
qualityquality={80}Integer (1-100)-
prioritypriority={true}Boolean-
placeholderplaceholder="blur"String-
stylestyle={{objectFit: "contain"}}Object-
onLoadingCompleteonLoadingComplete={img => done())}FunctionDeprecated
onLoadonLoad={event => done())}Function-
onErroronError(event => fail()}Function-
loadingloading="lazy"String-
blurDataURLblurDataURL="data:image/jpeg..."String-

数据获取

服务器渲染:在服务器上请求数据并渲染好组件

import React from 'react'

interface IUser {
    name: string,
    age: number
}

//1.请求数据的方法
const getData = async () => {
    //模拟后端请求,这里也可以换成第三方的请求库,比如axios
    const res: IUser = await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: 'tom', age: 18 })
        }, 1000);
    })
    console.log('getData res', res)
    return res
}

//2.使用async关键字标记组件
export default async function page() {
    //3.请求数据并渲染组件
    const data = await getData()
    return (
        <div>
            <div>name: {data.name}</div>
            <div>age: {data.age}</div>
        </div>
    )
}

客户端渲染:和react的写法一直,在useEffect中发送请求,渲染数组即可

Metadata配置

静态配置:直接写死

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: '...',
  description: '...',
}
 
export default function Page() {}

动态配置:获取数据之后在配置

app/[lang]/news/[id]/page.tsx

import {getData} from '@/api/news'
export async function generateMetadata({ params }) {
  const { lang, id } = params;
  const res = await getData(id); //获取数据
  let title = lang == "zh" ? '中文标题' : '英文标题';
  let description = lang == "zh" ? '中文描述' : '英文描述';
  return {
    title,
    description,
    keywords: title
  };
}
export default function Page() {}

@相对路径的配置

tsconfig.json文件中paths对象中配置

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/api/*": ["api/*"],
      "@/components/*": ["components/*"],
      "@/lang/*": ["lang/*"],
      "@/lib/*": ["lib/*"],
      "@/styles/*": ["styles/*"],
      "@/utils/*": ["utils/*"]
    }
  }
}

sass的安装和使用

安装

npm install --save-dev sass

style.scss

.g-container{
    background-color: deeppink;
}

page.tsx

import React from 'react'
import './style.scss'
const page = () => {
  return (
    <div className='g-container'>page</div>
  )
}
export default page

环境变量配置

第一步:安装cross-env

npm install --save-dev cross-env

第二步:新建三个环境文件

.env.development

NODE_ENV = development
NEXT_PUBLIC_API = https://dev.api.com

.env.test

NODE_ENV = test
NEXT_PUBLIC_API = https://test.api.com

.env.production

NODE_ENV = production
NEXT_PUBLIC_API = https://production.api.com

第三步:修改package.json文件中的script

"scripts": {
    "dev": "cross-env NODE_ENV=development next dev -p 3000",
    "build:stage": "cross-env NODE_ENV=test next build",
    "build:prod": "cross-env NODE_ENV=production next build ",
    "start:stage": "cross-env NODE_ENV=test next start -p 3000",
    "start:prod": "cross-env NODE_ENV=production next start -p 3000"
  }

第四步:页面组件中使用

import React from 'react'
import './style.scss'
const page = () => {
  const api = process.env.NEXT_PUBLIC_API
  return (
    <div className='g-container'>BASE——API: {api}</div>
  )
}
export default page

参考:NextJS 创建项目和环境变量配置

全局状态管理

在Next.js中,跨不同组件管理状态可能是一项具有挑战性的任务。因此,像ContextApi这样的全局状态管理工具可以帮助简化该过程。核心技术点:react的createContext和useContext
参考:用ContextApi在Next.js中进行全局状态管理

国际化

利用动态路由和路由中间件来实现

中文:localhost:3000/zh/a
英文:localhost:3000/en/a

第一步:在src/app目录中新建一个[lang]文件夹,以后所有的页面都放到[lang]文件夹中

第二步:新建两个国际化文件

src/lang/zh.json

{
    "name": "姓名",
    "age": "年龄"
}

src/lang/en.json

{
    "name": "name",
    "age": "age"
}

第三步:编写一个国际化方法

src/utils/locale.js

const en = require("@/lang/en.json");
const zh = require("@/lang/zh.json");

export const getDictionaryByStr = (locale) => {
  return (key) => getDeepDict(key, locale === "zh" ? zh : en);
};

function getDeepDict(str, value) {
  let keys = str?.split(".");
  for (let key of keys) {
    if (value) {
      value = value[key];
    }
  }
  return value;
}

第四步:组件中使用

src/[lang]/a/page.tsx

import React from 'react'
import {getDictionaryByStr} from '@/utils/locale'
const page = (props: {params:{lang:string}}) => {
  const $t = getDictionaryByStr(props.params.lang)
  return (
    <div>
        lang:{props.params.lang}
        <div>name: {$t('name')}</div>
        <div>age: {$t('age')}</div>
    </div>
  )
}
export default page
------ 本文结束,感谢您的阅读 ------
本文作者: 贺刘芳
版权声明: 本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。