本文是React造輪系列第三篇。css
1.React 造輪子系列:Icon 組件思路html
2.React造輪系列:對話框組件 - Dialog 思路前端
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!node
參考 And Design ,Layout 組件分別分爲 Layout
, Header
, Aside
, Content
,Footer
五個組件。基本使用結構以下:react
<Layout> <Header>header</Header> <Content>content</Content> <Footer>footer</Footer> </Layout>
假如咱們想直接在 Layout
組件添加 style
和 className
如:git
<Layout style={{height: 500}} className='hi'> // 同上 </Layout>
這樣寫並不支持,咱們須要在組件內聲明它:es6
// lib/layout/layout.tsx interface Props { style: CSSProperties, className: string } const Layout: React.FunctionComponent<Props> = (props) => { return ( <div className={sc()}> {props.children} </div> ) }
注意這個 style
是一個 CSSProperties,若是不知道 style 是什麼類型的,這邊有間技巧就是在正常 div
上寫 style
,而後經過 IDE 功能跳轉到定義代碼塊,就能知道類型了。github
上面寫法看上去沒問題,但若是我還想支持 id 或者 src 等 html 原生的屬性呢,是否是要一個一個的寫呢,固然不是,由於接口是能夠繼承的,咱們直接繼承 MapHTMLAttributes
便可:編程
interface Props extends React.MapHTMLAttributes<HTMLElement>{ }
接下就是使用傳入的 style, className:segmentfault
const Layout: React.FunctionComponent<Props> = (props) => { const {className, ...rest} = props return ( <div className={sc(''), className} {...rest}> {props.children} </div> ) }
這裏的 sc
是作第一個輪子的時候封裝,對應的方法以下:
function scopedClassMaker(prefix: string) { return function x(name?: string) { const result = [prefix, name].filter(Boolean).join('-'); return [result, options && options.extra].filter(Boolean).join(' ') }; } export {scopedClassMaker};
從上述的實現方式,能夠發現問題,若是咱們直接在組件內寫 className={sc(''), className}
, 咱們經過 sc
方法生成的函數會被傳入的 className
覆蓋。因此須要就 sc 方法進一步驟改造,擴展傳入 className
,實現方式以下:
interface Options { extra: string | undefined } function scopedClassMaker(prefix: string) { return function x(name?: string, options ?:Options ) { const result = [prefix, name].filter(Boolean).join('-'); if (options && options.extra) { return [result, options && options.extra].filter(Boolean).join(' ') } else { return result; } }; } export {scopedClassMaker};
若是懂 Es6 閱讀如下代碼應該很容易,這裏就一在詳細講了。
而後調用方式以下:
// lib/layout/layout.tsx ... const Layout: React.FunctionComponent<Props> = (props) => { const {className, ...rest} = props return ( <div className={sc('', {extra: className})} {...rest}> {props.children} </div> ) } ...
在回顧一下,開始的結構:
//lib/layout/layout.example.tsx <Layout> <Header>header</Header> <Content>content</Content> <Footer>footer</Footer> </Layout>
再次運行:
這裏有個問題,實際咱們想要的效果是 Content 內容是要撐開的,因此咱們須要使用 flex
來佈局,自動填寫使用的 flex-grow
屬性:
// lib/layout/layout.scss .gu-layout { border: 1px solid red; display: flex; flex-direction: column; &-content { flex-grow: 1; } }
運行效果:
那若是 Layout
裏面還有 Layout
呢,以下:
<h1>第二個例子</h1> <Layout style={{height: 500}}> <Header>header</Header> <Layout> <Aside>aside</Aside> <Content>content</Content> </Layout> <Footer>footer</Footer> </Layout>
運行效果:
若是嵌套 Layout
,content
仍是沒有撐開。說明若是 Layout 裏面還有 Layout,那裏面的 Layout 應該佔滿所有。
.gu-layout { // 同上 & & { flex-grow: 1; border: 1px solid blue; } }
這裏說明一下 & &
, & 表示當前的類名,因此就是 & 就是 .gu-layout
。
運行效果:
這樣有個問題, 若是 Layout 裏面有 Layout
,這個裏面的通常是左右佈局,因此須要設置水平方向爲 row
& & { flex-grow: 1; border: 1px solid blue; flex-direction: row; }
運行效果:
若是想讓 Aside 換到右邊,只須要調整位置便可。
<h1>第三個例子</h1> <Layout style={{height: 500}}> <Header>header</Header> <Layout> <Content>content</Content> <Aside>aside</Aside> </Layout> <Footer>footer</Footer> </Layout>
運行效果:
在來看別外一種佈局:
<h1>第四個例子</h1> <Layout style={{height: 500}}> <Aside>aside</Aside> <Layout> <Header>header</Header> <Content>content</Content> <Footer>footer</Footer> </Layout> </Layout>
運行效果:
能夠看到 咱們但願當有 Aside
組件時,須要的是左右佈局,當前的樣式沒法知足,須要再次調整,參考 AntD 設計,當有裏面有 Aside
組件, Layout 就多了一個左右佈局的樣式的 className
,因此咱們要在 Layout 組件檢測 children
類型。
實現思路是,能夠先在 Layout 組件內打印 children
:
因此我能夠經過遍歷 children
來判斷,實現以下:
props.children.map(node => { console.log(node) })
這邊不能直接使用 map,由於 children 的類型有5種, ReactChild
, ReactFragment
,ReactPortal
,boolean
, null
, undefined
,因此這裏須要對 children 進行約束,至少要有一個元素。
// lib/layout/layout.tsx interface Props extends React.MapHTMLAttributes<HTMLElement>{ children: ReactElement | Array<ReactElement> } const Layout: React.FunctionComponent<Props> = (props) => { const {className, ...rest} = props let hasAside = false if ((props.children as Array<ReactElement>).length) { (props.children as Array<ReactElement>).map(node => { if (node.type === Aside) { hasAside = true } }) } return ( <div className={sc('', {extra: [className, hasAside && 'hasAside'].join(' ')})} {...rest}> {props.children} </div> ) } export default Layout
添加對應的 css:
.gu-layout { ... &.hasAside { flex-direction: row; .gu-layout{ flex-direction: column } } ... }
運行效果:
上述寫法,有些問題,這一個就是使用到了 let
聲明,這們就不符合咱們函數式編程了,第二個 sc
方法還須要進一步改善。
在上述代碼中,咱們使用了一個 let hasAside = false
,來判斷 Layout
裏面是否有 Aside
,這樣寫就不符合咱們函數式的定義了。
其實咱們作的是經過遍歷,而後一個一個判斷是否有 Aside ,若是有剛設置爲 true
, 從上圖能夠看出,咱們最後能夠把全部判斷結果 或(|)
起來,若是爲 true
,則有,不然無。這時候咱們就可使用 es6 新引入的 reduce
方法了。
// lib/layout/layout.tsx ... const Layout: React.FunctionComponent<Props> = (props) => { const {className, ...rest} = props if ((props.children as Array<ReactElement>).length) { const hasAside = (props.children as Array<ReactElement>) .reduce((result, node) => result || node.type === Aside, false) } return ( <div className={sc('', {extra: [className, hasAside && 'hasAside'].join(' ')})} {...rest}> {props.children} </div> ) } ...
經過 reduce 改進後的方法有個問題,咱們 hasAside
是在 if
塊域裏面的,外部訪問不到,那有沒有什麼辦法刪除 {}
塊做用域呢?
咱們把把 if
條件經過 &&
放到跟遍歷同一級:
// lib/layout/layout.tsx ... const children = props.children as Array<ReactElement> const hasAside = ( children.length) && children.reduce((result, node) => result || node.type === Aside, false) ...
Layout 組件相對簡單,這邊主要介紹一些實現思路,源碼已經到這裏。
參考
《方應杭老師的React造輪子課程》
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
https://github.com/qq44924588...
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。