简介
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
目录说明
- 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
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路径。
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
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>
)
}
中间件
中间件允许在请求完成之前运行代码。然后,根据传入的请求,可以通过重写、重定向、修改请求或响应头或直接响应来修改响应。 (类似拦截器)
新建文件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组件可配置的属性如下:
Prop | Example | Type | Status |
---|---|---|---|
src | src="/profile.png" | String | Required |
width | width={500} | Integer (px) | Required |
height | height={500} | Integer (px) | Required |
alt | alt="Picture of the author" | String | Required |
loader | loader={imageLoader} | Function | - |
fill | fill={true} | Boolean | - |
sizes | sizes="(max-width: 768px) 100vw, 33vw" | String | - |
quality | quality={80} | Integer (1-100) | - |
priority | priority={true} | Boolean | - |
placeholder | placeholder="blur" | String | - |
style | style={{objectFit: "contain"}} | Object | - |
onLoadingComplete | onLoadingComplete={img => done())} | Function | Deprecated |
onLoad | onLoad={event => done())} | Function | - |
onError | onError(event => fail()} | Function | - |
loading | loading="lazy" | String | - |
blurDataURL | blurDataURL="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
全局状态管理
在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