1. 首页

React造轮系列:Layout 组件思路

本文是React造轮系列第三篇。

1.React 造轮子系列:Icon 组件思路

2.React造轮系列:对话框组件 – Dialog 思路

初始化 Layout

参考 And Design ,Layout 组件分别分为 Layout, Header, Aside, Content,Footer 五个组件。基本使用结构如下:


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

假如我们想直接在 Layout 组件添加 styleclassName 如:


<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>

再次运行:

码农进阶题库,每天一道面试题 or Js小知识

这里有个问题,实际我们想要的效果是 Content 内容是要撑开的,所以我们需要使用 flex 来布局,自动填写使用的 flex-grow 属性:


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

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

那如果 Layout 里面还有 Layout 呢,如下:


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

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

如果嵌套 Layoutcontent 还是没有撑开。说明如果 Layout 里面还有 Layout,那里面的 Layout 应该占满全部。


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

这里说明一下 & &, & 表示当前的类名,所以就是 & 就是 .gu-layout

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

这样有个问题, 如果 Layout 里面有 Layout,这个里面的一般是左右布局,所以需要设置水平方向为 row


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

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

如果想让 Aside 换到右边,只需要调整位置即可。


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

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

在来看别外一种布局:


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

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

可以看到 我们希望当有 Aside 组件时,需要的是左右布局,当前的样式无法满足,需要再次调整,参考 AntD 设计,当有里面有 Aside 组件, Layout 就多了一个左右布局的样式的 className,所以我们要在 Layout 组件检测 children 类型。

实现思路是,可以先在 Layout 组件内打印 children

码农进阶题库,每天一道面试题 or Js小知识

所以我可以通过遍历 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 } } ... }

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

上述写法,有些问题,这一个就是使用到了 let 声明,这们就不符合我们函数式编程了,第二个 sc 方法还需要进一步改善。

删除代码里的 let

在上述代码中,我们使用了一个 let hasAside = false,来判断 Layout 里面是否有 Aside,这样写就不符合我们函数式的定义了。

码农进阶题库,每天一道面试题 or Js小知识

其实我们做的是通过遍历,然后一个一个判断是否有 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 组件相对简单,这边主要介绍一些实现思路,源码已经到这里

作者:前端小智
链接:https://segmentfault.com/a/1190000019331920

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React造轮系列:Layout 组件思路

链接:https://www.javascriptc.com/3373.html

« JavaScript 中几种轻松处理’this’指向方式
简洁时尚 UI 的 7条规则(二)»
Flutter 中文教程资源

相关推荐

QR code