本文目录
  1. 简介
  2. 安装
  3. 目录说明
  4. 路由
    1. 定义路由
    2. layout布局
    3. Link导航组件
    4. usePathname
    5. useRouter
    6. 路由组
    7. 动态路由
    8. loading组件
    9. error组件
    10. 404组件
    11. 并行路由
    12. 中间件
  5. 组件
    1. 服务端组件
    2. 客户端组件
    3. 内置组件
      1. Image组件
  6. 数据获取
  7. Metadata配置
  8. @相对路径的配置
  9. sass的安装和使用
  10. 环境变量配置
  11. 全局状态管理
  12. 国际化

分类: 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 中国大陆许可协议进行许可。