React 高階組件就是以高階函數的方式包裹須要修飾的 React 組件,並返回處理完成後的 React 組件。React 高階組件在 React 生態中使用的很是頻繁,好比react-router
中的 withRouter
以及 react-redux
中 connect
等許多 API 都是以這樣的方式來實現的。前端
<!-- more -->react
在工做中,咱們常常會有不少功能類似,組件代碼重複的頁面需求,一般咱們能夠經過徹底複製一遍代碼的方式實現功能,可是這樣頁面的維護可維護性就會變得極差,須要對每個頁面裏的相同組件去作更改。所以,咱們能夠將其中共同的部分,好比接受相同的查詢操做結果、組件外同一的標籤包裹等抽離出來,作一個單獨的函數,並傳入不一樣的業務組件做爲子組件參數,而這個函數不會修改子組件,只是經過組合的方式將子組件包裝在容器組件中,是一個無反作用的純函數,從而咱們可以在不改變這些組件邏輯的狀況下將這部分代碼解耦,提高代碼可維護性。redux
前端項目裏,帶連接指向的麪包屑導航十分經常使用,但因爲麪包屑導航須要手動維護一個全部目錄路徑與目錄名映射的數組,而這裏全部的數據咱們都能從 react-router
的路由表中取得,所以咱們能夠從這裏入手,實現一個麪包屑導航的高階組件。數組
首先咱們看看咱們的路由表提供的數據以及目標麪包屑組件所須要的數據: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 提供的數據扁平化成麪包屑導航須要的格式,咱們可使用一個函數來實現它。ui
/** * 以遞歸的方式展平react router數組 */ const flattenRoutes = arr => arr.reduce(function(prev, item) { prev.push(item); return prev.concat( Array.isArray(item.items) ? flattenRoutes(item.items) : item ); }, []);
以後將展平的目錄路徑映射與當前頁面路徑一同放入處理函數,生成麪包屑導航結構。spa
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; };
而後對於每個麪包屑路徑部分,生成目錄名稱並附上指向對應路由位置的連接屬性。code
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) );
若是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 生態的青睞,不少開源庫裏都能看到這種特性的影子,有空也能夠都拿出來分析一下。