六、路由系统
Nextjs默认匹配pages目录的index.js作为根路径/,其他的路径也是这样按文件名匹配的。
路由跳转
Nextjs官方推荐了两种跳转方式,一种是Link组件包裹,一种使用Router。Link的原理也是用Router实现的,Link用起来总感觉很冗余,个人推荐使用Router。
Nextjs提供了一个'next/router'的包,专门用来处理路由。Router便是其中一个对象,Router.push('url')进行跳转。
简单Demo:
import React from 'react' import Router from 'next/router' export default () => { return( <> <button onClick={()=>Router.push('/demo')} >前往demo页</button> <div>这里是首页</div> </> ) }
路由传参
Nextjs使用query传参数!
官方例子:
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> ) }
接收参数的时候使用props.router.query.pid
6个路由钩子
// routeChangeStart history模式路由改变刚开始 // routeChangeComplete history模式路由改变结束 // routeChangeError 路由改变失败 // hashChangeStart hash模式路由改变刚开始 // beforeHistoryChange 在routerChangeComplete之前执行 // hashChangeComplete hash模式路由改变结束
来个Demo看看:
import React from 'react' import Link from 'next/link' import Router from 'next/router' const Home = () => { /**6个钩子事件 routeChangeStart routerChangeComplete beforeHistoryChange routeChangeError hashChangeStart hashChangeComplete*/ //路由开始变化 Router.events.on('routeChangeStart',(...args)=>{ console.log('1.routeChangeStart->路由开始变化,参数为:',...args) }) //路由变化结束 Router.events.on('routeChangeComplete',(...args)=>{ console.log('2.routeChangeComplete->路由变化结束,参数为:',...args) }) //Next.js全部都用History模式 Router.events.on('beforeHistoryChange',(...args)=>{ console.log('3.beforeHistoryChange,参数为:',...args) }) //路由发生错误时,404不算 Router.events.on('routeChangeError',(...args)=>{ console.log('4.routeChangeError->路由发生错误,参数为:',...args) }) //Hash路由切换之前 Router.events.on('hashChangeStart',(...args)=>{ console.log('5.hashChangeStart,参数为:',...args) }) //Hash路由切换完成 Router.events.on('hashChangeComplete',(...args)=>{ console.log('6.hashChangeComplete,参数为:',...args) }) function gotoSport(){ Router.push({ pathname:'/sport', query:{name:'前端早茶'} }) // 同以下: // Router.push('/sport?前端早茶') } return ( <> <div>调试下6个钩子</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> <!-- 这里没有设置锚点,因此不会有跳转效果 --> <div> <Link href='/#juan'><a>选Juan</a></Link> </div> </> ) }
七、状态管理
Token存储
SSR之间只能通过cookie才能在Client和Server之间通信,以往我们在SPA项目中是使用localStorage或者sessionStorage来存储,但是在SSR项目中Server端是拿不到的,因为它是浏览器的属性,要想客户端和服务端同时都能拿到我们可以使用Cookie,所以token信息只能存储到Cookie中。
集成状态管理器
大型项目推荐使用Redux,方便我们维护以及二次开发。
四个步骤
- 创建store/axios.js文件
- 修改pages/_app.js文件
- 创建store/index.js文件
- 创建store/slice/auth.js文件
核心梳理:pages/_app.js文件使用next-redux-wrapper插件将redux store数据注入到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并创建store,
使用next-redux-wrapper连接next.js和redux,
使用next-redux-cookie-wrapper注册要共享到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({ // 在这里设置在客户端和服务器端共享的cookie数据 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'; // 获取用户信息 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) { // 获取用户信息 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}); } }); // 初始化数据 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: { // 水合,拿到服务器端的reducer注入到客户端的reducer,达到数据统一的目的 [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,目的是同步服务端和客户端reducer数据,否则两个端数据不一致造成冲突
2、注意next-redux-wrapper和next-redux-cookie-wrapper版本
八、旧项目升级Next12
温馨提示:看Nextjs的文档我们最好选择英文版本,中文文档好像很久不更新了
React Server Components
允许我们在服务器上渲染所有内容,包括组件本身。
开启配置:
// next.config.js module.exports = { experimental: { concurrentFeatures: true, serverComponents: true } }
现在我们可以在组件级别进行数据获取,通过使用 React Server 组件,我们可以简化事情。不再需要getServerSideProps
或getStaticProps
。
我们可以将任何 Next.js 页面重命名为.server.js
以创建服务器组件并直接在我们的服务器组件中导入客户端组件。
【温馨提示】广东靓仔从官网截了个图:
我们需要安装React18才能使用哦~
React 18添加了新功能,包括 Suspense、自动批处理更新、API 等startTransition
,以及支持React.lazy
.
【广东靓仔试用了下,确实方便,不建议在生产项目上使用】
详细内容
官方出了一个 demo :https://github1s.com/vercel/next-rsc-demo/blob/HEAD/pages/ssr.js
demo在线预览地址: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> ) }
页面添加 getServerSideProps 函数用于 服务端获取数据,每个页面都需要这样编写。
更新后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> ) }
可以看到,我们还是按平时React项目来开发就可以实现SSR了。
最重要的一点,支持 HTTP Streaming,文档还没加载完,页面已经开始渲染了。
详情前往:https://nextjs.org/blog/next-12
九、总结
在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。
这里广东靓仔给下一些小建议:
- 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
- 阅读下框架官方开发人员写的相关文章
- 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
- 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍