什么是 React 高階組件
React 高階組件就是以高階函式的方式包裹需要修飾的 React 組件,并回傳處理完成后的 React 組件,React 高階組件在 React 生態中使用的非常頻繁,比如react-router 中的 withRouter 以及 react-redux 中 connect 等許多 API 都是以這樣的方式來實作的,
使用 React 高階組件的好處
在作業中,我們經常會有很多功能相似,組件代碼重復的頁面需求,通常我們可以通過完全復制一遍代碼的方式實作功能,但是這樣頁面的維護可維護性就會變得極差,需要對每一個頁面里的相同組件去做更改,因此,我們可以將其中共同的部分,比如接受相同的查詢操作結果、組件外同一的標簽包裹等抽離出來,做一個單獨的函式,并傳入不同的業務組件作為子組件引數,而這個函式不會修改子組件,只是通過組合的方式將子組件包裝在容器組件中,是一個無副作用的純函式,從而我們能夠在不改變這些組件邏輯的情況下將這部分代碼解耦,提升代碼可維護性,
自己動手實作一個高階組件
前端專案里,帶鏈接指向的面包屑導航十分常用,但由于面包屑導航需要手動維護一個所有目錄路徑與目錄名映射的陣列,而這里所有的資料我們都能從 react-router 的路由表中取得,因此我們可以從這里入手,實作一個面包屑導航的高階組件,
首先我們看看我們的路由表提供的資料以及目標面包屑組件所需要的資料:
// 這里展示的是 react-router4 的route示例
let routes = [
{
breadcrumb: '一級目錄',
path: '/a',
component: require('../a/index.js').default,
items: [
{
breadcrumb: '二級目錄',
path: '/a/b',
component: require('../a/b/index.js').default,
items: [
{
breadcrumb: '三級目錄1',
path: '/a/b/c1',
component: require('../a/b/c1/index.js').default,
exact: true,
},
{
breadcrumb: '三級目錄2',
path: '/a/b/c2',
component: require('../a/b/c2/index.js').default,
exact: true,
},
}
]
}
]
// 理想中的面包屑組件
// 展示格式為 a / b / c1 并都附上鏈接
const BreadcrumbsComponent = ({ breadcrumbs }) => (
<div>
{breadcrumbs.map((breadcrumb, index) => (
<span key={breadcrumb.props.path}>
<link to={breadcrumb.props.path}>{breadcrumb}</link>
{index < breadcrumbs.length - 1 && <i> / </i>}
</span>
))}
</div>
);
這里我們可以看到,面包屑組件需要提供的資料一共有三種,一種是當前頁面的路徑,一種是面包屑所帶的文字,一種是該面包屑的導航鏈接指向,
其中第一種我們可以通過 react-router 提供的 withRouter 高階組件包裹,可使子組件獲取到當前頁面的 location 屬性,從而獲取頁面路徑,
后兩種需要我們對 routes 進行操作,首先將 routes 提供的資料扁平化成面包屑導航需要的格式,我們可以使用一個函式來實作它,
/**
* 以遞回的方式展平react router陣列
*/
const flattenRoutes = arr =>
arr.reduce(function(prev, item) {
prev.push(item);
return prev.concat(
Array.isArray(item.items) ? flattenRoutes(item.items) : item
);
}, []);
之后將展平的目錄路徑映射與當前頁面路徑一同放入處理函式,生成面包屑導航結構,
export const getBreadcrumbs = ({ flattenRoutes, location }) => {
// 初始化匹配陣列match
let matches = [];
location.pathname
// 取得路徑名,然后將路徑分割成每一路由部分.
.split('?')[0]
.split('/')
// 對每一部分執行一次呼叫`getBreadcrumb()`的reduce.
.reduce((prev, curSection) => {
// 將最后一個路由部分與當前部分合并,比如當路徑為 `/x/xx/xxx` 時,pathSection分別檢查 `/x` `/x/xx` `/x/xx/xxx` 的匹配,并分別生成面包屑
const pathSection = `${prev}/${curSection}`;
const breadcrumb = getBreadcrumb({
flattenRoutes,
curSection,
pathSection,
});
// 將面包屑匯入到matches陣列中
matches.push(breadcrumb);
// 傳遞給下一次reduce的路徑部分
return pathSection;
});
return matches;
};
然后對于每一個面包屑路徑部分,生成目錄名稱并附上指向對應路由位置的鏈接屬性,
const getBreadcrumb = ({ flattenRoutes, curSection, pathSection }) => {
const matchRoute = flattenRoutes.find(ele => {
const { breadcrumb, path } = ele;
if (!breadcrumb || !path) {
throw new Error(
'Router中的每一個route必須包含 `path` 以及 `breadcrumb` 屬性'
);
}
// 查找是否有匹配
// exact 為 react router4 的屬性,用于精確匹配路由
return matchPath(pathSection, { path, exact: true });
});
// 回傳breadcrumb的值,沒有就回傳原匹配子路徑名
if (matchRoute) {
return render({
content: matchRoute.breadcrumb || curSection,
path: matchRoute.path,
});
}
// 對于routes表中不存在的路徑
// 根目錄默認名稱為首頁.
return render({
content: pathSection === '/' ? '首頁' : curSection,
path: pathSection,
});
};
之后由 render 函式生成最后的單個面包屑導航樣式,單個面包屑組件需要為 render 函式提供該面包屑指向的路徑 path, 以及該面包屑內容映射content 這兩個 props,
/**
*
*/
const render = ({ content, path }) => {
const componentProps = { path };
if (typeof content === 'function') {
return <content {...componentProps} />;
}
return <span {...componentProps}>{content}</span>;
};
有了這些功能函式,我們就能實作一個能為包裹組件傳入當前所在路徑以及路由屬性的 React 高階組件了,傳入一個組件,回傳一個新的相同的組件結構,這樣便不會對組件外的任何功能與操作造成破壞,
const BreadcrumbsHoc = (
location = window.location,
routes = []
) => Component => {
const BreadComponent = (
<Component
breadcrumbs={getBreadcrumbs({
flattenRoutes: flattenRoutes(routes),
location,
})}
/>
);
return BreadComponent;
};
export default BreadcrumbsHoc;
呼叫這個高階組件的方法也非常簡單,只需要傳入當前所在路徑以及整個 react router 生成的 routes 屬性即可,
至于如何取得當前所在路徑,我們可以利用 react router 提供的 withRouter 函式,如何使用請自行查閱相關檔案,
值得一提的是,withRouter 本身就是一個高階組件,能為包裹組件提供包括 location 屬性在內的若干路由屬性,所以這個 API 也能作為學習高階組件一個很好的參考,
withRouter(({ location }) =>
BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
);
4. Q&A
- 如果
react router生成的routes不是由自己手動維護的,甚至都沒有存在本地,而是通過請求拉取到的,存盤在 redux 里,通過react-redux提供的connect高階函式包裹時,路由發生變化時并不會導致該面包屑組件更新,使用方法如下:
function mapStateToProps(state) {
return {
routes: state.routes,
};
}
connect(mapStateToProps)(
withRouter(({ location }) =>
BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
)
);
這其實是 connect 函式的一個bug,因為 react-redux 的 connect 高階組件會為傳入的引陣列件實作 shouldComponentUpdate 這個鉤子函式,導致只有 prop 發生變化時才觸發更新相關的生命周期函式(含 render),而很顯然,我們的 location 物件并沒有作為 prop 傳入該引陣列件,
官方推薦的做法是使用 withRouter 來包裹 connect 的 return value,即
withRouter(
connect(mapStateToProps)(({ location, routes }) =>
BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
)
);
其實我們從這里也可以看出,高階組件同高階函式一樣,不會對組件的型別造成任何更改,因此高階組件就如同鏈式呼叫一樣,可以任意多層包裹來給組件傳入不同的屬性,在正常情況下也可以隨意調換位置,在使用上非常的靈活,這種可插拔特性使得高階組件非常受React生態的青睞,很多開源庫里都能看到這種特性的影子,有空也可以都拿出來分析一下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/47809.html
標籤:JavaScript
上一篇:講講 Promise
