什麼是 React 高階元件?聊聊怎麼使用React高階元件創建一個麵包屑導航,希望對大家有幫助!
#React 高階元件就是以高階函數的方式包覆需要修飾的React 元件,並傳回處理完成後的React 元件。 React 高階元件在React 生態中使用的非常頻繁,例如react-router
中的withRouter
以及react-redux
中connect
等許多API 都是以這樣的方式來實現的。
在工作中,我們常常會有很多功能相似,元件程式碼重複的頁面需求,通常我們可以透過完全複製一遍程式碼的方式實現功能,但是這樣頁面的維護可維護性就會變得極差,需要對每個頁面裡的相同元件去做更改。因此,我們可以將其中共同的部分,例如接受相同的查詢操作結果、組件外同一的標籤包裹等抽離出來,做一個單獨的函數,並傳入不同的業務組件作為子組件參數,而這個函數不會修改子元件,只是透過組合的方式將子元件包裝在容器元件中,是一個無副作用的純函數,從而我們能夠在不改變這些元件邏輯的情況下將這部分程式碼解耦,提升程式碼可維護性。
前端專案裡,帶連結指向的麵包屑導航十分常用,但由於麵包屑導航需要手動維護一個所有目錄路徑與目錄名映射的數組,而這裡所有的資料我們都能從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) );
如果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生态的青睐,很多开源库里都能看到这种特性的影子,有空也可以都拿出来分析一下。
以上是什麼是React高階元件?怎麼它創建一個麵包屑導航?的詳細內容。更多資訊請關注PHP中文網其他相關文章!