本文是 React 造轮系列第三篇。
1.React 造轮子系列:Icon 组件思路
2.React 造轮系列:对话框组件 – Dialog 思路
想阅读更多优质文章请猛戳 GitHub 博客, 一年百来篇优质文章等着你!
初始化 Layout
参考 And Design ,Layout 组件分别分为 Layout
, Header
, Aside
, Content
,Footer
五个组件。基本使用结构如下:
<Layout>
<Header>header</Header>
<Content>content</Content>
<Footer>footer</Footer>
</Layout>
假如我们想直接在 Layout
组件添加 style
和 className
如:
<Layout style={{height: 500}} className='hi'>
// 同上
</Layout>
这样写并不支持,我们需要在组件内声明它:
// 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 功能跳转到定义代码块,就能知道类型了。
上面写法看上去没问题,但如果我还想支持 id 或者 src 等 html 原生的属性呢,是不是要一个一个的写呢,当然不是,因为接口是可以继承的,我们直接继承 MapHTMLAttributes
即可:
interface Props extends React.MapHTMLAttributes<HTMLElement>{
}
接下就是使用传入的 style, className:
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
在上述代码中,我们使用了一个 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…
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复 福利,即可看到福利,你懂的。