什么是栅格(Grid)
定义
首先,栅格是一个设计概念,并且是一个比较经典的平面设计概念,摘一段维基百科的描述:
栅格设计系统(又称网格设计系统、标准尺寸系统、程序版面设计、瑞士平面设计风格、国际主义平面设计风格),是一种平面设计的方法与风格。运用固定的格子设计版面布局,其风格工整简洁,在二战后大受欢迎,已成为今日出版物设计的主流风格之一。
1629年,法王路易十四命令成立一个管理印刷的皇家特别委员会,由数学家尼古拉斯·加宗(Nicolas Jaugeon)担任领导。委员会提出了新字体设计建议:以罗马体为基础,采用方格为设计依据,每个字体方格分为64个基本方格单位,每个方格单位再分成36小格,这样,一个印刷版面就由2304个小格组成。这是世上最早对字体和版面进行科学实验的活动。也是栅格系统的雏形。
20世纪50年代,栅格设计系统终于在前西德与瑞士得到完善。通过瑞士平面设计杂志的宣传,将瑞士苏黎士和巴塞尔两个城市的设计家从20世纪40年代探索的成果全面展示,并影响世界各国,因此也被称为“瑞士平面设计风格”(Swiss Design)。由于这种风格简单明确,传达功能准确,因而很快得到世界范围内的普遍认可,成为战后影响最大的一种平面设计风格,也是国际最流行的风格,因此又被称为“国际主义平面设计风格”(International Typographic Style)。
通俗的理解,栅格布局就是以网格的方式来实现二维排版布局的方式,如下图所示,就是经典的栅格设计风格:
组成部分
网格原子单位
从微观角度来看,栅格系统会把可视区域划分为一系列规格一致的小网格,这些网格会辅助设计师更规范的排版和布局,这些小网格也是整个栅格系统的最小单位,需要注意的是,对于研发来说一般不会注意到这个网格原子单位的存在。
以AntDesign为代表的大部分设计语言会把网格原子单位定为8,为什么会定为8而不是其他的数字呢?
如果以4、6、8、10、12作为栅格的原子单位,可以看到目前主流屏幕分辨率和它们的整除关系如下图,可以看出来4是整除率最高的单位,但是4作为原子单位实在是太小,增减看起来差别并不明显,所以在整除率和合适之间寻求一个平衡,选择8作为栅格的原子单位,这也就解释了为什么AntDesign的Grid组件要使用(16+8n)px来作为栅格间隔水槽。
列(Column)和水槽(Gutter)
上面讲到了栅格的网格原子单位,但是在使用中我们会把整个可视区域划分为若干列,我们会直接声明某个内容区域在横向占了多少列来标识整个内容区域的宽度。
通常我们使用的组件库中的Grid组件会直接把可视区域划分为12列或者24列,那为什么是12和24呢?我还特意查了一下这个问题,得到的解答(知乎)是:
“
因为12是1,2,3,4,6的最小公倍数,所以12列栅格系统相对较灵活,支持将一行分成1列,2列,3列,4列,6列。若是想要支持5列,那1,2,3,4,5的最小公倍数是60,而60这个数对于栅格系统来说显然太大了。18能均分4列不?24能做的12都能做,所以12是最好的选择。
”
水槽是相邻两个列宽之间的间隔,用来规范页面中内容间的间距,水槽的值越大,页面中留白部分的面积越多,视觉效果越松散,反之,页面越紧凑。水槽通常设置为定值。
React组件实现
文章中只是部分代码,完整代码地址:github.com/erdong-fe/t…
为了探究React组件对于Grid的实现,我研读了Antd对于Grid的源码。
React组件对于Grid的实现,关键在于使用一维的Flex布局来模拟二维的效果,它分拆出了两个组件,分别是Row和Col,来实现行和列的摆放布局,所以对于React Grid的研究,重点在于研究Row和Col这两个组件。
Row
Row组件最大的作用在于创建一个Flex布局的Dom容器,并且接收行相关的参数,为了简单起见,我只实现Row接收gutter参数,Row的参数定义如下:
interface RowProps extends React.HTMLAttributes<HTMLDivElement> { gutter?: number } 复制代码
还有一个问题需要注意,比如对于下面这个Grid布局来说,列与列之间有gutter,这个比较好实现,设置列左右的margin就行了,但是对于最左侧和最右侧的列来说,它们的margin-left和margin-right是多余的,所以我们需要在Row的代码里面做调整
调整代码如下:
function Row(props: RowProps) { // ... const rowStyle: React.CSSProperties = {}; if (gutter && gutter > 0) { rowStyle.marginLeft = gutter / -2; rowStyle.marginRight = gutter / -2; } // ... } 复制代码
剩下的就是实现Row代码结构里包含Col、把Row接受的参数透传给Col组件以及相关的样式代码即可,完整代码如下:
/** RowContext文件 **/ import { createContext, Context } from 'react'; export interface RowContextState { gutter?: number } const RowContext: Context<RowContextState> = createContext({}); export default RowContext; /** row文件 **/ import React from 'react'; import RowContext from './RowContext'; import './style.scss'; interface RowProps extends React.HTMLAttributes<HTMLDivElement> { gutter?: number } function Row(props: RowProps) { const { gutter = 0, children } = props; const rowStyle: React.CSSProperties = {}; if (gutter && gutter > 0) { rowStyle.marginLeft = gutter / -2; rowStyle.marginRight = gutter / -2; } const rowContext = React.useMemo(() => ({ gutter }), [gutter]) return ( <RowContext.Provider value={rowContext}> <div className="row" style={{...rowStyle}}> { children } </div> </RowContext.Provider> ) } /** style.scss **/ .row { display: flex; } 复制代码
Col
列组件实现要点是:
读取自身的span参数,并且根据24等分实现自身宽度
从Row组件中读取gutter参数,并且把它变成相应的“水槽”宽度
首先实现读取span参数和实现自身宽度:
/** style.scss **/ @for $index from 1 to 24 { .col-#{$index} { flex: 0 0 percentage($number: $index / $grid-columns); } } .col { box-sizing: border-box; } /** Col文件 **/ interface ColProps extends React.HTMLAttributes<HTMLDivElement> { span: number } function Col(props: ColProps) { const { children, span } = props; const classObj = { [`col-${span}`]: span !== void 0 } const classes = classNames('col', classObj); return ( <div className={classes}> { children } </div> ) } 复制代码
然后实现从Row组件中读取gutter参数,并且把它变成相应的“水槽”宽度,完整代码如下:
import React, { useContext, CSSProperties } from 'react'; import classNames from 'classnames'; import RowContext from './RowContext'; import './style.scss'; interface ColProps extends React.HTMLAttributes<HTMLDivElement> { span: number } function Col(props: ColProps) { const { children, span } = props; const classObj = { [`col-${span}`]: span !== void 0 } const classes = classNames('col', classObj); const { gutter } = useContext(RowContext); const styleObj: CSSProperties = {}; if (gutter && gutter > 0) { const horizontalGutter = gutter / 2; styleObj.paddingLeft = horizontalGutter; styleObj.paddingRight = horizontalGutter; } return ( <div className={classes} style={{...styleObj}}> { children } </div> ) }
总结
前端组件里面除了代码实现以外,也有很多设计思想的体现,理解设计思想或许比单纯会实现代码更有意义