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)注:前端早茶