2KB实现任意等分支持responsive的栅格系统(GRID)
基本上所有的UI库里都会有个栅格系统,比如bootstrap的12等分,purecss的24等分,阿里妈妈的64等分等等。数字越大,处理的粒度自会更细一些,适用的场景也会更多。但是即使24等分这样约数很多的系统,依然不可避免的有一些盲区,比如5等分,7等分。
为了适应更多的场景,purecss在24等分的基础上额外的提供了5等分的方案。然而,这带来了另外一个问题。
作为一个现代的栅格系统,不支持responsive是说不过去的,一般都会提供手机、平板、桌面、宽屏4种屏幕宽度的适配,每种宽度都要定义24等分的class,这样下来purecss的grid压缩后差不多10KB左右大小,而整个purecss的其它代码加一起也不到20KB。并且这仅仅是grid的部分,如果加上purecss没有实现的offset,体积又要增加一倍。对于非常轻量化的UI库来说,这实在是一个庞大的包袱。
动态生成style
那么有没有办法让整个库更小一些呢?实际上大部分的UI库栅格系统都是打包的时候通过less的mixins或者直接是js生成的,那么可不可以把这个生成的过程放到浏览器来做呢?于是有了第一个版本:
// 提取公约数,实现 rct-u-1-3 这样的别名调用
function getGcd(m, n) {
let u = m, v = n
while (v !== 0) {
[u, v] = [v, u % v]
}
return u
}
// 生成24等分栅格
function gridUnit(pre, responsive) {
responsive = responsive ? responsive + '-' : ''
let text = [], width
for (let i = 1; i <= 24; i++) {
let gcd = getGcd(i, 24)
width = (i * 100 / 24).toFixed(6)
text.push(`.${pre}-${responsive}${i}-24`)
if (gcd > 1) {
text.push(`,.${pre}-${responsive}` + (i / gcd) + '-' + (24 / gcd))
}
text.push(`{width:${width}%;}`)
}
for (let i = 1; i <= 5; i++) {
width = (i * 20).toFixed(6)
text.push(`.${pre}-${responsive}${i}-5{width:${width}%;}`)
}
return text.join('')
}
export function create (pre = 'rct-g') {
let style = document.createElement('style')
let text = []
style.type = 'text/css'
text.push(`
.${pre} {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
}`)
text.push(`.${pre}-1{width:100%}`)
text.push(gridUnit(pre))
; [['35.5', 'sm'], ['48', 'md'], ['64', 'lg'], ['80', 'xl']].forEach(([x, m]) => {
text.push(`@media screen and (min-width: ${x}em) {`)
text.push(gridUnit(pre, m))
text.push('}')
})
style.innerHTML = text.join('')
document.head.appendChild(style)
}
功能很简单,就是在页面加载的时候,生成一个和purecss的Grid一样的style内容,插到head里去。页面里用到的时候,就像普通的grid一样使用就好了。
<div class="rct-g">
<div class="rct-u-1-3"><p>Thirds</p></div>
<div class="rct-u-1-3"><p>Thirds</p></div>
<div class="rct-u-1-3"><p>Thirds</p></div>
</div>
而文件的大小,从10多KB一下变成1KB不到。
任意等分的栅格
体积的问题解决了,还有另一个问题,如果需要5等分,7等分,甚至13等分的系统怎么办呢?再生成5等分,7等分的css?如果要1024等分呢……
既然动态了,就动态的彻底一些吧。
如果要做任意等分,那么在页面加载的时候生成一个完整的style内容肯定是不可能了,那么只有按需生成,需要什么grid的时候就生成一个。另外,让使用者记住类似'rct-u-md-1-3'这样的class也不是什么好选择。于是就变成了这样:
const GRIDS = {};
const RESPONSIVE = {
'sm': '568',
'md': '768',
'lg': '992',
'xl': '1200'
};
module.exports = function getGrid(width, responsive) {
let gridClass = generate(width, responsive);
return `${gridPre} ${gridPre}-1 ${gridClass}`;
}
function generate(width, responsive) {
if (!width || width <= 0) {
return '';
}
width = (width * 100).toFixed(4);
width = width.substr(0, width.length - 1);
responsive = responsive || defaultResponsive;
let key = responsive + '-' + width.replace('.', '-');
if (!GRIDS[key]) {
generateGrid(width, key, responsive);
}
return `${gridPre}-${key}`;
}
function generateGrid(width, key, responsive) {
GRIDS[key] = true;
let minWidth = RESPONSIVE[responsive];
let text = `@media screen and (min-width: ${minWidth}px) { .${gridPre}-${key}{width: ${width}%} }`;
let style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = text
document.head.appendChild(style);
}
当我们需要一个grid的时候,调用getGrid这个方法,width是一个分数,比如1/2, 2/5, 4/8,计算数值乘以100得到一个截断小数点后4位得到一个宽度,这个宽度既作为class的宽度值,再加上前缀变为className,生成style插入head。为了防止重复插入,使用了一个全局变量GRIDS来记录生成的class。getGrid会返回这个className,妈妈再也不怕我写错className啦。
不过这个方式也会有个副作用,就是每生成一个grid,就会插入一个style标签。尝试过只用一个标签来维护,就是每生成一个grid,把之前的style值取出来,加上当前生成的style内容,再塞回去。测试下来,性能很不理想,性能比直接插入style相差100倍左右。另外,尝试过延迟批量生成style的方式,发现只延迟50ms都不可接受。事实上,插入style的速度非常快,生成从1到100等分栅格,共5050个grid的style标签,chrome下测试大约100ms。实际应用中,一个页面不同布局的grid不会太多,可以忽略不计。
然后,可以再完善一下,加入offset。
export function getGrid(options) {
if (!options) {
return '';
}
let { width, offset, responsive } = options;
let gridClass = generate(width, 'grid', responsive);
let offsetClass = generate(offset, 'offset', responsive);
return `${gridPre} ${gridPre}-1 ${gridClass} ${offsetClass}`;
}
function generate(width, type, responsive) {
if (!width || width <= 0) {
return '';
}
if (width > 1) { width = 1; }
width = (width * 100).toFixed(4);
width = width.substr(0, width.length - 1);
responsive = responsive || defaultResponsive;
let key = responsive + '-' + width.replace('.', '-');
if (type === 'grid') {
if (!GRIDS[key]) {
generateGrid(width, key, responsive);
}
return `${gridPre}-${key}`;
} else {
if (!OFFSETS[key]) {
generateOffset(width, key, responsive);
}
return `${offsetPre}-${key}`;
}
}
function generateGrid(width, key, responsive) {
GRIDS[key] = true;
let minWidth = RESPONSIVE[responsive];
let text = `@media screen and (min-width: ${minWidth}em) { .${gridPre}-${key}{width: ${width}%} }`;
createStyle(text);
}
function generateOffset(width, key, responsive) {
OFFSETS[key] = true;
let minWidth = RESPONSIVE[responsive];
let text = `@media screen and (min-width: ${minWidth}em) { .${offsetPre}-${key}{margin-left: ${width}%} }`;
createStyle(text);
}
function createStyle(text) {
let style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = text
document.head.appendChild(style);
}
完整代码见这里。
写一个React Grid
方法有了,就实际使用下把,写个React的组件,其实很简单。
import { Component, PropTypes } from 'react';
import { getGrid } from './utils/grids';
class Grid extends Component {
render () {
let { width, offset, responsive, style, children } = this.props;
let className = getGrid({ width, offset, responsive });
return (
<div style={style} className={className}>
{children}
</div>
);
}
}
Grid.propTypes = {
children: PropTypes.any,
offset: PropTypes.number,
responsive: PropTypes.string,
style: PropTypes.object,
width: PropTypes.number
};
module.exports = Grid;
使用的时候,这样
<Grid width={ 1/2 }>
<Grid width={ 1/3 }>1/3</Grid>
<Grid width={ 2/3 }>2/3</Grid>
</Grid>
结果
<div class="rct-grid rct-grid-1 rct-grid-md-50-000 ">
<div class="rct-grid rct-grid-1 rct-grid-md-33-333 ">1/3</div>
<div class="rct-grid rct-grid-1 rct-grid-md-66-666 ">2/3</div>
</div>
在线的demo