next-redux-wrapper使用細節(jié)及源碼分析
引言
前幾天寫了一篇文章講解了 wrapper 概念,本篇文章主要從源碼分析它的實現(xiàn)過程,去更好的理解服務端使用 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 庫也是為案例庫服務的,那么只用管 wrapper 庫即可。
wrapper 結(jié)構(gòu):
packages/wrapper ├── jest.config.js jest 配置 ├── next-env.d.ts ├── package.json ├── src │ └── index.tsx 核心實現(xiàn)代碼 ├── tests 測試代碼 │ ├── client.spec.tsx │ ├── server.spec.tsx │ └── testlib.tsx ├── tsconfig.es6.json └── tsconfig.json
核心代碼都在 packages/wrapper/src/index.tsx文件,下面開始分析。
代碼結(jié)構(gòu)
把大部分代碼的具體實現(xiàn)去掉,然后留下整體代碼的頂層聲明:
// 用于客戶端 hydrate 的時發(fā)送初始化指令用的 action key
export const HYDRATE = '__NEXT_REDUX_WRAPPER_HYDRATE__';
// 用于判斷是否是服務器
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用于判斷是否是服務器.getDeserializedState用于反序列化State,類似解密函數(shù),依賴外部使用方傳入。getSerializedState用于序列化State,類似加密函數(shù),依賴外部使用方傳入。sharedClientStore變量,用于客戶端存儲redux store用的,用在initStore中。initStore初始化redux store,主要是處理 服務端 和 客戶端 在創(chuàng)建時緩存的位置問題。createWrapper創(chuàng)建 wrapper 核心default默認導出函數(shù),以前的函數(shù),忽略掉,不建議使用了,主要使用createWrapper函數(shù)返回的withRedux函數(shù),因此本篇文章不進行講解withRedux函數(shù)。
一共就 8 個聲明,其中只暴露了 HYDRATE 、 createWrapper 和默認導出函數(shù),忽略掉默認導出函數(shù)。
下面進行對 createWrapper 函數(shù)的分析。
核心實現(xiàn)
createWrapper 整體實現(xiàn)概覽
// 創(chuàng)建 wrapper
export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => {
// 幾個 wrapper 函數(shù)的實際核心實現(xiàn)函數(shù)
const makeProps = async (): Promise<WrapperProps> => {/** ... */};
// 頁面級 getInitialProps 函數(shù)的包裹函數(shù)
const getInitialPageProps = (callback) => async (context) => {/** ... */};
// 應用級 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) => {
// ...
};
// 用于在應用中關(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ù)實現(xiàn)。
- Next.js 渲染頁面內(nèi)容 時的處理實現(xiàn)。
從 數(shù)據(jù)獲取 到 渲染頁面內(nèi)容 本身就有一個前后順序,這里也按照這個順序來分析。
createWrapper 中 數(shù)據(jù)獲取函數(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 — 避免雙重包裝,因為有可能上層組件也調(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});
};
// 應用級 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ū)別點在于 參數(shù)類型(也就是context 類型) 和 返回值類型 不一致,這也是 Next.js 不同數(shù)據(jù)獲取函數(shù) 的區(qū)別之一。
getInitialPageProps 需要注意有可能上層組件也調(diào)用了 getInitialPageProps 或者 getInitialAppProps 函數(shù)。 因此需要判斷是否把 store 存儲到了 context 中,是的話,需要直接從 context 中獲取。(注釋是這樣的,這里有點問題,待驗證)
getInitialAppProps 和 getInitialPageProps 實現(xiàn)類似,只是只需要執(zhí)行一次,不用考慮緩存,還有就是 返回值類型 不一樣
getStaticProps 調(diào)用 makeProps 函數(shù)時,相比 getInitialPageProps 和 getInitialAppProps 缺少了 addStoreToContext 參數(shù),返回值也不一樣。
getServerSideProps 返回參數(shù)和 getStaticProps 一樣,因此直接調(diào)用 getStaticProps 函數(shù)即可。
接下來就是幾個數(shù)據(jù)獲取的 wrapper 函數(shù) 的實際核心實現(xiàn),也就是 makeProps 函數(shù):
// 數(shù)據(jù)獲取的 `wrapper 函數(shù)` 的實際核心
const makeProps = async ({
callback,
context,
addStoreToContext = false,
}: {
callback: Callback<S, any>;
context: any;
addStoreToContext?: boolean;
}): Promise<WrapperProps> => {
// 服務端初始化 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;
}
}
// 這里實現(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,
// 在服務端可以對 state 數(shù)據(jù)加密,防止被輕易解析
initialState: getIsServer() ? getSerializedState<S>(state, config) : state,
};
};
makeProps 函數(shù)主要初始化 store ,然后把 store 傳遞給 callback 函數(shù)并執(zhí)行獲取到一個函數(shù),這個函數(shù)傳入 context 再次執(zhí)行,就可以獲取到對應 數(shù)據(jù)獲取函數(shù) 的返回值,處理后進行返回即可。
其中需要注意下面幾點:
callback函數(shù)必須返回一個函數(shù),否則其返回的initialProps會被是空對象- 調(diào)用
createWrapper時傳遞參數(shù)中的debug參數(shù)在這里會起作用 - 把
store放入context是一個遺留的處理方式,后續(xù)可能會去掉。但并沒說明后續(xù)處理方式。 - 返回的
initialState在服務端可能自定義加密方法。
createWrapper 中 渲染頁面內(nèi)容 時的處理實現(xiàn)分析
服務端把數(shù)據(jù)獲取后,下面就要進行的步驟是:渲染頁面內(nèi)容。
有幾個概念需要先理清楚,不然會直接看前面幾個函數(shù)的實現(xiàn)會很懵逼。
giapState:對應getInitialAppProps處理后的stategspState:對應getStaticProps處理后的stategsspState:對應getServerSideProps處理后的stategippState:對應getInitialPageProps處理后的state
useWrappedStore hook
// 用于在應用中關(guān)聯(lián) store 的hook。
const useWrappedStore = <P extends AppProps>(incomingProps: P, displayName = 'useWrappedStore'): {store: S; props: P} => {
// createWrapper 給 incomingProps 添加了 WrapperProps 類型,因為它們不是從 P 類型獲取,因此在這里進行強制賦予類型
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 ,有則直接緩存,沒有則進行初始化。
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 應該覆蓋 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ù),并進行分類 - 初始化
store:服務端不會再初始化,會從緩存獲取,客戶端則會在這里初始化。 - 處理數(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()) {
// 服務端則存儲到 req.__nextReduxWrapperStore 參數(shù) 中進行服用
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ù)中,服務端利用每次請求都會新生成的對象 req 來緩存服務端請求上下文的 store,客戶端利用 sharedClientStore 變量來存儲。
useHybridHydrate hook
// hydrate 初始化前置邏輯處理
const useHybridHydrate = (store: S, giapState: any, gspState: any, gsspState: any, gippState: any) => {
const {events} = useRouter();
const shouldHydrate = useRef(true);
// 客戶端路由改變的時候好應該重新初始化。
useEffect(() => {
const handleStart = () => {
shouldHydrate.current = true;
};
events?.on('routeChangeStart', handleStart);
return () => {
events?.off('routeChangeStart', handleStart);
};
}, [events]);
// 這里寫了一大堆注釋,就是表面 useMemo 里面進行 hydrate 并不會導致頁面重新不斷渲染,主要是當作 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ā)服務端渲染過程中的數(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)分析,最好再分析重點代碼實現(xiàn),這樣一步一步的去讀源碼,可以讓思路更加清晰。
讀源碼之前,有使用經(jīng)驗也是一個很重要的點,這樣才能帶著疑問和思考去讀,讓閱讀源碼更有意思
以上就是next-redux-wrapper使用細節(jié)及源碼分析的詳細內(nèi)容,更多關(guān)于next-redux-wrapper使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React.js組件實現(xiàn)拖拽排序組件功能過程解析
這篇文章主要介紹了React.js組件實現(xiàn)拖拽排序組件功能過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04
詳解React setState數(shù)據(jù)更新機制
這篇文章主要介紹了React setState數(shù)據(jù)更新機制的相關(guān)資料,幫助大家更好的理解和學習使用React框架,感興趣的朋友可以了解下2021-04-04

