next-redux-wrapper使用細(xì)節(jié)及源碼分析
引言
前幾天寫了一篇文章講解了 wrapper
概念,本篇文章主要從源碼分析它的實(shí)現(xiàn)過程,去更好的理解服務(wù)端使用 redux 的注意事項。
- 分析版本:8.1.0
- 倉庫地址:github.com/kirill-kons…
先附上一份主要代碼結(jié)構(gòu)圖:
目錄結(jié)構(gòu)
. ├── lerna.json ├── package.json ├── packages │ ├── configs │ ├── demo │ ├── demo-page │ ├── demo-redux-toolkit │ ├── demo-saga │ ├── demo-saga-page │ └── wrapper └── yarn.lock
可以明顯的看出來這是一個用 lerna
管理的 monorepo
倉庫
demo-*
開頭的是案例庫,而configs
庫也是為案例庫服務(wù)的,那么只用管 wrapper
庫即可。
wrapper
結(jié)構(gòu):
packages/wrapper ├── jest.config.js jest 配置 ├── next-env.d.ts ├── package.json ├── src │ └── index.tsx 核心實(shí)現(xiàn)代碼 ├── tests 測試代碼 │ ├── client.spec.tsx │ ├── server.spec.tsx │ └── testlib.tsx ├── tsconfig.es6.json └── tsconfig.json
核心代碼都在 packages/wrapper/src/index.tsx
文件,下面開始分析。
代碼結(jié)構(gòu)
把大部分代碼的具體實(shí)現(xiàn)去掉,然后留下整體代碼的頂層聲明:
// 用于客戶端 hydrate 的時發(fā)送初始化指令用的 action key export const HYDRATE = '__NEXT_REDUX_WRAPPER_HYDRATE__'; // 用于判斷是否是服務(wù)器 const getIsServer = () => typeof window === 'undefined'; // 用于反序列化 State,類似解密函數(shù) const getDeserializedState = <S extends Store>(initialState: any, {deserializeState}: Config<S> = {}) => deserializeState ? deserializeState(initialState) : initialState; // 用于序列化 State,類似加密函數(shù) const getSerializedState = <S extends Store>(state: any, {serializeState}: Config<S> = {}) => serializeState ? serializeState(state) : state; // 客戶端存儲 store 用的變量 let sharedClientStore: any; // 初始化 redux store const initStore = <S extends Store>({makeStore, context = {}}: InitStoreOptions<S>): S => { // ... }; // 創(chuàng)建 wrapper export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => { // ... return { getServerSideProps, getStaticProps, getInitialAppProps, getInitialPageProps, // 以前的函數(shù),忽略掉,不建議使用了 withRedux, useWrappedStore, }; }; // 以前的函數(shù),忽略掉,不建議使用了 export default <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => { // ... return createWrapper(makeStore, config).withRedux; };
代碼分析:
HYDRATE
,常量,用于客戶端react
執(zhí)行hydrate
的時發(fā)送初始化指令用的 action key。getIsServer
用于判斷是否是服務(wù)器.getDeserializedState
用于反序列化State
,類似解密函數(shù),依賴外部使用方傳入。getSerializedState
用于序列化State
,類似加密函數(shù),依賴外部使用方傳入。sharedClientStore
變量,用于客戶端存儲redux store
用的,用在initStore
中。initStore
初始化redux store
,主要是處理 服務(wù)端 和 客戶端 在創(chuàng)建時緩存的位置問題。createWrapper
創(chuàng)建 wrapper 核心default
默認(rèn)導(dǎo)出函數(shù),以前的函數(shù),忽略掉,不建議使用了,主要使用createWrapper
函數(shù)返回的withRedux
函數(shù),因此本篇文章不進(jìn)行講解withRedux
函數(shù)。
一共就 8
個聲明,其中只暴露了 HYDRATE
、 createWrapper
和默認(rèn)導(dǎo)出函數(shù),忽略掉默認(rèn)導(dǎo)出函數(shù)。
下面進(jìn)行對 createWrapper
函數(shù)的分析。
核心實(shí)現(xiàn)
createWrapper 整體實(shí)現(xiàn)概覽
// 創(chuàng)建 wrapper export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => { // 幾個 wrapper 函數(shù)的實(shí)際核心實(shí)現(xiàn)函數(shù) const makeProps = async (): Promise<WrapperProps> => {/** ... */}; // 頁面級 getInitialProps 函數(shù)的包裹函數(shù) const getInitialPageProps = (callback) => async (context) => {/** ... */}; // 應(yīng)用級 getInitialProps 函數(shù)的包裹函數(shù) const getInitialAppProps = (callback) => async (context) => {/** ... */}; // getStaticProps 函數(shù)的包裹函數(shù) const getStaticProps = (callback) => async (context) => {/** ... */}; // getServerSideProps 函數(shù)的包裹函數(shù) const getServerSideProps = (callback) => async (context) => await getStaticProps(callback)(context); // hydrate 處理函數(shù),調(diào)用 store.dispatch 發(fā)起初始化 const hydrate = (store: S, state: any) => { if (!state) { return; } store.dispatch({ type: HYDRATE, payload: getDeserializedState<S>(state, config), } as any); }; // hydrate 處理中間數(shù)據(jù)的函數(shù)(用于分析處理各種情況) const hydrateOrchestrator = (store: S, giapState: any, gspState: any, gsspState: any, gippState: any) => { //... }; // redux hydrate 執(zhí)行 hook const useHybridHydrate = (store: S, giapState: any, gspState: any, gsspState: any, gippState: any) => { // ... }; // 用于在應(yīng)用中關(guān)聯(lián) store 的 hook。一般用在 App 組件 // giapState stands for getInitialAppProps state const useWrappedStore = <P extends AppProps>(incomingProps: P, displayName = 'useWrappedStore'): {store: S; props: P} => { // ... return {store, props: {...initialProps, ...resultProps}}; }; return { getServerSideProps, getStaticProps, getInitialAppProps, getInitialPageProps, useWrappedStore, }; };
可以把 createWrapper
函數(shù)的內(nèi)容分兩類:
- Next.js 數(shù)據(jù)獲取函數(shù)的包裹函數(shù)實(shí)現(xiàn)。
- Next.js 渲染頁面內(nèi)容 時的處理實(shí)現(xiàn)。
從 數(shù)據(jù)獲取 到 渲染頁面內(nèi)容 本身就有一個前后順序,這里也按照這個順序來分析。
createWrapper 中 數(shù)據(jù)獲取函數(shù) 的包裹函數(shù)實(shí)現(xiàn)分析
callback
函數(shù)說明,就是我們調(diào)用 wrapper.[wrapperFunctionName]
函數(shù)時傳遞的參數(shù)。
比如:
App.getInitialProps = wrapper.getInitialAppProps(({ store }) => async (ctx) => { // ... });
callback
就是 ({ store }) => async (ctx) => {}
這部分代碼.
我們來看幾個 數(shù)據(jù)獲取函數(shù) 的代碼:
// 頁面級 getInitialProps 函數(shù)的包裹函數(shù) const getInitialPageProps = <P extends {} = any>(callback: PageCallback<S, P>): GetInitialPageProps<P> => async ( context: NextPageContext | any, // legacy ) => { // context 中會存儲 store — 避免雙重包裝,因?yàn)橛锌赡苌蠈咏M件也調(diào)用了 `getInitialPageProps` 或者 `getInitialAppProps` 函數(shù)。 if ('getState' in context) { // 有緩存的 store ,直接給 callback 傳入的 store return callback && callback(context as any); } // 調(diào)用 makeProps 執(zhí)行開發(fā)者傳入要執(zhí)行的函數(shù) return await makeProps({callback, context, addStoreToContext: true}); }; // 應(yīng)用級 getInitialProps 函數(shù)的包裹函數(shù) const getInitialAppProps = <P extends {} = any>(callback: AppCallback<S, P>): GetInitialAppProps<P> => async (context: AppContext) => { // 調(diào)用 makeProps 執(zhí)行開發(fā)者傳入要執(zhí)行的函數(shù) const {initialProps, initialState} = await makeProps({callback, context, addStoreToContext: true}); // 每個 `數(shù)據(jù)獲取函數(shù)` 最后需要的返回函數(shù)類型不一樣,因此需要處理成 Next.js 需要的類型 return { ...initialProps, initialState, }; }; // getStaticProps 函數(shù)的包裹函數(shù) const getStaticProps = <P extends {} = any>(callback: GetStaticPropsCallback<S, P>): GetStaticProps<P> => async context => { // 調(diào)用 makeProps 函數(shù)時,相比 getInitialPageProps 和 getInitialAppProps 缺少了 addStoreToContext 參數(shù) const {initialProps, initialState} = await makeProps({callback, context}); return { ...initialProps, props: { ...initialProps.props, initialState, }, } as any; }; // getServerSideProps 函數(shù)的包裹函數(shù) const getServerSideProps = <P extends {} = any>(callback: GetServerSidePropsCallback<S, P>): GetServerSideProps<P> => async context => await getStaticProps(callback as any)(context); // 返回參數(shù)和static一樣,因此直接調(diào)用 getStaticProps 函數(shù)即可
我們可以看出幾個函數(shù)的主要區(qū)別點(diǎn)在于 參數(shù)類型(也就是context 類型) 和 返回值類型 不一致,這也是 Next.js 不同數(shù)據(jù)獲取函數(shù) 的區(qū)別之一。
getInitialPageProps
需要注意有可能上層組件也調(diào)用了 getInitialPageProps
或者 getInitialAppProps
函數(shù)。 因此需要判斷是否把 store
存儲到了 context
中,是的話,需要直接從 context
中獲取。(注釋是這樣的,這里有點(diǎn)問題,待驗(yàn)證)
getInitialAppProps
和 getInitialPageProps
實(shí)現(xiàn)類似,只是只需要執(zhí)行一次,不用考慮緩存,還有就是 返回值類型
不一樣
getStaticProps
調(diào)用 makeProps
函數(shù)時,相比 getInitialPageProps
和 getInitialAppProps
缺少了 addStoreToContext
參數(shù),返回值也不一樣。
getServerSideProps
返回參數(shù)和 getStaticProps
一樣,因此直接調(diào)用 getStaticProps
函數(shù)即可。
接下來就是幾個數(shù)據(jù)獲取的 wrapper 函數(shù) 的實(shí)際核心實(shí)現(xiàn),也就是 makeProps
函數(shù):
// 數(shù)據(jù)獲取的 `wrapper 函數(shù)` 的實(shí)際核心 const makeProps = async ({ callback, context, addStoreToContext = false, }: { callback: Callback<S, any>; context: any; addStoreToContext?: boolean; }): Promise<WrapperProps> => { // 服務(wù)端初始化 store const store = initStore({context, makeStore}); if (config.debug) { console.log(`1. getProps created store with state`, store.getState()); } // Legacy stuff - 把 store 放入 context 中 if (addStoreToContext) { if (context.ctx) { context.ctx.store = store; } else { context.store = store; } } // 這里實(shí)現(xiàn)了 callback 雙層函數(shù)執(zhí)行,先傳遞 store 給 callback 生成新的函數(shù) const nextCallback = callback && callback(store); // 再用新的函數(shù)傳遞 context 參數(shù),獲取 initialProps const initialProps = (nextCallback && (await nextCallback(context))) || {}; if (config.debug) { console.log(`3. getProps after dispatches has store state`, store.getState()); } const state = store.getState(); return { initialProps, // 在服務(wù)端可以對 state 數(shù)據(jù)加密,防止被輕易解析 initialState: getIsServer() ? getSerializedState<S>(state, config) : state, }; };
makeProps
函數(shù)主要初始化 store
,然后把 store
傳遞給 callback
函數(shù)并執(zhí)行獲取到一個函數(shù),這個函數(shù)傳入 context
再次執(zhí)行,就可以獲取到對應(yīng) 數(shù)據(jù)獲取函數(shù)
的返回值,處理后進(jìn)行返回即可。
其中需要注意下面幾點(diǎn):
callback
函數(shù)必須返回一個函數(shù),否則其返回的initialProps
會被是空對象- 調(diào)用
createWrapper
時傳遞參數(shù)中的debug
參數(shù)在這里會起作用 - 把
store
放入context
是一個遺留的處理方式,后續(xù)可能會去掉。但并沒說明后續(xù)處理方式。 - 返回的
initialState
在服務(wù)端可能自定義加密方法。
createWrapper 中 渲染頁面內(nèi)容 時的處理實(shí)現(xiàn)分析
服務(wù)端把數(shù)據(jù)獲取后,下面就要進(jìn)行的步驟是:渲染頁面內(nèi)容。
有幾個概念需要先理清楚,不然會直接看前面幾個函數(shù)的實(shí)現(xiàn)會很懵逼。
giapState
:對應(yīng)getInitialAppProps
處理后的state
gspState
:對應(yīng)getStaticProps
處理后的state
gsspState
:對應(yīng)getServerSideProps
處理后的state
gippState
:對應(yīng)getInitialPageProps
處理后的state
useWrappedStore hook
// 用于在應(yīng)用中關(guān)聯(lián) store 的hook。 const useWrappedStore = <P extends AppProps>(incomingProps: P, displayName = 'useWrappedStore'): {store: S; props: P} => { // createWrapper 給 incomingProps 添加了 WrapperProps 類型,因?yàn)樗鼈儾皇菑?P 類型獲取,因此在這里進(jìn)行強(qiáng)制賦予類型 const {initialState: giapState, initialProps, ...props} = incomingProps as P & WrapperProps; // getStaticProps state const gspState = props?.__N_SSG ? props?.pageProps?.initialState : null; // getServerSideProps state const gsspState = props?.__N_SSP ? props?.pageProps?.initialState : null; // getInitialPageProps state const gippState = !gspState && !gsspState ? props?.pageProps?.initialState ?? null : null; // debug 打印代碼,刪除掉了 if (config.debug) {} // 獲取store,initStore 會緩存 store ,有則直接緩存,沒有則進(jìn)行初始化。 const store = useMemo<S>(() => initStore<S>({makeStore}), []); // state 初始化 useHybridHydrate(store, giapState, gspState, gsspState, gippState); // 后續(xù)余state 無關(guān),主要是去掉 let resultProps: any = props; // 順序很重要! Next.js 中 page 的 getStaticProps 應(yīng)該覆蓋 pages/_app 中的 props // @see https://github.com/zeit/next.js/issues/11648 if (initialProps && initialProps.pageProps) { resultProps.pageProps = { ...initialProps.pageProps, // this comes from wrapper in _app mode ...props.pageProps, // this comes from gssp/gsp in _app mode }; } // 防止props中的 initialState被到處傳遞,為了數(shù)據(jù)安全,清除掉 initialState if (props?.pageProps?.initialState) { resultProps = {...props, pageProps: {...props.pageProps}}; delete resultProps.pageProps.initialState; } // 處理 getInitialPageProps 的數(shù)據(jù), 清除掉 initialProps if (resultProps?.pageProps?.initialProps) { resultProps.pageProps = {...resultProps.pageProps, ...resultProps.pageProps.initialProps}; delete resultProps.pageProps.initialProps; } return {store, props: {...initialProps, ...resultProps}}; };
useWrappedStore
hook 主要做了以下幾件事:
- 從
App props
中獲取數(shù)據(jù),并進(jìn)行分類 - 初始化
store
:服務(wù)端不會再初始化,會從緩存獲取,客戶端則會在這里初始化。 - 處理數(shù)據(jù)覆蓋排序及清除
wrapper
函數(shù)注入的自定義數(shù)據(jù)字段:initialProps
和initialState
。
initStore
// 初始化 redux store const initStore = <S extends Store>({makeStore, context = {}}: InitStoreOptions<S>): S => { const createStore = () => makeStore(context); if (getIsServer()) { // 服務(wù)端則存儲到 req.__nextReduxWrapperStore 參數(shù) 中進(jìn)行服用 const req: any = (context as NextPageContext)?.req || (context as AppContext)?.ctx?.req; if (req) { // ATTENTION! THIS IS INTERNAL, DO NOT ACCESS DIRECTLY ANYWHERE ELSE // @see https://github.com/kirill-konshin/next-redux-wrapper/pull/196#issuecomment-611673546 if (!req.__nextReduxWrapperStore) { req.__nextReduxWrapperStore = createStore(); // Used in GIP/GSSP } return req.__nextReduxWrapperStore; } return createStore(); } // Memoize the store if we're on the client // 客戶端則存儲到 sharedClientStore 上 if (!sharedClientStore) { sharedClientStore = createStore(); } return sharedClientStore; };
initStore
函數(shù)中,服務(wù)端利用每次請求都會新生成的對象 req
來緩存服務(wù)端請求上下文的 store
,客戶端利用 sharedClientStore
變量來存儲。
useHybridHydrate hook
// hydrate 初始化前置邏輯處理 const useHybridHydrate = (store: S, giapState: any, gspState: any, gsspState: any, gippState: any) => { const {events} = useRouter(); const shouldHydrate = useRef(true); // 客戶端路由改變的時候好應(yīng)該重新初始化。 useEffect(() => { const handleStart = () => { shouldHydrate.current = true; }; events?.on('routeChangeStart', handleStart); return () => { events?.off('routeChangeStart', handleStart); }; }, [events]); // 這里寫了一大堆注釋,就是表面 useMemo 里面進(jìn)行 hydrate 并不會導(dǎo)致頁面重新不斷渲染,主要是當(dāng)作 constructor 來使用 useMemo(() => { if (shouldHydrate.current) { hydrateOrchestrator(store, giapState, gspState, gsspState, gippState); shouldHydrate.current = false; } }, [store, giapState, gspState, gsspState, gippState]); };
useHybridHydrate
函數(shù)主要用于處理 初始化 state
的前置邏輯,主要使用 useMemo
來盡早調(diào)用 hydrateOrchestrator
函數(shù)初始化 state
, 并使用監(jiān)聽路由的方式來處理客戶端路由切換時需要重新初始化的問題。
hydrateOrchestrator
// hydrate 處理中間數(shù)據(jù)的函數(shù)(用于分析處理各種情況) const hydrateOrchestrator = (store: S, giapState: any, gspState: any, gsspState: any, gippState: any) => { if (gspState) { // `getStaticProps` 并不能和其他數(shù)據(jù)獲取函數(shù)同時存在在一個頁面,但可能存在 `getInitialAppProps`,這時候只會在構(gòu)建的時候執(zhí)行,因此需要覆蓋 `giapState` hydrate(store, giapState); hydrate(store, gspState); } else if (gsspState || gippState || giapState) { // 處理優(yōu)先級問題,getServerSideProps > getInitialPageProps > getInitialAppProps hydrate(store, gsspState ?? gippState ?? giapState); } };
hydrateOrchestrator
函數(shù)主要用于處理獲取state的覆蓋和優(yōu)先級問題:
getStaticProps
并不能和其他數(shù)據(jù)獲取函數(shù)同時存在在一個頁面,但可能存在getInitialAppProps
,這時候只會在構(gòu)建的時候執(zhí)行,因此需要覆蓋giapState
。- 非
SSG
情況,優(yōu)先級getServerSideProps
>getInitialPageProps
>getInitialAppProps
hydrate
// hydrate 處理函數(shù),觸發(fā)服務(wù)端渲染過程中的數(shù)據(jù) HYDRATE const hydrate = (store: S, state: any) => { if (!state) { return; } store.dispatch({ type: HYDRATE, payload: getDeserializedState<S>(state, config), } as any); };
hydrate
函數(shù)就是用于發(fā)起 HYDRATE
action 的。
總結(jié)
本文主要從 目錄結(jié)構(gòu) 分析,然后再到 代碼結(jié)構(gòu)分析,最好再分析重點(diǎn)代碼實(shí)現(xiàn),這樣一步一步的去讀源碼,可以讓思路更加清晰。
讀源碼之前,有使用經(jīng)驗(yàn)也是一個很重要的點(diǎn),這樣才能帶著疑問和思考去讀,讓閱讀源碼更有意思
以上就是next-redux-wrapper使用細(xì)節(jié)及源碼分析的詳細(xì)內(nèi)容,更多關(guān)于next-redux-wrapper使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React.js組件實(shí)現(xiàn)拖拽排序組件功能過程解析
這篇文章主要介紹了React.js組件實(shí)現(xiàn)拖拽排序組件功能過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04react自動化構(gòu)建路由的實(shí)現(xiàn)
這篇文章主要介紹了react自動化構(gòu)建路由的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04React中用@符號編寫文件路徑實(shí)現(xiàn)方法介紹
在Vue中,我們導(dǎo)入文件時,文件路徑中可以使用@符號指代src目錄,極大的簡化了我們對路徑的書寫。但是react中,要想實(shí)現(xiàn)這種方式書寫文件路徑,需要寫配置文件來實(shí)現(xiàn)2022-09-09詳解React setState數(shù)據(jù)更新機(jī)制
這篇文章主要介紹了React setState數(shù)據(jù)更新機(jī)制的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用React框架,感興趣的朋友可以了解下2021-04-04