千萬別小瞧九宮格 一道題就能讓候選人原形畢露!

語言: CN / TW / HK

前言

據不完全統計(其實就統計了自己身邊的朋友和同事),在刨除抖音或快手這一類短視訊 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)

最上面那一排負邊距可以不用管,因為如果頁面上的九宮格往左邊移動了,哪怕只有一兩畫素,也會導致和頁面上的版面無法對齊,而往上移動個一兩畫素的話誰也看不出來。

每個宮格內的數字要居中,這裡推薦用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:first-child, li:nth-child(2), li:nth-child(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:first-child, li:nth-child(2), li:nth-child(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題來確定一下此人的水平。

該文章首發於前端學不動公眾號

往期精彩文章