本文是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...
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。