阅读 2284

千万别小瞧九宫格 一道题就能让候选人原形毕露!

前言

据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。

在刷微博和逛朋友圈的时候经常会看到这种东西:

它有一个高大上的名字:九宫格。 顾名思义,九宫格通常为如图这种三行三列的布局。

微信客户端就用到了这种布局方式:

大家最熟悉的朋友圈也采用了九宫格:

还有微博:

它在移动端的运用十分的广泛,而且不仅仅是在移动端的运用,它甚至还运用到了一些面试题中,因为九宫格可以很好的考察面试者的 CSS 功底。

边距九宫格

九宫格通常分为两种,一种是边距九宫格,另一种是边框九宫格。

边距九宫格就是朋友圈那种每张图都带有一定边距的那种:

这种其实反而更简单一些,因为不涉及到边框问题,像这种几行几列的布局用网格布局(grid)简直再合适不过了。

但考虑到大家普遍对网格不太熟悉,所以咱们用同样适合几行几列的表格布局来实现,为什么不用万能的弹性盒子(flex)来做呢?因为下面那道面试题就是用flex实现的,不想用两个一样的布局来实现,为了美观一点,这里使用了一个中文渐变色的库:chinese-gradient,来看代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 在这里用link标签引入中文渐变色 -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chinese-gradient">
  <style>
    /* 清除默认样式 */
    * { padding: 0; margin: 0; }

    /* 全屏显示 */
    html, body, ul { height: 100% }

    /* 父元素 */
    ul {
      /* 给个合适的宽度 */
      width: 100%;

      /* 清除默认样式 */
      list-style: none;

      /* 令其用table方式去显示 */
      display: table;
 
      /* 设置间距 */
      border-spacing: 3px
    }

    /* 子元素 */
    li {
      /* 令其用table-row方式去显示 */
      display: table-row
    }

    /* 孙子元素 */
    div {
      /* 令其用table-cell方式去显示 */
      display: table-cell;

      /* 蓝色渐变 */
      background: var(--湖蓝)
    }
  </style>
</head>
<body>
  <ul>
    <li>
      <div></div>
      <div></div>
      <div></div>
    </li>
    <li>
      <div></div>
      <div></div>
      <div></div>
    </li>
    <li>
      <div></div>
      <div></div>
      <div></div>
    </li>
  </ul>
</body>
</html>
复制代码

运行结果:

可以看到在 DOM 结构上我们并没有用到 <table>、<tr>、<td> 这类传统表格元素,因为在这种情况下只是用到了表格的那种几行几列而已。但实际上九宫格并不是表格,所以为了符合 W3C 的语义化标准,我们采用了其他的 DOM 元素。

在有些适合使用表格布局但又不是表格的情况下,可以利用 display 属性来模仿表格的行为:

  • display: table;相当于把元素的行为变成<table></table>
  • display: inline-table;相当于把元素的行为变成行内元素版的<table></table>
  • display: table-header-group;相当于把元素的行为变成<thead></thead>
  • display: table-row-group;相当于把元素的行为变成<tbody></tbody>
  • display: table-footer-group;相当于把元素的行为变成<tfoot></tfoot>
  • display: table-row;相当于把元素的行为变成<tr></tr>
  • display: table-column-group;相当于把元素的行为变成<colgroup></colgroup>
  • display: table-column;相当于把元素的行为变成<col></col>
  • display: table-cell;相当于把元素的行为变成<td></td><th></th>
  • display: table-caption;相当于把元素的行为变成<caption></caption>

边框九宫格

可能大家看了前面的内容觉得:就这?这么简单还想让人原形毕露?

那咱们来看这么一道题:

要求如下:

  • 边框九宫格的每个格子中的数字都要居中
  • 鼠标经过时边框和数字都要变红
  • 点击九宫格会弹出对应的数字

看起来还是没什么大不了对不对?是不是觉得就是把九宫格加个边框就行了?如果你是这么想的话,那么你写出来的九宫格将会变成这样:

是不是跟想象中的好像不太一样?为什么会这样呢?

因为给每个盒子加入了边框以后,在有边距的情况下看起来都挺正常的,但要将他们合并在一起的话相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框:

那么怎么解决这个问题呢?

解法1

不是相邻的两个边框合并在一起会变粗吗?那么最简单粗暴的办法就是让两个相邻的盒子的其中一个的相邻边不显示边框不就完了!就像这样:

这么做完全可以实现,绝对没毛病。但这种属于笨方法,如果给换成四宫格、六宫格、十二宫格,那么又要重新去想一下该怎么实现,而且写出来的代码也比较冗余,几乎每个盒子都要给它定义一个不同的样式。

如果去参加面试的时候这么实现出来,面试官也不会给你满分,甚至可能连个及格分都不会给。但毕竟算是实现出来了,总比那些没实现出来的强点,不会给零分的。

解法2

上面那种实现方式要给每一个盒子都写一套不同的样式,而且还不适合别的像六宫格、十二宫格这类,代码冗余、可复用性差。

那么怎么才能每个盒子只用到一个样式,并且同样还适用于别的宫格呢?来看看这个思路:

但是仔细一看经不起推敲啊:整个九宫格最右边和最下边的边框都没有了!其实只要咱们在父元素上再加上右侧和下侧的边框即可:

而且并不一定非得是这个方向的,别的方向也可以实现啊,比如酱婶儿的:

酱婶儿的:

还有酱婶儿的:

这种方式不管你是4、6、9还是12宫格,只需在子元素上加一个样式即可,然后再在父元素上加一个互补的边框样式。

解法3

上面那种解法其实已经可以了,但还不是最完美的,那么它都有哪些问题呢?

  • 首先,虽然换成别的宫格也可以复用,但都只适合"满"的情况。比如像朋友圈,最大就是九宫格对吧?但用户可以不是每次都发满九张照片,有可能发7张、有可能发五张,这样的话就会露馅(所以朋友圈采用的是边距九宫格而不是边框九宫格)。

  • 其次,它并不适合这道面试题,因为这道面试题的要求是在鼠标移入时边框变红,而上面那种解法会导致每个盒子的边框都不完整,所以当鼠标移入时效果会变成这样:

那么怎么样才能完美的解出这道题呢?首先每个盒子的边框不能再给它缺斤少两了,但那又会回到最初的那个问题上去:

有的面试题就是这样,在你苦思冥想的时候怎么也想不出来,但是稍微给点思路立马就能明白!

其实就是每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上,我们来写一个粗点的半透明边框演示一下:

中间那些颜色变深了的就是叠在一起的边框,由于是半透明,所以叠在一起时颜色会变深。

不过一些比较细心的朋友可能会纳闷:既然所有盒子都用负边距向左上角移动了,岂不是九宫格不会处在原来的位置上了,没错是这样的!所以我们需要让最左边那一排和最上面那一排不要有负边距,这时候就要考察候选人的CSS水平了,看看他/她能不能够灵活运用伪类选择器:每一行的第一个,应该怎么写?

  • :nth-child(1), :nth-child(4), :nth-child(7)

这样也能实现,不过更好的方式是写成这样:

  • :nth-child(3n+1)

最上面那一排负边距可以不用管,因为如果页面上的九宫格往左边移动了,哪怕只有一两像素,也会导致和页面上的版面无法对齐,而往上移动个一两像素的话谁也看不出来。

但如果要写的话大多数人想的可能是这样:

  • :first-child, :nth-child(2), :nth-child(3)

而更好的方式是这样:

  • :nth-child(-n+3)

每个宫格内的数字要居中,这里推荐用grid,因为九宫格可以用flex去实现,但里面的内容还继续用它去实现的话就体现不出你技术的全面性了,而且在居中这一方面grid可以做到比flex代码更少,即使你对grid不感兴趣,那么只需记住这一固定用法即可:

父元素 {
    display: grid;

    /* 令其子元素居中 */
    place-items: center;
}
复制代码

点击这里查看更多实现居中布局的方式

里面的内容解决了,外面的九宫格咱们来用万能的flex去实现,flex默认是一维布局,但如果仅支持一维的话就不会称之为万能的flex了,思路是这样的,假如每一个宫格宽高为100 x 100,九宫格加起来是300 x 300,每三个就让它换行,这样就可以考察到候选人对flex的灵活运用的程度了:

父元素 {
  width: 300px;

  /* 设置为flex布局 */
  display: flex;

  /* 设置换行 */
  flex-flow: wrap;
}

子元素 {
  width: 100px;
  height: 100px;
  
  border: 1px solid black;
}
复制代码

看起来没毛病对不对?实际上确是每行只有两个宫格就会换行,因为加了边框以后子元素的宽高就变成了102 x 102了,三个的话就已经超过了300,所以还没到三个就开始换行了,这时候就考察到候选人的盒模型了:

子元素 {
  width: 100px;
  height: 100px;
  
  border: 1px solid black;
  
  /* 设置盒模型 */
  box-sizing: border-box;
}
复制代码

这样即使加了边框,宽高也还是100,刚好能满3个就换行,想象一下如果你是面试官,直接问盒模型是不是显得很low,但是就这一个小小的九宫格立马就能区分出这个候选人的水平如何。

再接下来就是鼠标移入时边框和里面的内容一起变红,这有啥难的,不就是:

:hover {
  /* 红色字体 */
  color: red;

  /* 红色边框 */
  border: 1px solid red;
}
复制代码

还是那句话,这样确实能实现,但如果在咱们写js的过程中像red这种多处地方使用的值是不是一般都会给它设置成变量啊?那么这里要写CSS变量?也可以,但有一个更好的变量叫做currentColor,这个属性可以把它理解成一个内置变量,就像js里的innerWidth(window.innerWidth)一样,不用定义自然就是一个变量。

CSS变量不同的是它取的是自身或父元素上的color值,而且它的兼容性还更好,可以一直兼容到IE9

如果你觉得纳闷:这单词这么长,还不如直接写个red多方便啊,那么请别忘了color是可以继承的!如果在一个外层元素中定义了一个颜色,里面的子元素都可以继承,用JS来控制的话只需要获取外层DOM元素然后修改它的color样式即可。

currentColor作为一个变量,可以用在 border、box-shadow、background、linear-gradient() 等一大堆的 CSS 属性上…甚至连svg中的 fill 和 stroke 都可以使用这个变量,它能做的事情很多,这里为了不跑题就先不展开讲,有兴趣的可以去搜一下。

:hover {
  /* 红色字体 */
  color: red;

  /* 红色边框 */
  border: 1px solid;
}
复制代码

修改后的代码如上,为什么没有currentColor?那是因为如果你不写的话,默认就是currentColor,这个关键字代表的就是你当前的color值。

大多数的候选人可能都不会写成这样,如果你作为面试官的话最好是适当的提示一下,看他能不能说出currentColor这个变量或者CSS变量

然后就是点击每个宫格弹出对应的数字,这个考察的是事件冒泡和事件代理:

父元素.addEventListener('click', e => alert(e.target.innerText))
复制代码

你可以观察一下候选人是把事件绑定在父元素上还是一个个的绑定在子元素上,这个问题按理说基本上都不会错。但如果发现候选人一个个把事件绑定在子元素上了,那就可以到此为止了,也不用浪费时间再去问别的问题了,可以十分装B的来一句:行,你的情况我已基本了解了,回去等通知吧!

接下来我们再来写一下完整一点的代码,以便引出下一个问题:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 清除默认样式 */
    * { padding: 0; margin: 0; }

    /* 全屏显示 */
    html, body { height: 100% }

    body {
      /* 网格布局 */
      display: grid;

      /* 子元素居中 */
      place-items: center;
    }

    /* 父元素 */
    ul {
      width: 300px;
      
      /* 清除默认样式 */
      list-style: none;

      /* 设置为flex布局 */
      display: flex;

      /* 设置换行 */
      flex-flow: wrap;
    }

    /* 子元素 */
    li {
      /* 显示为网格布局 */
      display: grid;

      /* 子元素水平垂直居中 */
      place-items: center;

      /* 宽高都是100像素 */
      width: 100px;
      height: 100px;

      /* 设置盒模型 */
      box-sizing: border-box;

      /* 设置1像素的边框 */
      border: 1px solid black;

      /* 负边距 */
      margin: -1px 0 0 -1px;
    }

    /* 第1、4、7个子元素 */
    li:nth-child(3n+1) {
      /* 取消左负边距 */
      margin-left: 0
    }

    /* 前三个子元素 */
    li:nth-child(-n+3) {
      /* 取消上负边距 */
      margin-top: 0
    }

    /* 当鼠标经过时 */
    li:hover {
      /* 红色字体 */
      color: red;

      /* 红色边框 */
      border: 1px solid;
    }
  </style>
</head>
<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
  </ul>
  <script>
    // 选择ul元素
    const ul = document.getElementsByTagName('ul')[0]

    // 监听ul元素的点击事件
    ul.addEventListener('click', e => alert(e.target.innerText))
  </script>
</body>
</html>
复制代码

运行结果:

想知道为什么会这样吗?因为当前这个边框被后面的宫格压住了嘛!那么只需要当鼠标经过时不让后面的压住就好了(调高层级)。

说到调高层级,大家首先想到的可能就是z-index了,这个属性用的最多的地方可能就是绝对定位和固定定位了。但其实很少有人知道,z-index不是只能用在position: xxx的,万能的弹性盒子(display:flex)也是支持z-index的:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 清除默认样式 */
    * { padding: 0; margin: 0; }

    /* 全屏显示 */
    html, body { height: 100% }

    body {
      /* 网格布局 */
      display: grid;

      /* 子元素居中 */
      place-items: center;
    }

    /* 父元素 */
    ul {
      width: 300px;
      
      /* 清除默认样式 */
      list-style: none;

      /* 设置为flex布局 */
      display: flex;

      /* 设置换行 */
      flex-flow: wrap;
    }

    /* 子元素 */
    li {
      /* 显示为网格布局 */
      display: grid;

      /* 子元素水平垂直居中 */
      place-items: center;

      /* 宽高都是100像素 */
      width: 100px;
      height: 100px;

      /* 设置盒模型 */
      box-sizing: border-box;

      /* 设置1像素的边框 */
      border: 1px solid black;

      /* 负边距 */
      margin: -1px 0 0 -1px;
    }

    /* 第1、4、7个子元素 */
    li:nth-child(3n+1) {
      /* 取消左负边距 */
      margin-left: 0
    }

    /* 前三个子元素 */
    li:nth-child(-n+3) {
      /* 取消上负边距 */
      margin-top: 0
    }

    /* 当鼠标经过时 */
    li:hover {
      /* 红色字体 */
      color: red;

      /* 红色边框 */
      border: 1px solid;

      /* 调高层级 */
      z-index: 1;
    }
  </style>
</head>
<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
  </ul>
  <script>
    // 选择ul元素
    const ul = document.getElementsByTagName('ul')[0]

    // 监听ul元素的点击事件
    ul.addEventListener('click', e => alert(e.target.innerText))
  </script>
</body>
</html>
复制代码

运行结果:

结语

没想到这么一个看似不起眼的九宫格一下子就能考察这么多内容吧!如果面试的时候直接问:

  • 你对 flex 了解的怎么样
  • 当元素的外边距为负值时会有什么样的行为
  • 请实现一下水平垂直居中
  • 了解过 grid 吗
  • 谈一下你对盒模型的理解
  • 说一下事件绑定和事件冒泡
  • CSS3的伪类选择器用的怎么样
  • 当页面元素重叠时如何控制哪个在上哪个在下
  • 在CSS中如何运用变量

直接这么问的话既浪费口舌,又显得很low,而且还不能筛选出真正能够灵活运用技术的候选人。

因为这些问题都不难,一般来说都能答出来,但具体能不能灵活运用就不一定了,而这一道九宫格,就像一面照妖镜一样,瞬间让人原形毕露!

如果你是候选人的话,那么一定要好好练习一下这道题。

如果是面试官的话,那么也推荐你用这道题来考察候选者的技术水平,如果能非常完美的做出来,那么基本上就不用再问其他的CSS题目了,日常开发所用到的样式基本难不倒他/她了,可以直接上JS面试题了。

但如果没做出来也不一定就代表这个人水平不行,可以试着提示一下候选者,然后再问一下其他的CSS题来确定一下此人的水平。

该文章首发于前端学不动公众号

往期精彩文章