React造輪系列:Layout 組件思路

本文是React造輪系列第三篇。css

1.React 造輪子系列:Icon 組件思路html

2.React造輪系列:對話框組件 - Dialog 思路前端

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!node

初始化 Layout

參考 And Design ,Layout 組件分別分爲 Layout, Header, Aside, Content,Footer 五個組件。基本使用結構以下:react

<Layout>
  <Header>header</Header>
  <Content>content</Content>
  <Footer>footer</Footer>
</Layout>

假如咱們想直接在 Layout 組件添加 styleclassName 如: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>

再次運行:

clipboard.png

這裏有個問題,實際咱們想要的效果是 Content 內容是要撐開的,因此咱們須要使用 flex 來佈局,自動填寫使用的 flex-grow 屬性:

// lib/layout/layout.scss
.gu-layout {
  border: 1px solid red;
  display: flex;
  flex-direction: column;
  &-content {
    flex-grow: 1;
  }
}

運行效果:

clipboard.png

那若是 Layout 裏面還有 Layout 呢,以下:

<h1>第二個例子</h1>
<Layout style={{height: 500}}>
  <Header>header</Header>
  <Layout>
    <Aside>aside</Aside>
    <Content>content</Content>
  </Layout>
  <Footer>footer</Footer>
</Layout>

運行效果:

clipboard.png

若是嵌套 Layoutcontent 仍是沒有撐開。說明若是 Layout 裏面還有 Layout,那裏面的 Layout 應該佔滿所有。

.gu-layout {
  // 同上
  & & {
    flex-grow: 1;
    border: 1px solid blue;
  }
}

這裏說明一下 & &, & 表示當前的類名,因此就是 & 就是 .gu-layout

運行效果:

clipboard.png

這樣有個問題, 若是 Layout 裏面有 Layout,這個裏面的通常是左右佈局,因此須要設置水平方向爲 row

& & {
    flex-grow: 1;
    border: 1px solid blue;
    flex-direction: row;
  }

運行效果:

clipboard.png

若是想讓 Aside 換到右邊,只須要調整位置便可。

<h1>第三個例子</h1>
<Layout style={{height: 500}}>
  <Header>header</Header>
  <Layout>
    <Content>content</Content>
    <Aside>aside</Aside>
  </Layout>
  <Footer>footer</Footer>
</Layout>

運行效果:

clipboard.png

在來看別外一種佈局:

<h1>第四個例子</h1>
<Layout style={{height: 500}}>
<Aside>aside</Aside>
  <Layout>
  <Header>header</Header>
    <Content>content</Content>
    <Footer>footer</Footer>
  </Layout>
</Layout>

運行效果:

clipboard.png

能夠看到 咱們但願當有 Aside 組件時,須要的是左右佈局,當前的樣式沒法知足,須要再次調整,參考 AntD 設計,當有裏面有 Aside 組件, Layout 就多了一個左右佈局的樣式的 className,因此咱們要在 Layout 組件檢測 children 類型。

實現思路是,能夠先在 Layout 組件內打印 children

clipboard.png

因此我能夠經過遍歷 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
    }
  }
  ...
}

運行效果:

clipboard.png

上述寫法,有些問題,這一個就是使用到了 let 聲明,這們就不符合咱們函數式編程了,第二個 sc 方法還須要進一步改善。

刪除代碼裏的 let

在上述代碼中,咱們使用了一個 let hasAside = false,來判斷 Layout 裏面是否有 Aside,這樣寫就不符合咱們函數式的定義了。

clipboard.png

其實咱們作的是經過遍歷,而後一個一個判斷是否有 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...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索