如何彈性地抽象化網站樣板的UI?

Failed

完整程式碼:Flexible Layout | CodePenopen_in_new

使用情境

#

同一個網站裡,每一頁總會有一部份的元素是共用的,例如Navigation、Header、Footer、Sidebar等,通常我們會將這些重複的UI抽象化成樣板元件,雖然這樣做已經大幅提升元件的復用性,卻未必能保留充足的彈性。真實的產品面對不同的需求,可能會需要實作各種Layout的頁面:

  • 有Header也有Sidebar
  • 有Sticky Header但不要Sidebar
  • 有Sticky Header與Sticky Secondary Navigation還要有Sidebar
  • ...

甚至要支援透過互動來改變Layout:

  • 點擊全螢幕按鈕時隱藏Sidebar
  • 向上捲動時顯示Navigation,向下捲動時則隱藏
  • ...

Layout Component

#

為了因應不同需求,我們可以將需要復用的元件()集中在Layout元件()裡:

const Navigation = () => (
<ul>
<li><Link to="/page/1">Page One</Link></li>
<li><Link to="/page/2">Page Two</Link></li>
<li><Link to="/page/3">Page Three</Link></li>
</ul>
)
const Footer = () => (
<footer>
<hr />
This page has footer.
</footer>
)
const AppLayout = ({ navigation, footer, children }) => (
<>
<Navigation />
{children}
{footer && <Footer />}
</>
)
AppLayout.propTypes = {
navigation: PropTypes.bool,
footer: PropTypes.bool,
}
AppLayout.defaultProps = {
navigation: true,
footer: false,
}

在需要套用樣板的頁面就可以直接將當作root component來使用,如果想為特定頁面調整Layout,只需要改變傳入的props即可:

const PageOne = () => (
<AppLayout footer>
This is a page wrapped with layout component.
</AppLayout>
)
const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/page/1" component={PageOne} />
<Route component={PageOne} />
</Switch>
</BrowserRouter>
)

使用HOC擴充彈性

#

如果每一個頁面的Layout不會隨著使用者的互動而改變,也就是各頁面裡的prop始終不變的話,我們其實可以將其提升為HOC

const withAppLayout = layoutProps => WrappedComponent => pageProps => (
<AppLayout {...layoutProps}>
<WrappedComponent {...pageProps} />
</AppLayout>
)

使用方式可以在page level:

const PageTwo = withAppLayout({ footer: true })(() => (
<>
This is a page wrapped with page-level layout HOC.
</>
))
const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/page/1" component={PageOne} />
<Route exact path="/page/2" component={PageTwo} />
<Route component={PageOne} />
</Switch>
</BrowserRouter>
)

甚至可以在route level:

const PageThree = () => (
<>
This is a page wrapped with route-level layout HOC.
</>
)
const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/page/1" component={PageOne} />
<Route exact path="/page/2" component={PageTwo} />
<Route exact path="/page/3" component={withAppLayout({ footer: true })(PageThree)} />
<Route component={PageOne} />
</Switch>
</BrowserRouter>
)

技術總結

#

Layout的實作方式可以透過Component的抽象,也可以透過簡單的封裝提供HOC的抽象,不僅與保持一致,提高復用性,甚至可以在路由的層級就決定好每一個頁面的Layout,方便集中管理,偶爾遇到需要動態Layout的頁面也能隨時改用元件,傳入不同的props就能根據使用者的行為做出千變萬化的Layout了。