三剑客

传说在前端界有这么三剑客——HTML、CSS、JavaScript。

HTML

HTML——无非就是一些标签,配上一些属性,算不上编程语言,所以可能很多人对其是看不上眼的,小学生都会,但其实HTML只是深藏不露。如果它使出“语义化”大概很多人,包括很多工作多年的前端er们都要“死”在它的刀下。更别说都9012年了还div/span一把梭的还大有人在。虽然关于HTML的语义化也值得学习研究和讨论。不过这篇文章主旨并非在此。

JavaScript

作为一门真正的编程语言,虽然其具有被饱受诟病的一些“特性”,但也不妨它成为当今被广泛运用的语言,而且JS也确实在进步和改善。而今的JS已经今非昔比,到处“踢馆”,拳打后端,脚踢移动端,桌面端也展露头角,结果如何暂且不论,这份勇气着实可嘉。以至于"江湖"流传这么一句话:

JS在江湖的地位可见一斑。虽然JS很受欢迎,但它依然不是今天的重点。

CSS

虽然JS非常强大,很受欢迎,但大概会是男粉居多,女生应该会更青睐CSS,作为三剑客中最会“打扮”的一位,CSS可比亚洲四大邪术厉害多了,HTML本来是“奇丑无比”的,但有了CSS的加持,就可以变得“千姿百态”。
CSS虽然有许多的化妆工具来美化HTML的脸,但是这些工具又不像现实中妹纸们使用的——粉扑、眉笔、睫毛刷...是单一作用的,有时为了一个效果需要许多属性同时应用,所以CSS是非正交的。因为是非正交的,所以常会有很多难以理解的现象出现,用不好的话这个妆就毁了。

CSS VS JS

相信很多前端er们对自己的JS能力是很有自信的,能够熟练运用甚至精通,但大概是没人敢说精通CSS,可见CSS的难度其实不亚于JS,其实两者没什么比较性,说到底CSS也不是编程语言,只是因为其非正交的特点,使得其有许多难以理解的现象,同时也有很多出人意料的神奇之处。笔者对于两者也都是一知半解的程度,写这篇文章也是因为确实碰到这么一个需求,但对于这其中运用的技巧自认为可能对一些人会有帮助,故而在此献丑。如表述有误,或有何待改进的地方也欢迎批评指正。

需求

那么究竟是怎样一个需求呢?正所谓“有图有真相”,那么就来看看最终设计稿是何面貌吧。

这是一些社交类APP或者是一些内容型的APP可能会出现的场景,无非就是像微信朋友圈那样的一条消息,业务场景是很常见的,但是,既然是图文的展示,势必涉及到布局的问题,我们重点关注下其中的图片即可。九宫格图片是非常常见的一种图片布局,但“千不怕万不怕就怕设计有想法”,从上面图片中也可以看出,九宫格可以说对应了九种不同的样式。读者可以先尝试着针对这样的设计稿思考该如何实现。可能很多开发者接到这样一个设计稿的时候会选择通过使用JS修改相应位置图片的class来实现。加之现在三大框架的流行,使得DOM的修改更加的方便,不论是React的JSX或者Vue的模版语法都非常方便。也许是太过于较真,笔者总认为关于布局/样式之类的实现就应该是CSS干的事,JS不应该参与其中,就好像那句话一样:

也好比现今常被提及的MV*理论所述那样,HTML、CSS、JavaScript有各自的权责范围,“貌美如花”的活就应该让CSS单独承受。故而,当笔者接到这样一个设计稿的时候,就在思考如何让三剑客们都专注于自己分内的事。
重申一遍,这个需求的重点在于九宫格,故而其余部分请自行忽略。

实现

HTML

首先,HTML负责的是页面的骨架,理论上来说DOM越简单越好,如果抛开样式布局,无非就是有几张图就展示几张。HTML只需要把这些图片一个个排列即可。于是,HTML的结构应该是这样的:

<div>
 <img src="" />
 <!-- img并排显示 -->
</div>

这应该是最理想的DOM结构了,没有多余的元素。

CSS

接下来就是重头戏了,如何在上面这样的结构下,仅仅运用CSS就完美还原设计稿呢?我们先给元素class以便书写css。

<div>
 <img class="grid-img" src="" />
 <!-- img并排显示 -->
</div>

我们给img一个class——grid-img,之后的所有样式布局将会只使用这一个class来定义。整个九宫格的实现不会有第二个class。而且整个实现过程不会有JS的参与。看到这里也许有的读者会有疑问,如果不需要第二个class,又没有JS的参与到底应该如何实现不同张数图片时不同位置上的图片大小、圆角这些样式的设置呢?接下来,就是见证奇迹的时刻。
在揭秘魔术谜底之前先让大家看看魔术的成果:
纯CSS实现九宫格DEMO

揭秘

在动手写代码之前我们必须得知道我们究竟是要实现怎样一个效果,要达到什么目的。于是,我们需要理解需求,需要将其文字需求转化为代码语言。就像我们在学校学习如何解数学应用题时那样,将题目信息抽取关键的要点,并转译成数学公式进行解答。那么我们再来看看需求究竟都写了什么。

这里的截图只显示出了不同张数图片的差异点,但最为重要的关于图片的尺寸信息没有展示,所以这里笔者用文字稍作整理如下【2倍稿尺寸】:

  1. 一张图
  • 宽高:320*320;
  • 圆角:10 10 10 10;
  1. 两张图
  • 宽高:332*332;
  • 第一张圆角:10 0 0 10;
  • 第二张圆角:0 10 10 0;
  • 间距:6
  1. 三张图
  • 宽高:220*220;
  • 第一张圆角:10 0 0 10;
  • 第二张圆角:0;
  • 第三张圆角:0 10 10 0;
  • 间距:5;
  1. 四张图
  • 宽高:220*220;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0 10 0 0;
  • 第三张圆角:0 0 0 10;
  • 第四张圆角:0 0 10 0;
  • 间距:5;
  1. 五张图
  • 1~3宽高:220*220;
  • 4~5宽高:332*332;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0;
  • 第三张圆角:0 10 0 0;
  • 第四张圆角:0 0 0 10;
  • 第五张圆角:0 0 10 0;
  • 1~3图片间距:5;
  • 4~5图片间距:6;
  1. 六张图
  • 宽高:220*220;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0;
  • 第三张圆角:0 10 0 0;
  • 第四张圆角:0 0 0 10;
  • 第五张圆角:0;
  • 第六张圆角:0 0 10 0;
  • 间距:5;
  1. 七张图
  • 宽高:220*220;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0;
  • 第三张圆角:0 10 0 0;
  • 第四张圆角:0;
  • 第五张圆角:0;
  • 第六张圆角:0 0 10 0;
  • 第七张圆角:0 0 0 10;
  • 间距:5;
  1. 八张图
  • 1~6宽高:220*220;
  • 7~8宽高:332*332;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0;
  • 第三张圆角:0 10 0 0;
  • 第四张圆角:0;
  • 第五张圆角:0;
  • 第六张圆角:0;
  • 第七张圆角:0 0 0 10;
  • 第八张圆角:0 0 10 0;
  • 1~6图片间距:5;
  • 7~8图片间距:6;
  1. 九张图
  • 宽高:220*220;
  • 第一张圆角:10 0 0 0;
  • 第二张圆角:0;
  • 第三张圆角:0 10 0 0;
  • 第四张圆角:0;
  • 第五张圆角:0;
  • 第六张圆角:0;
  • 第七张圆角:0 0 0 10;
  • 第八张圆角:0;
  • 第九张圆角:0 0 10 0;
  • 间距:5;

以上便是九种情况对应的样式,相信很多人看到如此繁冗的差异样式很迷茫该如何只用CSS就实现这么多差异的样式,更何况是只有一个class。谜底先按下不表,这份整理看上去很复杂,实际上这是因为这里列出的是所有不同的样式,如果我们只着眼于其中一个样式差异再来看的话又会怎么样呢?比如我们先看看宽高:

  1. 一张图
  • 宽高:320*320;
  1. 两张图
  • 宽高:332*332;
  1. 三张图
  • 宽高:220*220;
  1. 四张图
  • 宽高:220*220;
  1. 五张图
  • 1~3宽高:220*220;
  • 4~5宽高:332*332;
  1. 六张图
  • 宽高:220*220;
  1. 七张图
  • 宽高:220*220;
  1. 八张图
  • 1~6宽高:220*220;
  • 7~8宽高:332*332;
  1. 九张图
  • 宽高:220*220;

有没有感觉瞬间简单了许多?如果就此需求用一个class来实现呢?读者可以尝试着思考下。
其实,看到像类似这种不同情况对应不同的值时,不知大家脑海中是否会浮现一个想法就是——这里面应该有某种规律。这有好比是当出现这样一组数字时“1、1、2、3、5、8、13、21、34”难免心生其中隐藏着某种规律的想法。但初看上面的情况,似乎还看不太出来有什么明显规律。我们不妨再继续对宽高的情况进一步整理,把同值的情况列出:

  1. 220*220——3,4,5(1~3),6,7,8(1~6),9
  2. 320*320——1
  3. 332*332——2,5(4~5),8(7~8)

突然之间,情况减少至三种了,现在大家有发现一些猫腻了吗?通过这样一步步的整理,最后我们发现,其中的规律是:

  1. 当图片大于2张时,宽高为220,其中的例外是5,8,也就是当图片最后一张余两张的时候,最后两张的宽高为332
  2. 只有一张的时候图片宽高为320

总结出规律后,我们就要思考如何将其转换成CSS了,而这也是本篇文章的重点之处,也是实现这个需求的难点之处。
这其中的难点在于——如何知道图片大于2?如何知道图片只有1、5、8张?只有解决了这两个问题,才有办法设置对应情况中的图片的大小。说到要选择一组元素中的第几个,大家应该都能想到就是用:nth-child/:nth-of-type这两个伪类选择器,这两个选择器有一点区别,但这不是本篇重点,不了解的同学可自行查阅文档。

:nth-child

我们都知道:nth-child可以选择某一个元素,甚至可以使用an+b这样的表达式来选择符合表达式的第n个元素,于是关于大于2这个问题我们很自然的想到就是:

.grid-img:nth-child(n + 3) {
    width: 220px;
    height: 220px;
}

欸~,等等,好像哪里不太对。这里只是选择2以后的元素,而需求是图片大于2张时的情况下所有元素都是220*220。这么看来nth-child似乎也不能解决的说。不,就是用nth-child的,只是还需要再更加灵活变通一点。我们不妨先略过这条,先从比较简单的第二条入手:

对于这样一个需求,CSS该如何实现呢?经过上面的错误尝试之后,相信不会再有人还以为是nth-child吧?也许有人会转念一想会不会是:first-last?不过再细想发现first-child不就是nth-child(1)嘛。笔者就不再卖关子了,答案是同为伪类选择器的:only-child。这个选择器可能很多读者没用过,甚至可能没听过,它的作用就跟它的名字一样正好完美契合我们的需求——只有一张图片。于是CSS就可以写成下面这样:

.grid-img:only-child {
    width: 320px;
    height: 320px;
}

如果读者去翻看MDN文档,会发现:only-child其实也可以写成:first-child:last-child,然后你可能还会发现——伪类选择器竟然还可以连着写?伪类选择器很常用,但像这样连着用的不会很经常,所以可能有的人还不知道可以这么用,伪类连着写就像集合两个集合取交集的意思,即:first-child:last-child意思是——既是第一个子元素又是最后一个元素——翻译过来就是只有一个元素的情况。至此,我们第二条的情况实现了。那么,有了这么一番发现和尝试后,我们再回过头来看第一条就会豁然开朗许多。

我们可以看到大多数情况图片都是220,只有几个例外情况,我们完全可以将图片默认为220,然后针对几个特殊情况进行重置处理即可,不需要每个情况都单独列出。这其中的例外是5,8张图时最后两张图为332,在知道了选择器的交集作用之后我们就可以通过这个技巧解决这个问题:

// 3n + 1表示每行的首个,两个伪类串联表示取交集
// 故这里表示该项不仅是行首且同时是倒数第二个,下行同理
:nth-child(3n + 1):nth-last-child(2),
:nth-child(3n + 2):last-child {
    width: 332px;
    height: 332px;
}

最终,我们实现不同情况宽高的代码为【scss】:

.grid-img {
    // 大多数情况是220。以大多数值为初值。
    width: 220px;
    height: 220px;
    // 当只有一个时,宽高为320,各边均有圆角。
    &:only-child {
        width: 320px;
        height: 320px;
        border-radius: 10px;
    }
    // 3n + 1表示每行的首个,两个伪类串联表示取交集
    // 故这里表示该项不仅是行首且同时是倒数第二个,下行同理
    &:nth-child(3n + 1):nth-last-child(2),
    &:nth-child(3n + 2):last-child {
        width: 332px;
        height: 332px;
    }
}

至此,魔术的谜底已经揭晓,看似并非是什么神奇的技巧,只是一个可能不被大多数人所熟悉的特性而已。包括另外的圆角、间距也都是运用这个技巧来实现的,故而笔者就不再赘述了。

完整代码

.grid-img {
  display: inline-block;
  // 大多数情况是220。以大多数值为初值。
  width: 220px;
  height: 220px;
  // 当只有一个时,宽高为320,各边均有圆角。
  &:only-child {
    width: 320px;
    height: 320px;
    border-radius: 10px;
  }
  // 3n + 1表示每行的首个,两个伪类串联表示取交集
  // 故这里表示该项不仅是行首且同时是倒数第二个,下行同理
  &:nth-child(3n + 1):nth-last-child(2),
  &:nth-child(3n + 2):last-child {
    width: 332px;
    height: 332px;
  }
  // 每行的第二个左右都有5px的margin,大多数的元素是220,加上margin,则最终容器最宽为220*3+5*2=670
  &:nth-child(3n + 2) {
    margin: 0 5px;
    // 例外的是当最后一行是两个元素时,宽为332,所以margin=670-332*2=6
    &:last-child {
      margin-left: 6px;
    }
  }
  // 从第二行开始有上边距
  &:nth-child(n + 4) {
    margin-top: 5px;
  }
  // 第一个元素固定有左上圆角
  &:first-child {
    border-top-left-radius: 10px;
  }
  // 最后一个元素肯定没有右边距,且有右下圆角
  &:last-child {
    margin-right: 0;
    border-bottom-right-radius: 10px;
  }
  // 最后一行的行首元素有左下圆角
  // 这里也可以枚举,第1,4,7个元素
  // 但这里要注意的是当元素只有四个时,布局不一样,需要单独处理这种情况,具体可看下面的该情况处理
  &:nth-child(3n + 1) {
    &:last-child,
    &:nth-last-child(2),
    &:nth-last-child(3) {
      border-bottom-left-radius: 10px;
    }
  }
  // 右上圆角只有最右上的元素有,实际上就是第一行的最后一个元素
  // 因为只有一个时在上面已经处理了,这里只需再列出剩余的两种情况
  &:nth-child(2):last-child,
  &:nth-child(3) {
    border-top-right-radius: 10px;
  }
  // 当元素只有4个时,布局是2x2的,有别于其他情况,这里需要特别处理
  // 为了换行,让第二个元素右边距大一点,以便于挤占空间让3,4跑到下一行
  // 然后这里是第二个元素的右上有圆角
  &:nth-child(2):nth-last-child(3) {
    margin-right: 220px;
    border-top-right-radius: 10px;
  }
  // 只有4个时,第三个元素因为符合之前的一些通用情况设置了margain和圆角,这里需要重置这些样式
  &:nth-child(3):nth-last-child(2) {
    margin-top: 5px;
    margin-right: 5px;
    border-radius: 0 0 0 10px;
  }
  // 这里同样也是处理4个布局时最后一个元素的样式
  &:nth-child(4):last-child {
    border-radius: 0 0 10px 0;
  }
}
03-05 18:12