手把手教你搭建 React UI 组件库 (五) 实现 layout 组件

2,445 阅读4分钟

1. 目标一: 先定好api

直接搜索查看 antd layout 的api 点击跳转

image.png

image.png

那我们就先定好先做 Layout, Header, Sider, Content, Footer 这些API

2. 目标二: 渲染最简单的layout

  1. 新建文件:@/lib/Layout/layout.tsx:
import * as React from 'react';
import Header from './components/Header/header';
import Content from './components/Content/content';
import Footer from './components/Footer/footer';


interface propsType {

}

const Layout: React.FC<propsType> = ({children}) => {
    return (
        <div>
            Layout
            {children}
        </div>
    );
};

export {Header, Content, Footer}

export default Layout;

新建文件:@/lib/Layout/components/Sider/sider.tsx

import * as React from 'react';

interface propsType {

}

const Sider: React.FC<propsType> = () => {
    return (
        <div>Sider</div>
    );
};
export default Sider;

新建: @/lib/Layout/components/Header/header.tsx

import * as React from 'react';

interface propsType {

}

const Header: React.FC<propsType> = () => {
    return (
        <div>Header</div>
    );
};
export default Header;

新建: @/lib/Layout/components/Footer/footer.tsx

import * as React from 'react';

interface propsType {

}

const Footer: React.FC<propsType> = () => {
    return (
        <div>Footer</div>
    );
};
export default Footer;

新建: @/lib/Layout/components/Content/content.tsx

import * as React from 'react';

interface propsType {

}

const Content: React.FC<propsType> = () => {
    return (
        <div>Content</div>
    );
};
export default Content;

新建: @/lib/Layout/layout.example.tsx

import * as React from 'react';
import Layout, {Content, Footer, Header} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <Layout>
            <Header/>
            <Content/>
            <Footer/>
        </Layout>
    );
};
export default LayoutExample;
  1. 修改:@/lib/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import LayoutExample from './Layout/layout.example';

ReactDOM.render(
    <div>
        <LayoutExample/>
    </div>
    ,
    document.getElementById('root')
)
  1. 查看浏览器:http://localhost:8080/

image.png

我们的目标实现了!

3. 目标三: 让Layout组件能接受ClassName , Style 属性

  1. @/lib/Layout/layout.tsx页面, 查看react的声明

image.png

  1. 在react声明文件中搜索 className

image.png

image.png

  1. 根据名字猜测觉得: HTMLAttributes更适合Layout
  2. 修改@/lib/Layout/layout.tsx:
import * as React from 'react';
import {HTMLAttributes } from 'react';
import classNames from 'classnames';
import Header from './components/Header/header';
import Content from './components/Content/content';
import Footer from './components/Footer/footer';
import {scopedClassMaker} from '../utils';
import './layout.scss'

interface propsType extends HTMLAttributes<HTMLElement>{

}
const sc = scopedClassMaker('sweetui-layout')

const Layout: React.FC<propsType> =
    ({children,
         className,
         ...restProps}) => {
    return (
        <div
            className={classNames(className,sc())}
            {...restProps}
        >
            Layout
            {children}
        </div>
    );
};

export {Header, Content, Footer}

export default Layout;

修改: @/lib/Layout/layout.example.tsx

import * as React from 'react';
import Layout, {Content, Footer, Header} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <Layout className='wm'>
            <Header/>
            <Content/>
            <Footer/>
        </Layout>
    );
};
export default LayoutExample;

新建文件:@/lib/Layout/layout.scss

.sweetui-layout{
  border: 1px red solid;
  width: 100%;
}
  1. 查看浏览器:

image.png

image.png

我们的目标实现了

4. 目标四: 实现 antd 各种例子

4.1 改造Header, Content, Footer, Sider组件, 让他们能够接受ClassName等属性

修改:@/lib/Layout/components/Content/content.tsx

import * as React from 'react';
import {HTMLAttributes} from 'react';
import {scopedClassMaker} from '../../../utils';
import classNames from 'classnames';
import './content.scss'


interface propsType extends HTMLAttributes<HTMLElement> {

}

const sc = scopedClassMaker('sweetui-layout');
const Content: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
        return (
            <div
                className={classNames(className, sc('content'))}
                {...restProps}
            >Content</div>
        );
    };
export default Content;

修改: @/lib/Layout/components/Content/content.scss

.sweetui-layout-content{
  border: 1px blue solid;
  width: 100%;
}

修改@/lib/Layout/components/Footer/footer.tsx

import * as React from 'react';
import {HTMLAttributes} from 'react';
import {scopedClassMaker} from '../../../utils';
import './footer.scss'
import classNames from 'classnames';


interface propsType extends HTMLAttributes<HTMLElement> {

}

const sc = scopedClassMaker('sweetui-layout');
const Footer: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
        return (
            <div
                className={classNames(className, sc('footer'))}
                {...restProps}
            >Footer</div>
        );
    };
export default Footer;

修改: @/lib/Layout/components/Footer/footer.scss

.sweetui-layout-footer{
  border: 1px green solid;
  width: 100%;
}

修改:@/lib/Layout/components/Header/header.tsx

import * as React from 'react';
import {HTMLAttributes} from 'react';
import {scopedClassMaker} from '../../../utils';
import './header.scss'
import classNames from 'classnames';


interface propsType extends HTMLAttributes<HTMLElement> {

}

const sc = scopedClassMaker('sweetui-layout');
const Header: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
        return (
            <div
                className={classNames(className, sc('header'))}
                {...restProps}
            >Header</div>
        );
    };
export default Header;

修改: @/lib/Layout/components/Header/header.scss

.sweetui-layout-header{
  border: 1px pink solid;
  width: 100%;
}

修改: @/lib/Layout/components/Sider/sider.tsx

import * as React from 'react';
import {HTMLAttributes} from 'react';
import {scopedClassMaker} from '../../../utils';
import './sider.scss'
import classNames from 'classnames';

interface propsType extends HTMLAttributes<HTMLElement>{

}

const sc = scopedClassMaker('sweetui-layout');

const Sider: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
    return (
        <div
            className={classNames(className, sc('sider'))}
            {...restProps}
        >Sider</div>
    );
};
export default Sider;

修改:@/lib/Layout/components/Sider/sider.scss

.sweetui-layout-sider{
  border: 1px blueviolet solid;
  width: 100%;
}

4.2 实现第一个例子:

目标:

image.png

image.png

修改: @/lib/Layout/layout.example.tsx

import * as React from 'react';
import Layout, {Content, Footer, Header} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <Layout style={{height: '500px'}}>
            <Header style={{height: '50px'}}/>
            <Content/>
            <Footer style={{height: '50px'}}/>
        </Layout>
    );
};
export default LayoutExample;

修改: @/lib/Layout/layout.tsx

import * as React from 'react';
import {HTMLAttributes} from 'react';
import classNames from 'classnames';
import Header from './components/Header/header';
import Content from './components/Content/content';
import Footer from './components/Footer/footer';
import {scopedClassMaker} from '../utils';
import './layout.scss';

interface propsType extends HTMLAttributes<HTMLElement> {

}

const sc = scopedClassMaker('sweetui-layout');

const Layout: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
        return (
            <div
                className={classNames(className, sc())}
                {...restProps}
            >
                {children}
            </div>
        );
    };

export {Header, Content, Footer};

export default Layout;

修改: @/lib/Layout/layout.scss


.sweetui-layout{
  border: 1px red solid;
  width: 100%;
  display: flex;
  flex-direction: column;

  &-content{
    flex-grow: 1;
  }
}

查看浏览器:

image.png 成功实现!

4.3 实现第二个例子:

目标:

image.png

image.png

修改:@/lib/Layout/layout.example.tsx

import * as React from 'react';
import Layout, {Content, Footer, Header, Sider} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <div>
            <h1>------第一个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Content/>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第二个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Layout>
                    <Sider>Sider</Sider>
                    <Content>Content</Content>
                </Layout>
                <Footer style={{height: '50px'}}/>
            </Layout>
        </div>
    );
};
export default LayoutExample;

修改@/lib/Layout/layout.tsx

import * as React from 'react';
import {HTMLAttributes, ReactElement } from 'react';
import classNames from 'classnames';
import Header from './components/Header/header';
import Content from './components/Content/content';
import Footer from './components/Footer/footer';
import {scopedClassMaker} from '../utils';
import './layout.scss';
import Sider from './components/Sider/sider';

interface propsType extends HTMLAttributes<HTMLElement> {
    children: ReactElement | Array<ReactElement>
}

const sc = scopedClassMaker('sweetui-layout');

const Layout: React.FC<propsType> =
    ({
         children,
         className,
         ...restProps
     }) => {
        let hasSider = false;
       if((children as Array<ReactElement>).length){
           (children as Array<ReactElement>).forEach(item =>{
               if(item.type === Sider){
                   hasSider = true;
               }
           })
       }

        return (
            <div
                className={classNames(className, sc(), hasSider && sc('has-sider'))}
                {...restProps}
            >
                {children}
            </div>
        );
    };

export {Header, Content, Footer, Sider};

export default Layout;

修改: @/lib/Layout/layout.scss

.sweetui-layout{
  border: 1px red solid;
  width: 100%;
  display: flex;
  flex-direction: column;
  flex-grow: 1;

  &-content{
    flex-grow: 1;
  }

 & &-has-sider{
   flex-direction: row;
 }
}

修改: @/lib/Layout/components/Sider/sider.scss

.sweetui-layout-sider{
  border: 1px blueviolet solid;
}

查看浏览器:

image.png

我们成功了!

4.4 实现第三个例子

目标: image.png

image.png

import * as React from 'react';
import Layout, {Content, Footer, Header, Sider} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <div>
            <h1>------第一个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Content/>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第二个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Layout>
                    <Sider>Sider</Sider>
                    <Content>Content</Content>
                </Layout>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第三个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Layout>
                    <Content>Content</Content>
                    <Sider>Sider</Sider>
                </Layout>
                <Footer style={{height: '50px'}}/>
            </Layout>
        </div>
    );
};
export default LayoutExample;

查看浏览器:

image.png

成功实现

4.5 实现第四个例子

image.png

image.png

修改@/lib/Layout/layout.example.tsx

import * as React from 'react';
import Layout, {Content, Footer, Header, Sider} from './layout';

interface propsType {

}

const LayoutExample: React.FC<propsType> = () => {
    return (
        <div>
            <h1>------第一个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Content/>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第二个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Layout>
                    <Sider>Sider</Sider>
                    <Content>Content</Content>
                </Layout>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第三个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Header style={{height: '50px'}}/>
                <Layout>
                    <Content>Content</Content>
                    <Sider>Sider</Sider>
                </Layout>
                <Footer style={{height: '50px'}}/>
            </Layout>

            <h1>------第四个例子------</h1>
            <Layout style={{height: '500px'}}>
                <Sider>Sider</Sider>
                <Layout>
                    <Header style={{height: '50px'}}/>
                    <Content>Content</Content>
                    <Footer style={{height: '50px'}}/>
                </Layout>
            </Layout>
        </div>
    );
};
export default LayoutExample;

修改: @/lib/Layout/layout.scss

.sweetui-layout{
  border: 1px red solid;
  width: 100%;
  display: flex;
  flex-direction: column;
  flex-grow: 1;

  &-content{
    flex-grow: 1;
  }

  &-has-sider{
   flex-direction: row;
 }
}

查看浏览器:

image.png

我们成功了

4.6 代码优化

修改: @/lib/Layout/layout.tsx

        const childrenAsArray = (children as Array<ReactElement>)
        const hasSider = childrenAsArray.length &&
            childrenAsArray.reduce(
                (previousValue, currentNode) =>
                    previousValue|| currentNode.type === Sider
                , false )

image.png