手写Next.js
next基础
简介和目录结构
app.jsx
配置
.env
next.config.js /mjs
样式的编写和静态资源导入
路面路径映射
编程导航
next支持编程导航,但是不利于SEO
一般在App内全局监听
import { useEffect } from "react";
const router = useRouter();
useEffect(() => {
const handler = (url:any) => {
//url 当前访问的路径
console.log(url);
};
router.events.on("routeChangeStart",handler);
return () => {
router.events.off("routeChangeStart",handler);
};
}, [router]);
动态路由
获取值
const router=useRouter()
//这种情况最好像nuxt那样路由拦截一下
//都使用query,如果重名,动态路由优先,查询字符串对应的字段会丢失
const {id} = router.query
404 Page
跟nuxt的错误页面相同,文件名为[...slug].tsx,这里的slug可以是任意的名称
const Error : FC<IProps>=function(props){
const router=useRouter()
const { slug} = router.query
return (<div>
{JSON.stringify(slug)}
</div>)
}
next核心
中间件
根目录创建middleware.ts
import { NextResponse, type NextRequest } from "next/server";
//这个中间件只在服务端运行
export function middleware(req:NextRequest){
console.log(req.url); //路径
console.log(req.nextUrl); //源,协议,路径
// return NextResponse.next() //跟不反悔效果一样
// const cookie=req.cookies.get("token")
// if(!cookie && req.nextUrl.pathname!="login"){
// return NextResponse.redirect(new URL("/login",req.nextUrl.origin))
// }
if(req.nextUrl.pathname.startsWith("/bane/ds")){
}
}
//过滤想要得到的请求,支持正则表达式
export const config={
matcher:["/(?!_next/static|api|favicon.ico.*)"]
}
layout
简单实现layout组件
import { memo,ReactElement} from "react"
import type {FC} from "react"
import MianHeader from "../mian-header"
import MainFooter from "../main-footer"
export interface IProps{
children?:ReactElement
}
const Layout : FC<IProps>=function(props){
const { children } = props
return (<div>
<MianHeader></MianHeader>
{children} 这个是页面内容
<MainFooter></MainFooter>
</div>)
}
export default memo(Layout)
Layout.displayName="Layout"
在App内包裹呈现的组件即可
export default function App({ Component, pageProps }: layoutAppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
某些页面没layout
我们我可以通过Component的displayName,每个组件都可以挂一个这个属性,我们根据名字来判断展示效果
export default function App({ Component, pageProps }: layoutAppProps) {
if(Component.displayName == "User"){
return <Component {...pageProps} />
} else{
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
}
抽取
但是如果我们定制度较高,主页面就太多的判断的
这里我们扩展了组件类型,增加一个调用前面,这个方法返回一个被layout包裹的组件
import Layout from "@/components/layout"
import { memo,ReactElement} from "react"
import type {FC} from "react"
export interface IProps{
children?:ReactElement
}
export interface ComLaout{
getlayout: (el:ReactElement)=>ReactElement
}
const User : FC<IProps> & ComLaout=function(props){
return (<>
this is a user
</> )
}
User.getlayout=(page:ReactElement)=>{
return (<Layout>{page}</Layout>)
}
export default User
User.displayName="User"
App.txs, 这里同样扩展了类型
import Layout from "@/components/layout";
import "@/styles/globals.css";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import Link from "next/link";
import { ReactElement } from "react";
type layoutcomponent= NextPage & {
getlayout?:(e:ReactElement) =>ReactElement
}
type layoutAppProps = AppProps & {
Component:layoutcomponent
}
export default function App({ Component, pageProps }: layoutAppProps) {
let commpone = Component.getlayout ? Component.getlayout : ( e:ReactElement)=>({e})
return commpone(<Component {...pageProps} />)
}
嵌套路由
伪造嵌套路由
在page中实现嵌套路由,只能找一个公共的layout,在layout内实现跳转的逻辑
嵌套路由
在新版中其实也是通过同一个layout来实现二级路由,每一个路径都可以有一个layout组件,子包下会复用父级的layout从而达成嵌套路由,但是现在还有很多问题
生命周期
Class Components,函数式组件只会调用方法本身
axios
图和代码有一点不同,一个是返回结果直接拿data,一个是拿到全部结果
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
const BASE_URL=""
class niurequest{
instance: AxiosInstance
constructor(config : AxiosRequestConfig){
this.instance=axios.create(config)
}
request<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return new Promise((resolve,reject)=>{
this.instance.request<T>(config).then(res=>resolve(res)).catch(err=>reject(err))
})
}
get<T>(config:AxiosRequestConfig):Promise<T>{
return this.request<T>({...config,method:"GET"})
}
post<T>(config:AxiosRequestConfig):Promise<T>{
return this.request<T>({...config,method:"POST"})
}
}
export default new niurequest({
baseURL:BASE_URL,
timeout:3000
})
后端接口
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
console.log(req.body);
console.log(req.query);
console.log(req.method)
res.status(200).send({ name: "John Doe" });
// res.status(405).end()
}
预渲染
不依赖外部文件的页面, 正常打包就是SSG,那如果我们的页面生成依赖数据,甚至依赖动态路由的数据
import { memo,ReactElement} from "react"
import type {FC} from "react"
export interface IProps{
children?:ReactElement,
books?:string[]
}
const PageSsg : FC<IProps>=function(props){
const {books}= props
return (<div>
{ books?.map(item=>{
return <div key={item}>{item}</div>
}) }
</div>)
}
export default memo(PageSsg)
PageSsg.displayName="PageSsg"
export const getStaticProps=(context:any)=>{
//发送网络请求拿到数据
const data=["da","bb","vv","ee"]
return{
props:{
books:data
}
}
}
有时候我们需要根据动态路由的参数再去请求对应的页面数据,我们需要枚举生成有可能访问的对应页面
SSR
每次请求都会重新请求
增量静态再生(ISR)
客户端渲染
也就是在 useEffect中获取数据渲染页面
集成Redux
项目集成
公共样式导入
_app.tsx
import Layout from "@/components/layout";
import "@/styles/globals.scss";
import type { AppProps } from "next/app";
import warpper from "@/store/index"
import { Provider } from "react-redux"
import "normalize.css"
export default function App({ Component, ...rest }: AppProps) {
//redux接入App
const {store,props} =warpper.useWrappedStore(rest)
return <Provider store={store}>
<Layout><Component {...props.pageProps} /></Layout>
</Provider> ;
}
axios
可能会出现域名无法访问,根据控制台提示来配置即可
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
const BASE_URL="http://codercba.com:9060/music-next/api"
export interface resultType<T>{
code:number,
data:T
}
class niurequest{
instance: AxiosInstance
constructor(config : AxiosRequestConfig){
this.instance=axios.create(config)
}
request<T=any>(config:AxiosRequestConfig):Promise<T>{
return new Promise((resolve,reject)=>{
this.instance.request<T>(config).then(res=>resolve(res.data)).catch(err=>reject(err))
})
}
get<T>(config:AxiosRequestConfig){
return this.request<T>({...config,method:"GET"})
}
post<T>(config:AxiosRequestConfig){
return this.request<T>({...config,method:"POST"})
}
}
export default new niurequest({
baseURL:BASE_URL,
timeout:3000
})
请求示例
export const getsearch=()=>{
return niurequest.get<resultType<searchSuggestResult>>({url:"/searchsuggest/get"})
}
export const gethomeinfo=()=>{
return niurequest.get<resultType<homeinfo>>({url:"/home/info"})
}
redux
需要安装next-redux-wrapper还有@redux/toolkit react-redux
改造store
import {configureStore} from "@reduxjs/toolkit"
import {createWrapper} from "next-redux-wrapper"
import home from "./moudule/home"
const store= configureStore({
reducer:{
home
}
})
const markstore=()=>store
export default createWrapper(markstore)
export type AppDispatch =typeof store.dispatch
export type rootState =ReturnType<typeof store.getState>
改造Slice
import { gethomeinfo, getsearch, homeinfo, searchSuggestResult } from "@/service/module/home"
import { createAsyncThunk, createSlice} from "@reduxjs/toolkit"
import { HYDRATE } from "next-redux-wrapper"
export const getSearchSugget=createAsyncThunk("getSearchSugget",async()=>{
const res=await getsearch()
return res.data
})
export const getHomeinfo=createAsyncThunk("gethomeinfo",async()=>{
const res=await gethomeinfo()
console.log(res.data);
return res.data
})
interface homestate{
name:string,
navbar: searchSuggestResult,
homeinfo:homeinfo
}
const homeSlice=createSlice({
name:"home",
initialState:{
name:"niuniu",
navbar:{},
homeinfo:{}
} as homestate ,
reducers:{
unpdatename(store,{payload}){
store.name=payload
}
},
extraReducers:(builder)=>{
builder.addCase(HYDRATE,(state,action:any)=>{
//state->initialState
//payload.home=>拿到根的一个模块,也就是当前模块
//目的是保证客户端和服务器端数据的一致性
return {
...state,
...action.payload.home
}
}).addCase(getSearchSugget.fulfilled,(state,{payload})=>{
state.navbar=payload
}).addCase(getHomeinfo.fulfilled,(state,{payload})=>{
state.homeinfo=payload
})
}
})
export default homeSlice.reducer
export const { unpdatename } = homeSlice.actions
改造_app.tsx
import Layout from "@/components/layout";
import "@/styles/globals.scss";
import type { AppProps } from "next/app";
import warpper from "@/store/index"
import { Provider } from "react-redux"
import "normalize.css"
export default function App({ Component, ...rest }: AppProps) {
//redux接入App
const {store,props} =warpper.useWrappedStore(rest)
return <Provider store={store}>
<Layout><Component {...props.pageProps} /></Layout>
</Provider> ;
}
我们采用SSR的渲染方式,页面的获取数据也要更改
import type { AppDispatch, rootState } from "@/store";
import { getSearchSugget, unpdatename } from "@/store/moudule/home";
import { GetServerSideProps } from "next";
import { Inter } from "next/font/google";
import { useSelector,useDispatch} from "react-redux"
import wapper from "@/store/index"
import { Button } from 'antd';
import { gethomeinfo, IBanner, ICategory } from "@/service/module/home";
import type { FC } from "react";
import Banners from "@/components/banners";
const inter = Inter({ subsets: ["latin"] });
interface IProps{
banners: IBanner[],
categorys: ICategory,
recommends: any,
disitalData:any
}
const Home :FC<IProps>=(props)=> {
const {banners} =props
const dispatch=useDispatch()
const {name,navbar} = useSelector((store:rootState)=>{
return{ name:store.home.name,navbar:store.home.navbar}
})
return (
<>
<div>
<Banners banners={banners}/>
</div>
</>
);
}
export default Home
export const getServerSideProps:GetServerSideProps=wapper.getServerSideProps((store)=>{
return async()=>{
await store.dispatch( getSearchSugget())
const res =await gethomeinfo()
console.log(res,"ddd");
return {
props:{
banners:res.data.banners || [],
categorys:res.data.categorys || [],
recommends:res.data.recommends ||[],
disitalData:res.data.digitalData || ""
}
}
}
})
sass
npm i sass
ant-design
npm i antd --save
npm @types/antd --save-dev
随后按照官网覆盖样式