Next.js詳細(xì)教程

一、前言
本文基于開(kāi)源項(xiàng)目:

https://github1s.com/vercel/next.js

https://nextjs.org/

    廣東靚仔之前也寫(xiě)過(guò)Next.js的相關(guān)文章,這篇文章來(lái)一個(gè)全面的介紹,希望對(duì)沒(méi)使用過(guò)Next.js又感興趣的小伙伴有一點(diǎn)點(diǎn)幫助。
溫馨提示:看Nextjs的文檔我們最好選擇英文版本,中文文檔好像很久不更新了
二、基礎(chǔ)知識(shí)
系統(tǒng)環(huán)境需求
Node.js 12.22.0 或更高版本

MacOS、Windows (包括 WSL) 和 Linux 都被支持

安裝
yarn global add create-next-app
# or
npm i -g create-next-app


or 官方推薦

npx create-next-app@latest
# or
yarn create next-app
TypeScript 項(xiàng)目

npx create-next-app@latest --typescript
# or
yarn create next-app --typescript


三、目錄梳理
運(yùn)行Demo
我們安裝好項(xiàng)目,運(yùn)行:http://localhost:3000/

效果如下:



目錄結(jié)構(gòu)說(shuō)明































next.config.js // 是我們的配置文件,用來(lái)修改next以及webpack的配置

pages            // Next.js路由文件夾

|--index.js       // 入口文件

|--_app.js       // 用來(lái)定義一些頁(yè)面共用的

Home.module.css // 帶有.module后綴的樣式文件一般是用來(lái)做樣式隔離的

【溫馨提示】

一般抽取組件的時(shí)候,我們可以在根目錄創(chuàng)建components文件夾

(不能存儲(chǔ)在pages目錄,會(huì)導(dǎo)致路由錯(cuò)亂)

四、配置修改
便捷開(kāi)發(fā)
一般在Next項(xiàng)目中,我們會(huì)結(jié)合antd搭配開(kāi)發(fā),常見(jiàn)的兩種使用方式如下:

一、Next.js + Antd (with Less)

安裝

yarn add next-plugin-antd-less
yarn add --dev babel-plugin-import
使用

// next.config.js
const withAntdLess = require('next-plugin-antd-less');

module.exports = withAntdLess({
  // 可選
  modifyVars: { '@primary-color': '#04f' },
  // 可選
  lessVarsFilePath: './src/styles/variables.less',
  // 可選
  lessVarsFilePathAppendToEndOfContent: false,
  // 可選 https://github.com/webpack-contrib/css-loader#object
  cssLoaderOptions: {},

  // 其他配置在這里...

  webpack(config) {
    return config;
  },

  //  僅適用于 Next.js 10,如果您使用 Next.js 11,請(qǐng)刪除此塊
  future: {
    webpack5: true,
  },
});
添加一個(gè) .babelrc.js

// .babelrc.js
module.exports = {
  presets: [['next/babel']],
  plugins: [['import', { libraryName: 'antd', style: true }]],
};
詳細(xì)前往:https://www.npmjs.com/package/next-plugin-antd-less

二、安裝antd同時(shí)也開(kāi)啟css modules

安裝支持next-css、babel-plugin-import

yarn add @zeit/next-css babel-plugin-import
# or
npm install @zeit/next-css babel-plugin-import --save-dev
修改babelrc

{
    "presets": [
        "next/babel"
    ],
    "plugins": [
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory":"lib",
                "style": true
            }
        ]
    ]
}
增加next-less.config.js

const cssLoaderConfig = require('@zeit/next-css/css-loader-config')
module.exports = (nextConfig = {}) => {
  return Object.assign({}, nextConfig, {
    webpack(config, options) {
      if (!options.defaultLoaders) {
        throw new Error(
          'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade'
        )
      }
      const { dev, isServer } = options
      const {
        cssModules,
        cssLoaderOptions,
        postcssLoaderOptions,
        lessLoaderOptions = {}
      } = nextConfig
      options.defaultLoaders.less = cssLoaderConfig(config, {
        extensions: ['less'],
        cssModules,
        cssLoaderOptions,
        postcssLoaderOptions,
        dev,
        isServer,
        loaders: [
          {
            loader: 'less-loader',
            options: lessLoaderOptions
          }
        ]
      })
      config.module.rules.push({
        test: /\.less$/,
        exclude: /node_modules/,
        use: options.defaultLoaders.less
      })
      // 我們禁用了antd的cssModules
      config.module.rules.push({
        test: /\.less$/,
        include: /node_modules/,
        use: cssLoaderConfig(config, {
          extensions: ['less'],
          cssModules:false,
          cssLoaderOptions:{},
          dev,
          isServer,
          loaders: [
            {
              loader: 'less-loader',
              options: lessLoaderOptions
            }
          ]
        })
      })
      if (typeof nextConfig.webpack === 'function') {
        return nextConfig.webpack(config, options)
      }
      return config
    }
  })
}
修改next.config.js

const withLessExcludeAntd = require("./next-less.config.js")
// choose your own modifyVars
const modifyVars = require("./utils/modifyVars")
if (typeof require !== 'undefined') {
  require.extensions['.less'] = (file) => {}
}
module.exports = withLessExcludeAntd({
    cssModules: true,
    cssLoaderOptions: {
      importLoaders: 1,
      localIdentName: "[local]___[hash:base64:5]",
    },
    lessLoaderOptions: {
      javascriptEnabled: true,
      modifyVars: modifyVars
    }
詳細(xì)前往:https://www.yuque.com/steven-kkr5g/aza/ig3x9w

三、組件級(jí)css

Next.js 通過(guò) [name].module.css 文件命名約定來(lái)支持 CSS 模塊 。

五、SSG和SSR
SSG-靜態(tài)生成
最簡(jiǎn)單、性能也最優(yōu)的預(yù)渲染方式就是靜態(tài)生成(SSG),把組件渲染工作完全前移到編譯時(shí):

(編譯時(shí))獲取數(shù)據(jù)
(編譯時(shí))渲染組件,生成 HTML
   Demo:

// pages/demo.js
export default function Home(props) { ... }
 
// 獲取靜態(tài)數(shù)據(jù)
export async function getStaticProps() {
  const data = ...
  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}
getStaticProps只在服務(wù)端執(zhí)行(根本不會(huì)進(jìn)入客戶(hù)端 bundle),返回的靜態(tài)數(shù)據(jù)會(huì)傳遞給頁(yè)面組件(上例中的Home)。也就是說(shuō),要求通過(guò)getStaticProps提前備好頁(yè)面所依賴(lài)的全部數(shù)據(jù),數(shù)據(jù) ready 之后組件才開(kāi)始渲染,并生成 HTML。
Tips:  只有頁(yè)面能通過(guò)getStaticProps聲明其數(shù)據(jù)依賴(lài),普通組件不允許,所以要求將整頁(yè)依賴(lài)的所有數(shù)據(jù)都組織到一處。
SSR-服務(wù)端渲染
Next.js 提供了 SSR 專(zhuān)用的getServerSideProps(context):

// pages/demo.js
export async function getServerSideProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()
 
  if (!data) {
    return {
      notFound: true,
    }
  }
 
  return {
    props: {}, // will be passed to the page component as props
  }
}
每個(gè)請(qǐng)求過(guò)來(lái)時(shí)都執(zhí)行,所以能夠拿到請(qǐng)求上下文參數(shù)(context)
除了這個(gè)外,編碼過(guò)程跟寫(xiě)React項(xiàng)目差不多。
六、路由系統(tǒng)
    Nextjs默認(rèn)匹配pages目錄的index.js作為根路徑/,其他的路徑也是這樣按文件名匹配的。
路由跳轉(zhuǎn)
   Nextjs官方推薦了兩種跳轉(zhuǎn)方式,一種是Link組件包裹,一種使用Router。Link的原理也是用Router實(shí)現(xiàn)的,Link用起來(lái)總感覺(jué)很冗余,個(gè)人推薦使用Router。

   Nextjs提供了一個(gè)'next/router'的包,專(zhuān)門(mén)用來(lái)處理路由。Router便是其中一個(gè)對(duì)象,Router.push('url')進(jìn)行跳轉(zhuǎn)。
簡(jiǎn)單Demo:

import React from 'react'     
import Router from 'next/router'

export default () => {
 return(
    <>
      <button onClick={()=>Router.push('/demo')} >前往demo頁(yè)</button>
      <div>這里是首頁(yè)</div>
    </>
  )
}
路由傳參
  Nextjs使用query傳參數(shù)!



官方例子:

import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <button
      type="button"
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      Click here to read more
    </button>
  )
}
接收參數(shù)的時(shí)候使用props.router.query.pid
6個(gè)路由鉤子
// routeChangeStart     history模式路由改變剛開(kāi)始  
// routeChangeComplete  history模式路由改變結(jié)束
// routeChangeError     路由改變失敗
// hashChangeStart      hash模式路由改變剛開(kāi)始
// beforeHistoryChange  在routerChangeComplete之前執(zhí)行
// hashChangeComplete   hash模式路由改變結(jié)束
來(lái)個(gè)Demo看看:

import React from 'react'
import Link from 'next/link'
import Router from 'next/router'  
 
const Home = () => {
    /**6個(gè)鉤子事件
    routeChangeStart
    routerChangeComplete
    beforeHistoryChange
    routeChangeError
    hashChangeStart
    hashChangeComplete*/
    //路由開(kāi)始變化
    Router.events.on('routeChangeStart',(...args)=>{
        console.log('1.routeChangeStart->路由開(kāi)始變化,參數(shù)為:',...args)
    })
    //路由變化結(jié)束
    Router.events.on('routeChangeComplete',(...args)=>{
        console.log('2.routeChangeComplete->路由變化結(jié)束,參數(shù)為:',...args)
    })
    //Next.js全部都用History模式
    Router.events.on('beforeHistoryChange',(...args)=>{
        console.log('3.beforeHistoryChange,參數(shù)為:',...args)
    })
    //路由發(fā)生錯(cuò)誤時(shí),404不算
    Router.events.on('routeChangeError',(...args)=>{
        console.log('4.routeChangeError->路由發(fā)生錯(cuò)誤,參數(shù)為:',...args)
    })
    //Hash路由切換之前
    Router.events.on('hashChangeStart',(...args)=>{
        console.log('5.hashChangeStart,參數(shù)為:',...args)
    })
    //Hash路由切換完成
    Router.events.on('hashChangeComplete',(...args)=>{
        console.log('6.hashChangeComplete,參數(shù)為:',...args)
    })
 
    function gotoSport(){
        Router.push({
            pathname:'/sport',
            query:{name:'前端早茶'}
        })
        // 同以下:
        // Router.push('/sport?前端早茶')
    }
 
    return (
        <>
            <div>調(diào)試下6個(gè)鉤子</div>
            <div>
                <Link href={{pathname:'/sport',query:{name:'前端早茶'}}}><a>選擇前端早茶</a></Link>
                <br/>
                <Link href="/sport?name=廣東靚仔"><a>選擇廣東靚仔</a></Link>
            </div>
            <div>
                <button onClick={gotoSport}>選前端早茶</button>
            </div>
            <!-- 這里沒(méi)有設(shè)置錨點(diǎn),因此不會(huì)有跳轉(zhuǎn)效果 -->
            <div>
                <Link href='/#juan'><a>選Juan</a></Link>
            </div>
        </>
    )
}

七、狀態(tài)管理
Token存儲(chǔ)
SSR之間只能通過(guò)cookie才能在Client和Server之間通信,以往我們?cè)赟PA項(xiàng)目中是使用localStorage或者sessionStorage來(lái)存儲(chǔ),但是在SSR項(xiàng)目中Server端是拿不到的,因?yàn)樗菫g覽器的屬性,要想客戶(hù)端和服務(wù)端同時(shí)都能拿到我們可以使用Cookie,所以token信息只能存儲(chǔ)到Cookie中。
集成狀態(tài)管理器
大型項(xiàng)目推薦使用Redux,方便我們維護(hù)以及二次開(kāi)發(fā)。


四個(gè)步驟
創(chuàng)建store/axios.js文件
修改pages/_app.js文件
創(chuàng)建store/index.js文件
創(chuàng)建store/slice/auth.js文件
核心梳理:
pages/_app.js文件
使用next-redux-wrapper插件將redux store數(shù)據(jù)注入到next.js。
import {Provider} from 'react-redux'
import {store, wrapper} from '@/store'
 
const MyApp = ({Component, pageProps}) => {
  return <Component {...pageProps} />
}
 
export default wrapper.withRedux(MyApp)
store/index.js文件
使用@reduxjs/toolkit集成reducer并創(chuàng)建store,

使用next-redux-wrapper連接next.js和redux,

使用next-redux-cookie-wrapper注冊(cè)要共享到cookie的slice信息。


import {configureStore, combineReducers} from '@reduxjs/toolkit';
import {createWrapper} from 'next-redux-wrapper';
import {nextReduxCookieMiddleware, wrapMakeStore} from "next-redux-cookie-wrapper";
import {authSlice} from './slices/auth';
import logger from "redux-logger";
 
const combinedReducers = combineReducers({
  [authSlice.name]: authSlice.reducer
});
export const store = wrapMakeStore(() => configureStore({
  reducer: combinedReducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(
      nextReduxCookieMiddleware({
        // 在這里設(shè)置在客戶(hù)端和服務(wù)器端共享的cookie數(shù)據(jù)
        subtrees: ["auth.accessToken", "auth.isLogin", "auth.me"],
        })
    ).concat(logger)
}));
const makeStore = () => store;
export const wrapper = createWrapper(store, {storeKey: 'key', debug: true});
store/slice/auth.js
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import axios from '../axios';
import qs from "qs";
import {HYDRATE} from 'next-redux-wrapper';
 
// 獲取用戶(hù)信息
export const fetchUser = createAsyncThunk('auth/me', async (_, thunkAPI) => {
  try {
    const response = await axios.get('/account/me');
    return response.data.name;
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});
 
// 登錄
export const login = createAsyncThunk('auth/login', async (credentials, thunkAPI) => {
  try {
 
    // 獲取token信息
    const response = await axios.post('/auth/oauth/token', qs.stringify(credentials));
    const resdata = response.data;
    if (resdata.access_token) {
      // 獲取用戶(hù)信息
      const refetch = await axios.get('/account/me', {
        headers: {Authorization: `Bearer ${resdata.access_token}`},
      });
      
      return {
        accessToken: resdata.access_token,
        isLogin: true,
        me: {name: refetch.data.name}
      };
    } else {
      return thunkAPI.rejectWithValue({errorMsg: response.data.message});
    }
 
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});
 
// 初始化數(shù)據(jù)
const internalInitialState = {
  accessToken: null,
  me: null,
  errorMsg: null,
  isLogin: false
};
 
// reducer
export const authSlice = createSlice({
  name: 'auth',
  initialState: internalInitialState,
  reducers: {
    updateAuth(state, action) {
      state.accessToken = action.payload.accessToken;
      state.me = action.payload.me;
    },
    reset: () => internalInitialState,
  },
  extraReducers: {
    // 水合,拿到服務(wù)器端的reducer注入到客戶(hù)端的reducer,達(dá)到數(shù)據(jù)統(tǒng)一的目的
    [HYDRATE]: (state, action) => {
      console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },
    [login.fulfilled]: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.isLogin = action.payload.isLogin;
      state.me = action.payload.me;
    },
    [login.rejected]: (state, action) => {
      console.log('action=>', action)
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.payload.errorMsg});
      console.log('state=>', state)
      // throw new Error(action.error.message);
    },
    [fetchUser.rejected]: (state, action) => {
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.errorMsg});
    },
    [fetchUser.fulfilled]: (state, action) => {
      state.me = action.payload;
    }
  }
});
 
export const {updateAuth, reset} = authSlice.actions;

Tips:
1、使用了next-redux-wrapper一定要加HYDRATE,目的是同步服務(wù)端和客戶(hù)端reducer數(shù)據(jù),否則兩個(gè)端數(shù)據(jù)不一致造成沖突
2、注意next-redux-wrapper和next-redux-cookie-wrapper版本
八、舊項(xiàng)目升級(jí)Next12
溫馨提示:看Nextjs的文檔我們最好選擇英文版本,中文文檔好像很久不更新了
React Server Components
允許我們?cè)诜?wù)器上渲染所有內(nèi)容,包括組件本身。
開(kāi)啟配置:
// next.config.js
module.exports = {
  experimental: {
    concurrentFeatures: true,
    serverComponents: true
  }
}
現(xiàn)在我們可以在組件級(jí)別進(jìn)行數(shù)據(jù)獲取,通過(guò)使用 React Server 組件,我們可以簡(jiǎn)化事情。不再需要getServerSideProps或getStaticProps。

我們可以將任何 Next.js 頁(yè)面重命名為.server.js以創(chuàng)建服務(wù)器組件并直接在我們的服務(wù)器組件中導(dǎo)入客戶(hù)端組件。

【溫馨提示】廣東靚仔從官網(wǎng)截了個(gè)圖:


我們需要安裝React18才能使用哦~
React 18添加了新功能,包括 Suspense、自動(dòng)批處理更新、API 等startTransition,以及支持React.lazy.

【廣東靚仔試用了下,確實(shí)方便,不建議在生產(chǎn)項(xiàng)目上使用】
詳細(xì)內(nèi)容
官方出了一個(gè) demo :https://github1s.com/vercel/next-rsc-demo/blob/HEAD/pages/ssr.js

demo在線預(yù)覽地址:https://next-news-rsc.vercel.sh/

目錄如下所示:



以往的SSR:
import Page from '../components/page.client'
import Story from '../components/story.client'
import Footer from '../components/footer.client'

// Utils
import fetchData from '../lib/fetch-data'
import { transform } from '../lib/get-item'

export async function getServerSideProps() {
  const storyIds = await fetchData('topstories', 500)
  const data = await Promise.all(
    storyIds
      .slice(0, 30)
      .map((id) => fetchData(`item/${id}`).then(transform))
  )

  return {
    props: {
      data,
    },
  }
}

export default function News({ data }) {
  return (
    <Page>
      {data.map((item, i) => {
        return <Story key={i} {...item} />
      })}
      <Footer />
    </Page>
  )
}

頁(yè)面添加 getServerSideProps 函數(shù)用于 服務(wù)端獲取數(shù)據(jù),每個(gè)頁(yè)面都需要這樣編寫(xiě)。

更新后rsc.server.js :
import { Suspense } from 'react'

// Shared Components
import Spinner from '../components/spinner'

// Server Components
import SystemInfo from '../components/server-info.server'

// Client Components
import Page from '../components/page.client'
import Story from '../components/story.client'
import Footer from '../components/footer.client'

// Utils
import fetchData from '../lib/fetch-data'
import { transform } from '../lib/get-item'
import useData from '../lib/use-data'

function StoryWithData({ id }) {
  const data = useData(`s-${id}`, () => fetchData(`item/${id}`).then(transform))
  return <Story {...data} />
}

function NewsWithData() {
  const storyIds = useData('top', () => fetchData('topstories'))
  return (
    <>
      {storyIds.slice(0, 30).map((id) => {
        return (
          <Suspense fallback={<Spinner />} key={id}>
            <StoryWithData id={id} />
          </Suspense>
        )
      })}
    </>
  )
}

export default function News() {
  return (
    <Page>
      <Suspense fallback={<Spinner />}>
        <NewsWithData />
      </Suspense>
      <Footer />
      <SystemInfo />
    </Page>
  )
}
可以看到,我們還是按平時(shí)React項(xiàng)目來(lái)開(kāi)發(fā)就可以實(shí)現(xiàn)SSR了。
最重要的一點(diǎn),支持 HTTP Streaming,文檔還沒(méi)加載完,頁(yè)面已經(jīng)開(kāi)始渲染了。
詳情前往:https://nextjs.org/blog/next-12
九、總結(jié)
在我們閱讀完官方文檔后,我們一定會(huì)進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。
    這里廣東靚仔給下一些小建議:
在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計(jì)理念、源碼分層設(shè)計(jì)
閱讀下框架官方開(kāi)發(fā)人員寫(xiě)的相關(guān)文章
借助框架的調(diào)用棧來(lái)進(jìn)行源碼的閱讀,通過(guò)這個(gè)執(zhí)行流程,我們就完整的對(duì)源碼進(jìn)行了一個(gè)初步的了解
接下來(lái)再對(duì)源碼執(zhí)行過(guò)程中涉及的所有函數(shù)邏輯梳理一遍

作者:廣東靚仔


歡迎關(guān)注:前端早茶