React 多頁簽緩存處理

暑不盡的盛夏

關(guān)注并將「趣談前端」設(shè)為星標(biāo)
每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維
1. 背景ant design pro v2的項目需要多頁簽顯示頁面而且還需要頁面能夠緩存下來。
- 多頁面展示
- 不同數(shù)據(jù)會對應(yīng)不同的詳情頁面
- 代碼里面的路由跳轉(zhuǎn)可以正常進(jìn)行頁面切換
- 頁簽可以關(guān)閉

這里主要是考慮多頁面+緩存問題。
這里是借用了ant tabs標(biāo)簽的緩存作用。tabs的多頁面只是顯示和隱藏,組件本身還存在。
路由這一塊,其實路由目前的基本使用頁面只是會渲染當(dāng)前匹配成功的路由節(jié)點。
這里鉆了一個空子。Route在沒有標(biāo)注path的情況下會作為沒有匹配路徑的url進(jìn)行渲染,作為沒有匹配任何路由的情況下渲染節(jié)點。
因為我們通過路由path去匹配是不行,只有使用不帶path的情況下渲染節(jié)點,而且不能使用switch。
而且不使用path的情況下,路由對應(yīng)渲染的組件則不能依賴于react-router的機(jī)制來自動匹配,頁面的渲染就需要我們進(jìn)行手動處理。因為打開多少個頁面就會有多少個no path的Route的節(jié)點。
3. 講解回到具體的實現(xiàn):BasicLayout.js
3.1 原先的版本
const?layout?=?(
??????<Layout>
????????{isTop?&&?!isMobile???null?:?(
??????????<SiderMenu
????????????logo={logo}
????????????theme={navTheme}
????????????onCollapse={this.handleMenuCollapse}
????????????menuData={menuData}
????????????isMobile={isMobile}
????????????{...this.props}
??????????/>
????????)}
????????<Layout
??????????style={{
????????????...this.getLayoutStyle(),
????????????minHeight:?'100vh',
??????????}}
????????>
??????????<Header
????????????menuData={menuData}
????????????handleMenuCollapse={this.handleMenuCollapse}
????????????logo={logo}
????????????isMobile={isMobile}
????????????{...this.props}
??????????/>
??????????<Content?className={styles.content}?style={contentStyle}>
????????????<Authorized?authority={routerConfig}?noMatch={<Exception403?/>}>
??????????????{children}
????????????</Authorized>
??????????</Content>
??????????<Footer?/>
????????</Layout>
??????</Layout>
????);
以前的版本里直接渲染對應(yīng)children。這里具體ant deisgn pro,或者umijs做了對應(yīng)的處理,我沒具體去看。現(xiàn)在的話我們不會用這個版本去做。
3.2 現(xiàn)在的版本
3.2.1 layout
const?layout?=?(
??????<Layout>
????????{isTop?&&?!isMobile???null?:?(
??????????<SiderMenu
????????????logo={logo}
????????????theme={navTheme}
????????????onCollapse={this.handleMenuCollapse}
????????????menuData={menuData}
????????????isMobile={isMobile}
????????????{...this.props}
????????????//?onHandlePage={this.onHandlePage}
??????????/>
????????)}
????????<Layout
??????????style={{
????????????...this.getLayoutStyle(),
????????????minHeight:?'100vh',
??????????}}
????????>
??????????<Header
????????????menuData={menuData}
????????????handleMenuCollapse={this.handleMenuCollapse}
????????????logo={logo}
????????????isMobile={isMobile}
????????????{...this.props}
??????????/>
??????????<Content?className={styles.content}?style={contentStyle}>
????????????<div?className={styles.contentBox}>
??????????????<div?className={styles.contentTabUrlBox}>
????????????????<div?className={styles.contentTabUrl}>
??????????????????<Tabs
????????????????????activeKey={activeKey}
????????????????????onChange={this.onChange}
????????????????????tabBarExtraContent={operations}
????????????????????type="editable-card"
????????????????????tabBarStyle={{?background:?'#fff'?}}
????????????????????tabPosition="top"
????????????????????tabBarGutter={-1}
????????????????????onEdit={this.onEdit}
????????????????????hideAdd
??????????????????>
????????????????????{listenRouterState.map(item?=>?(
??????????????????????<TabPane?tab={item.tab}?key={item.key}?closable={item.closable}>
????????????????????????<RouterContext.Provider?value={customerMatchs}>
??????????????????????????<Route?key={item.key}?component={item.content}?exact?/>
????????????????????????</RouterContext.Provider>
????????????????????????{/*?{item.component()}?*/}
??????????????????????</TabPane>
????????????????????))}
??????????????????</Tabs>
??????????????????<Footer?/>
????????????????</div>
??????????????</div>
????????????</div>
??????????</Content>
????????</Layout>
??????</Layout>
????);
核心的代碼是
??<Tabs
????activeKey={activeKey}
????onChange={this.onChange}
????tabBarExtraContent={operations}
????type="editable-card"
????tabBarStyle={{?background:?'#fff'?}}
????tabPosition="top"
????tabBarGutter={-1}
????onEdit={this.onEdit}
????hideAdd
??>
????{listenRouterState.map(item?=>?(
??????<TabPane?tab={item.tab}?key={item.key}?closable={item.closable}>
????????<RouterContext.Provider?value={customerMatchs}>
??????????<Route?key={item.key}?component={item.content}?exact?/>
????????</RouterContext.Provider>
??????</TabPane>
????))}
??</Tabs>
這里使用的是tab + 路由 with no path的方式。現(xiàn)在我們需要將組件和path進(jìn)行關(guān)聯(lián)起來。因為沒有使用路由匹配了。代碼里的listenRouterState就是我們打開頁面的key和對應(yīng)組件的mapping關(guān)系。
這里path的處理我使用的是路由監(jiān)控,因為是對于整個系統(tǒng)的頁面多頁簽,所以我使用了路由監(jiān)控。
3.2.2 componentDidMount路由監(jiān)控
UN_LISTTEN?=?history.listen(route?=>?{
??????const?{?listenRouterState,?listenRouterKey,?customerMatchs?}?=?this.state;
??????let?replaceRouter?=?routerArray.filter(itemRoute?=>
????????pathToRegexp(itemRoute.key?||?'').test(route.pathname),
??????)[0];
??????let?currentKey?=?'';
??????if?(replaceRouter?&&?replaceRouter.isOnlyOnePage)?{
????????currentKey?=?route.pathname;
??????}?else?{
????????currentKey?=?route.pathname?+?this.parseQueryString(route.search);
??????}
??????if?(!listenRouterKey.includes(currentKey))?{
????????if?(!replaceRouter)?{
??????????replaceRouter?=?routerArray.filter(itemroute?=>?itemroute.key?===?'/404')?.[0];
??????????this.setState({
????????????listenRouterState:?[
??????????????...listenRouterState,
??????????????{?...replaceRouter,?key:?currentKey,?tab:?'404'?},
????????????],
????????????activeKey:?currentKey,
????????????listenRouterKey:?[...listenRouterKey,?currentKey],
??????????});
????????}?else?{
??????????const?match?=?matchPath(route.pathname,?{?path:?replaceRouter.key?});
??????????this.setState({
????????????listenRouterState:?[
??????????????...listenRouterState,
??????????????{
????????????????...replaceRouter,
????????????????key:?currentKey,
????????????????tab:
??????????????????this.getPageTitle(route.pathname,?breadcrumbNameMap)?+
??????????????????this.getDetailPagePrimaryId(route,?match),
??????????????},
????????????],
????????????activeKey:?currentKey,
????????????listenRouterKey:?[...listenRouterKey,?currentKey],
????????????customerMatchs:?[...customerMatchs,?{?key:?currentKey,?match?}],
??????????});
????????}
??????}
??????this.setState({
????????activeKey:?currentKey,
??????});
????});
??}
3.2.2.1 主要介紹
這里主要是在做什么,監(jiān)控路由然后進(jìn)行路由匹配,獲取對應(yīng)的組件。先介紹一下這里面用到的一些state變量
- listenRouterState:打開頁面數(shù)據(jù)對象,也是在layout渲染的數(shù)組,存儲了pathname和component的mapping關(guān)系
- activeKey:當(dāng)前打開的頁面key
- listenRouterKey:listenRouterState對象key屬性的數(shù)組集合,用于一些數(shù)據(jù)判斷。
- customerMatchs:適配match,這里可以先不管,因為這個是服務(wù)于下面實際問題的。
這里的主要邏輯就是,監(jiān)控路由,判斷路由是否已經(jīng)打開,如果已經(jīng)打開就不會重新打開。這里的key是全路徑,是加上查詢參數(shù)的。如下面的這個地址:

3.2.2.2 pathname 和 key的區(qū)別
但是匹配組件內(nèi)容不能使用這個進(jìn)行匹配的,還是需要使用pathname進(jìn)行匹配的。還是先看一下具體路由監(jiān)控的到的route數(shù)據(jù)是什么?

所以我們上面說的key:是pathname + query。這里要分清key和pathname的區(qū)別,因為pathname是用來匹配獲取組件的,key是為了進(jìn)行多個詳情頁面的區(qū)分,如果不是全路徑是沒有辦法區(qū)分不同詳情頁面的。
當(dāng)然我們這個pathname是比較好匹配的,假如是下面的這種,下面的路由對應(yīng)的是:
??????????{
????????????path:?'/List/:title/table-match-detail',
????????????hideInMenu:?true,
????????????name:?'detail',
????????????code:?'list_tableDetail_page',
????????????component:?'./List/MatchDetail',
??????????},

3.2.2.3 如何將pathname和路由正確的匹配
pathToRegexp(itemRoute.key?||?'').test(route.pathname)
針對路由的匹配,因為有match參數(shù)的存在,所以這里我用的是pathToRegexp,可以很好的解決這個問題。
3.2.2.4 listenRouterState的邏輯判斷
if?(!listenRouterKey.includes(currentKey))?{
????????if?(!replaceRouter)?{
??????????replaceRouter?=?routerArray.filter(itemroute?=>?itemroute.key?===?'/404')?.[0];
??????????this.setState({
????????????listenRouterState:?[
??????????????...listenRouterState,
??????????????{?...replaceRouter,?key:?currentKey,?tab:?'404'?},
????????????],
????????????activeKey:?currentKey,
????????????listenRouterKey:?[...listenRouterKey,?currentKey],
??????????});
????????}?else?{
??????????const?match?=?matchPath(route.pathname,?{?path:?replaceRouter.key?});
??????????this.setState({
????????????listenRouterState:?[
??????????????...listenRouterState,
??????????????{
????????????????...replaceRouter,
????????????????key:?currentKey,
????????????????tab:
??????????????????this.getPageTitle(route.pathname,?breadcrumbNameMap)?+
??????????????????this.getDetailPagePrimaryId(route,?match),
??????????????},
????????????],
????????????activeKey:?currentKey,
????????????listenRouterKey:?[...listenRouterKey,?currentKey],
????????????customerMatchs:?[...customerMatchs,?{?key:?currentKey,?match?}],
??????????});
????????}
??????}
??????this.setState({
????????activeKey:?currentKey,
??????});
這里做的就是對當(dāng)前的key進(jìn)行判斷,如果不存在,那就是頁面沒有打開,則添加新的數(shù)據(jù)進(jìn)行,如果已經(jīng)打開,則跳轉(zhuǎn)到新的頁面,如果匹配路徑獲取組件沒有成功,則跳轉(zhuǎn)到404。
3.2.2.5 不同詳情頁面的title如何處理
因為詳情頁有多個,但是tab的標(biāo)簽頁title要不同

tab:?this.getPageTitle(route.pathname,?breadcrumbNameMap)?
?????+?this.getDetailPagePrimaryId(route,?match),
getPageTitle主要用的還是之前的邏輯,主要說明一下getDetailPagePrimaryId
getDetailPagePrimaryId?=?(route,?match)?=>?{
????const?detailPageIdEnum?=?['id',?'title',?'activityNo'];
????let?titleValue?=?'';
????//?處理query類型
????Object.keys(route.query).forEach(item?=>?{
??????if?(detailPageIdEnum.includes(item)?&&?!titleValue)?{
????????titleValue?=?route.query[item];
??????}
????});
????//?處理match
????Object.keys(match.params).forEach(item?=>?{
??????if?(detailPageIdEnum.includes(item)?&&?!titleValue)?{
????????titleValue?=?match.params[item];
??????}
????});
????return?titleValue???`?-?${titleValue}`?:?'';
??};
這里的邏輯主要是從query和match中間變量值,只要匹配成功,就會返回匹配的數(shù)據(jù)值。detailPageIdEnum主要是系統(tǒng)層級可能對應(yīng)的變量名稱
比如query中的title

或者match中title 【當(dāng)然這里的match是有問題的,在下面實際問題的時候會說明一下】

3.2.3 其他代碼
其他的就不是核心代碼,基本分為兩塊,一塊是初始化處理,另外一塊是table的menu的處理.
3.2.3.1 初始化處理
??constructor(props)?{
????super(props);
????this.getPageTitle?=?memoizeOne(this.getPageTitle);
????this.matchParamsPath?=?memoizeOne(this.matchParamsPath,?isEqual);
????routerArray?=?this.updateTree(props.route.routes);
????const?homeRouter?=?routerArray.filter(itemroute?=>?itemroute.key?===?'/')[0];
????this.state?=?{
??????listenRouterState:?[{?...homeRouter,?key:?'/',?tab:?'首頁',?closable:?false?}],
??????listenRouterKey:?['/'],
??????activeKey:?'/',
??????customerMatchs:?[],
????};
??}
主要就是會將routers的數(shù)據(jù)做一個基礎(chǔ)處理,第二個就是添加一個首頁在tab標(biāo)簽頁面。
3.2.3.2 tab menu的處理
主要處理,關(guān)閉打開的頁簽,關(guān)閉當(dāng)前頁面,關(guān)閉全部頁面,關(guān)閉其他頁面

這一塊比較簡單,就不介紹了。
onClickHover?=?e?=>?{
????//?message.info(`Click?on?item?${key}`);
????const?{?key?}?=?e;
????const?{?activeKey,?listenRouterState,?listenRouterKey,?routeKey?}?=?this.state;
????if?(key?===?'1')?{
??????this.setState({
????????activeKey:?routeKey,
????????listenRouterState:?listenRouterState.filter(
??????????v?=>?v.key?!==?activeKey?||?v.key?===?routeKey?||?!v.closable,
????????),
????????listenRouterKey:?listenRouterKey.filter(
??????????v?=>?v?!==?activeKey?||?v?===?routeKey?||?!v.closable,
????????),
??????});
????}?else?if?(key?===?'2')?{
??????this.setState({
????????activeKey,
????????listenRouterState:?listenRouterState.filter(
??????????v?=>?v.key?===?activeKey?||?v.key?===?routeKey?||?!v.closable,
????????),
????????listenRouterKey:?listenRouterKey.filter(
??????????v?=>?v?===?activeKey?||?v?===?routeKey?||?v?===?'/',
????????),
????????customerMatchs:?listenRouterState.filter(
??????????v?=>?v.key?===?activeKey?||?v.key?===?routeKey?||?!v.closable,
????????),
??????});
????}?else?if?(key?===?'3')?{
??????this.setState({
????????activeKey:?'/',
????????listenRouterState:?listenRouterState.filter(v?=>?v.key?===?routeKey?||?!v.closable),
????????listenRouterKey:?listenRouterKey.filter(v?=>?v?===?routeKey?||?v?===?'/'),
????????customerMatchs:?listenRouterState.filter(v?=>?v.key?===?routeKey?||?!v.closable),
??????});
????}
??};
??onEdit?=?(targetKey,?action)?=>?{
????this[action](targetKey);
??};
??remove?=?targetKey?=>?{
????const?{?activeKey,?listenRouterState?}?=?this.state;
????let?newActiviKey?=?activeKey;
????let?lastIndex;
????listenRouterState.forEach((pane,?i)?=>?{
??????if?(pane.key?===?targetKey)?{
????????lastIndex?=?i?-?1;
??????}
????});
????const?tabList?=?[];
????const?tabListKey?=?[];
????listenRouterState.forEach(pane?=>?{
??????if?(pane.key?!==?targetKey)?{
????????tabList.push(pane);
????????tabListKey.push(pane.key);
??????}
????});
????if?(lastIndex?>=?0?&&?activeKey?===?targetKey)?{
??????newActiviKey?=?tabList[lastIndex].key;
????}
????router.push(newActiviKey);
????this.setState({
??????listenRouterState:?tabList,
??????activeKey:?newActiviKey,
??????listenRouterKey:?tabListKey,
????});
??};4.1 問題
redux在多頁簽的頁面里會存在問題,比如以下兩種情況:
- 詳情頁面:因為詳情頁面可以打開多個,但是都是公用同一個redux。
- 多列表頁面共用同一個model


4.2 解決思路
- 動態(tài)路由,手動注冊model實現(xiàn),但是在ant design pro內(nèi)部不是很好實現(xiàn)。提了一個官方issue問題:ant design pro多頁簽緩存問題
- 不使用redux,大部分頁面是不需要使用redux。只通過頁面調(diào)用接口請求,數(shù)據(jù)存儲都放在組件state中去。
- 使用redux,同一個model通過關(guān)鍵key進(jìn)行數(shù)據(jù)區(qū)分
4.3 公用同一個model的具體操作



4.3.1 hoc
為什么使用hoc,這里是為了不影響業(yè)務(wù)頁面做修改,將修改的地方放在hoc統(tǒng)一處理掉。這里主要是兩個問題:
- dispatch需要將當(dāng)前primaryKey傳到redux,因為對應(yīng)的數(shù)據(jù)更新需要放到對應(yīng)的primarykey對應(yīng)的state里面去。
- 業(yè)務(wù)組件使用state數(shù)據(jù)的時候,需要將當(dāng)前primaryKey對應(yīng)的數(shù)據(jù)傳入到props里面
/**
?*?高階函數(shù)
?*?@param?{*}?namespace
?*?@param?{*}?primaryKey
?*/
function?wrapperWithSubscription(namespace,?primaryKey)?{
??//?eslint-disable-next-line?no-use-before-define
??const?modelNameSpace?=?namespace;
??const?modelPrimaryKey?=?primaryKey;
??return?function?withSubscription(WrappedComponent)?{
????//?...并返回另一個組件...
????return?class?extends?React.Component?{
??????constructor(props)?{
????????super(props);
????????this.state?=?{
??????????currentProps:?Object.assign({},?props.location),
??????????initData:?{},
????????};
??????}
??????componentWillMount()?{
????????const?{?dispatch,?location?}?=?this.props;
????????dispatch({
??????????type:?`${modelNameSpace}/initData`,
??????????payload:?{
????????????primaryKey:?location.query[modelPrimaryKey],
??????????},
????????});
????????dispatch({
??????????type:?`${modelNameSpace}/getExample`,
??????????payload:?{},
??????????callback:?result?=>?{
????????????this.setState({
??????????????initData:?result,
????????????});
??????????},
????????});
??????}
??????componentWillUnmount()?{
????????//?可以自定擴(kuò)展如何消除當(dāng)前primarykey對應(yīng)的數(shù)據(jù)
????????//?一般情況下,前端業(yè)務(wù)組件會自己清除state的數(shù)據(jù)
??????}
??????wrapperDispatch?=?(dispatchPrams)?=>?{
????????const?{
??????????dispatch,
????????}?=?this.props;
????????const?{?currentProps:?{?query?}?}?=?this.state;
????????dispatch({
??????????...dispatchPrams,
??????????primaryKey:?query[modelPrimaryKey],
????????});
??????};
??????render()?{
????????const?{
??????????initData,
??????????currentProps:?{?query?},
????????}?=?this.state;
????????const?modelNameSpaceProps?=?{
??????????//?eslint-disable-next-line?react/destructuring-assignment
??????????[modelNameSpace]:?this.props[modelNameSpace][query[modelPrimaryKey]]?||?initData,
????????};
????????return?(
??????????<WrappedComponent
????????????{...this.props}
????????????dispatch={this.wrapperDispatch}
????????????{...modelNameSpaceProps}
??????????/>
????????);
??????}
????};
??};
}
4.3.1.1 wrapperDispatch
其實頁面組件的dispatch會走當(dāng)前的頁面,這里會統(tǒng)一將primaryKey傳入進(jìn)去
wrapperDispatch?=?(dispatchPrams)?=>?{
??const?{
????dispatch,
??}?=?this.props;
??const?{?currentProps:?{?query?}?}?=?this.state;
??dispatch({
????...dispatchPrams,
????primaryKey:?query[modelPrimaryKey],
??});
};
4.3.1.2 render
render函數(shù)會處理redux的state,將對應(yīng)當(dāng)前頁面的數(shù)據(jù)傳回,頁面組件還按照之前直接訪問,下面的detail就是當(dāng)前頁面對應(yīng)的model的namespace名稱
????const?{?history,?location,?detail?}?=?this.props;
??????render()?{
????????const?{
??????????initData,
??????????currentProps:?{?query?},
????????}?=?this.state;
????????const?modelNameSpaceProps?=?{
??????????//?eslint-disable-next-line?react/destructuring-assignment
??????????[modelNameSpace]:?this.props[modelNameSpace][query[modelPrimaryKey]]?||?initData,
????????};
????????return?(
??????????<WrappedComponent
????????????{...this.props}
????????????dispatch={this.wrapperDispatch}
????????????{...modelNameSpaceProps}
??????????/>
????????);
??????}
4.3.2 model文件會做哪些改變呢?
4.3.2.1 初始state的不同
const?initDataExample?=?{
??data:?{
????name:?'',
??},
};
export?default?{
??namespace:?'detail',
??state:?{},
??
??*****
}
現(xiàn)在我們的state里面初始是沒有值,因為state的一級屬性值使我們頁面對應(yīng)的primaryKey。
我們會定義一個基礎(chǔ)結(jié)構(gòu),initDataExample。用于在組件初始化的時候使用這個初始值添加到state對應(yīng)的primaryKey。
4.3.2.2 新增兩個服務(wù)于hoc方法effects
????*getExample({?callback?})?{
??????if?(callback)?callback({?...initDataExample?});
????},
????*initData({?payload?},?{?put?})?{
??????yield?put({
????????type:?'init',
????????payload:?{
??????????[payload.primaryKey]:?{
????????????...initDataExample,
??????????},
????????},
??????});
????},
getExample:獲取初始數(shù)據(jù)結(jié)構(gòu)
initData:初始化數(shù)據(jù)結(jié)構(gòu)
getExample呢,是因為hoc內(nèi)部初始化函數(shù)的時候,state是異步的,不會直接在頁面render的時候直接初始成功,所以這里的getExample是為了在state還沒有更新的情況下,使用初始函數(shù)去拿到值,傳遞給組件。
hoc
??????componentWillMount()?{
????????const?{?dispatch,?location?}?=?this.props;
????????dispatch({
??????????type:?`${modelNameSpace}/initData`,
??????????payload:?{
????????????primaryKey:?location.query.title,
??????????},
????????});
????????dispatch({
??????????type:?`${modelNameSpace}/getExample`,
??????????payload:?{},
??????????callback:?result?=>?{
????????????this.setState({
??????????????initData:?result,
????????????});
??????????},
????????});
??????}
???*****
????????
????????
?????render()?{
????????const?{
??????????initData,
??????????currentProps:?{?query?},
????????}?=?this.state;
????????const?modelNameSpaceProps?=?{
??????????//?eslint-disable-next-line?react/destructuring-assignment
??????????[modelNameSpace]:?this.props[modelNameSpace][query[modelPrimaryKey]]?||?initData,
????????};
????????return?(
??????????<WrappedComponent
????????????{...this.props}
????????????dispatch={this.wrapperDispatch}
????????????{...modelNameSpaceProps}
??????????/>
????????);
??????}
4.3.2.3 更新state的不同
????*fetch({?payload,?primaryKey?},?{?put,?select?})?{
??????const?currentPrimaryKeyState?=?yield?select(state?=>?state.detail[primaryKey]);
??????yield?put({
????????type:?'save',
????????payload:?updateWrapperModel('data',?payload,?primaryKey,?currentPrimaryKeyState),
??????});
????}
現(xiàn)在更新數(shù)據(jù),需要定位到更新到哪一個primaryKey。所以這里提供了一個函數(shù):更新的時候傳入對應(yīng)的值,然后更新對應(yīng)primaryKey下的具體的key / value
/**
?*?updateWrapperModel
?*?@param?{*}?updateKey
?*?@param?{*}?updateValue
?*?@param?{*}?primaryKey
?*?@param?{*}?currentPrimaryKeyState
?*/
export?function?updateWrapperModel(updateKey,?updateValue,?primaryKey,?currentPrimaryKeyState)?{
??return?{
????[primaryKey]:?{
??????...currentPrimaryKeyState,
??????[updateKey]:?updateValue,
????},
??};
}
4.4 業(yè)務(wù)組件的修改
我們使用hoc就是為了盡量少的減少業(yè)務(wù)組件的改動。
hoc內(nèi)部是一個統(tǒng)一的函數(shù)處理,所以hoc是不知道具體當(dāng)前業(yè)務(wù)組件對應(yīng)的modelspace是什么,以及當(dāng)前路由下對應(yīng)的primaryKey的key是什么,因為有的頁面可能是id,有的頁面取得是title。所以modelNamespace和primaryKey需要傳入到hoc。
業(yè)務(wù)組件只需要添加下面的一行代碼進(jìn)行hoc的傳遞就可以了。
@withSubscription('detail',?'title')

高階函數(shù)會接受這兩個值使用

5.1 控制是否使用多頁簽
這里其實是想可以控制部分頁面不需要根據(jù)key進(jìn)行判斷,而是根據(jù)pathname進(jìn)行判斷就好了。
解決的代碼提交是https://github.com/rodchen-king/ant-design-pro-v2/commit/86430c03d3c13f2aed1090c71fb96cf95f195853
路由需要進(jìn)行只會存在一個頁面標(biāo)示:

路由監(jiān)控的地方,判斷當(dāng)前路由如果是isOnlyOnePage,則采用pathname進(jìn)行key標(biāo)示

5.2 支持路由match數(shù)據(jù)
這里其實就是為了處理參數(shù)在pathname里面的參數(shù)

這里主要的問題是采用目前的這種方式,match的數(shù)據(jù)是不會正常返回的
所以這里對于項目中用到的props.match都需要單獨處理一下。
所以在BasicLayout里面做了單獨的處理,就是上面說的customerMatch。其實是為了處理這個問題的。
5.2.1 具體實現(xiàn)
整體的思想和redux應(yīng)用里面有類似的思路。不同的是
監(jiān)控路由匹配的時候會處理得到當(dāng)前路由的match值。
const?match?=?matchPath(route.pathname,?{?path:?replaceRouter.key?});
這里其實是處理當(dāng)前url和匹配的路由pathname處理得到一個match的結(jié)果。

至于matchPath這個方法,其實是我從react-router源碼里面復(fù)制出來的:
import?pathToRegexp?from?'path-to-regexp';
const?cache?=?{};
const?cacheLimit?=?10000;
let?cacheCount?=?0;
function?compilePath(path,?options)?{
??const?cacheKey?=?`${options.end}${options.strict}${options.sensitive}`;
??const?pathCache?=?cache[cacheKey]?||?(cache[cacheKey]?=?{});
??if?(pathCache[path])?return?pathCache[path];
??const?keys?=?[];
??const?regexp?=?pathToRegexp(path,?keys,?options);
??const?result?=?{?regexp,?keys?};
??if?(cacheCount?<?cacheLimit)?{
????pathCache[path]?=?result;
????//?eslint-disable-next-line?no-plusplus
????cacheCount++;
??}
??return?result;
}
/**
?*?Public?API?for?matching?a?URL?pathname?to?a?path.
?*/
function?matchPath(pathname,?options?=?{})?{
??if?(typeof?options?===?'string'?||?Array.isArray(options))?{
????//?eslint-disable-next-line?no-param-reassign
????options?=?{?path:?options?};
??}
??const?{?path,?exact?=?false,?strict?=?false,?sensitive?=?false?}?=?options;
??const?paths?=?[].concat(path);
??//?eslint-disable-next-line?no-shadow
??return?paths.reduce((matched,?path)?=>?{
????if?(!path?&&?path?!==?'')?return?null;
????if?(matched)?return?matched;
????const?{?regexp,?keys?}?=?compilePath(path,?{
??????end:?exact,
??????strict,
??????sensitive,
????});
????const?match?=?regexp.exec(pathname);
????if?(!match)?return?null;
????const?[url,?...values]?=?match;
????const?isExact?=?pathname?===?url;
????if?(exact?&&?!isExact)?return?null;
????return?{
??????path,?//?the?path?used?to?match
??????url:?path?===?'/'?&&?url?===?''???'/'?:?url,?//?the?matched?portion?of?the?URL
??????isExact,?//?whether?or?not?we?matched?exactly
??????params:?keys.reduce((memo,?key,?index)?=>?{
????????//?eslint-disable-next-line?no-param-reassign
????????memo[key.name]?=?values[index];
????????return?memo;
??????},?{}),
????};
??},?null);
}
export?default?matchPath;
然后不同頁面的match值會存儲在customerMatchs。然后通過context進(jìn)行數(shù)據(jù)傳遞。
<RouterContext.Provider?value={customerMatchs}>
??<Route?key={item.key}?component={item.content}?exact?/>
</RouterContext.Provider>
HOC函數(shù)進(jìn)行消費withRouterMath
import?React?from?'react';
import?{?RouterContext?}?from?'@/layouts/BasicLayout';
/**
?*?高階函數(shù):?適配match
?*/
function?withRouterMath()?{
??//?eslint-disable-next-line?no-use-before-define
??return?function?withSubscription(WrappedComponent)?{
????//?...并返回另一個組件...
????return?class?extends?React.Component?{
??????constructor(props)?{
????????super(props);
????????this.state?=?{
??????????currentLocation:?Object.assign({},?props.location),
????????};
??????}
??????getMatch?=?value?=>?{
????????const?{
??????????currentLocation:?{?pathname?},
????????}?=?this.state;
????????const?returnValue?=?value.filter(item?=>?item.key?===?pathname);
????????if?(returnValue.length)?{
??????????return?returnValue[0].match;
????????}
????????return?{};
??????};
??????render()?{
????????return?(
??????????<RouterContext.Consumer>
????????????{_value?=>?<WrappedComponent?{...this.props}?match={this.getMatch(_value)}?/>}
??????????</RouterContext.Consumer>
????????);
??????}
????};
??};
}
export?default?withRouterMath;
使用的時候
@withRouterMatth()
@withSubscription('detail',?'title')
class?ListDetail?extends?React.PureComponent?{
??componentDidMount()?{
????const?{?match,?dispatch?}?=?this.props;
????dispatch({
??????type:?'detail/fetch',
??????payload:?{
????????name:?match.params.title,
??????},
????});
??}
}
這樣頁面就可以正常使用match,和原先的location一樣獲取值,然后系統(tǒng)層級也會匹配query和match的數(shù)據(jù)進(jìn)行primarykey處理。

5.3 ?業(yè)務(wù)頁面不是由單個的參數(shù)進(jìn)行唯一表示,而是多個業(yè)務(wù)參數(shù)進(jìn)行唯一表示
前面說的都是單個primarykey作為唯一頁面標(biāo)示的,但是可能部分代碼存在很復(fù)雜的情況。
舉個例子,存在一個業(yè)務(wù)組件既是詳情頁面,也是新增頁面,而且業(yè)務(wù)上還存在多個。
類似營銷活動:為了更好的說明,我在單獨加一個字段,templateName
滿減活動【templateName:滿減】
- activityType:1;
- 詳情的時候會有一個activityNo
- 新增的時候則沒有activityNo
滿折活動【templateName:滿折】
- activityType:2:
- 詳情的時候會有一個activityNo,
- 新增的時候則沒有activityNo
5.3.1 具體實現(xiàn)
https://github.com/rodchen-king/ant-design-pro-v2/commit/d0ecfd2e795cb90837b0ed94de5f4ad13012af31
這里主要是支持多個參數(shù):
修改BasicLayout.js

因為可能參數(shù)當(dāng)中涉及到中文,所以判斷key的邏輯用了decodeURIComponent方法解析。
第二個在getDetailPagePrimaryId,這里加上這個是為了適配新的查詢參數(shù)。
修改wrapperWithSubscription

主要是兼容多個參數(shù)的情況,傳進(jìn)來的是一個數(shù)組。
調(diào)用的方式參考新增的業(yè)務(wù)Maket組件的調(diào)用方式
@withSubscription('detail',?['activityNo',?'templateName',?'activityType'])

6. 優(yōu)化
6.1 dispatch.then的使用
代碼:https://github.com/rodchen-king/ant-design-pro-v2/commit/5f160db67aaad31cb1ac04d4d01a66a1fc6d0582
開發(fā)過程中存在dispatch().then的方式:
所以這里支持也要優(yōu)化一下:
????wrapperDispatch?=?dispatchPrams?=>?{
????????const?{?dispatch?}?=?this.props;
????????const?{?primaryKeyValue?}?=?this.state;
????????dispatch({
??????????...dispatchPrams,
??????????primaryKey:?primaryKeyValue,
????????});
??????};6.2 redux-state更新多個參數(shù)
代碼:https://github.com/rodchen-king/ant-design-pro-v2/commit/5f160db67aaad31cb1ac04d4d01a66a1fc6d0582
/**
?*?model相關(guān)的處理函數(shù)
?*/
/**
?*?updateWrapperModel
?*?@param?{*}?updateStateObject ????????要更新state的健值對
?*?@param?{*}?primaryKey????????????????當(dāng)前頁面對應(yīng)的primaryKey
?*?@param?{*}?currentPrimaryKeyState????primaryKey對應(yīng)的數(shù)據(jù)源
?*/
export?function?updateWrapperModel(updateStateObject,?primaryKey,?currentPrimaryKeyState)?{
??return?{
????[primaryKey]:?{
??????...currentPrimaryKeyState,
??????...updateStateObject,
????},
??};
}
https://procomponents.ant.design/components/layout
因為最新的v5 菜單已經(jīng)采用ProLayout作為布局。所以這里一種方式是利用配置項目的childRender

另一種方式則是注釋這里的代碼,然后重新寫以前版本的BasicLayout.js
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
- 點個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
- 關(guān)注公眾號【趣談前端】,定期分享?工程化?/?可視化?/?低代碼?/?優(yōu)秀開源。

Dooring可視化搭建平臺數(shù)據(jù)源設(shè)計剖析
基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進(jìn)階實戰(zhàn))
點個在看你最好看

