Day 58-59:CSS 重構實作:DOM 佈局空間管理、BEM 命名方法、圖片容器問題

學習背景

重構時間:2026 年 2 月
重構範圍:ZERO TYPE Landing Page (index.html + style.css)
技術限制:尚未使用 Flexbox/Grid,僅使用傳統 Box Model + Position 定位
學習目標:建立可維護、可預測的版面結構與命名系統


Part 1: DOM 佈局與元素空間管理

為什麼需要空間管理原則?

當使用傳統 Box Model 建立版面時,常見問題:

  • 圖片載入前後版面高度跳動

  • 圖片破圖或尺寸過大導致版面崩潰

  • 修改內容後其他元素位置連帶跑掉

  • 不同瀏覽器或載入速度下版面不一致

核心問題:沒有明確定義「誰負責版面空間」、「誰負責填充內容」。


原則 1:父層元素「切出空間」,不靠子元素推擠

問題場景:依賴子元素推擠

<!-- ❌ 錯誤:沒有定義父層高度 -->
<div class="hero">
  <img src="box.png" />
</div>
.hero {
  position: relative;
  /* height 沒設定,由子元素決定 */
}

.hero img {
  width: 600px;
}

執行流程問題:

  1. 瀏覽器遇到 .hero,不知道高度,先畫 0px

  2. 遇到 <img>,等待圖片下載

  3. 圖片下載完成 400px

  4. .hero 被撐高到 400px,整個版面重排(reflow)

結果:版面跳動,使用者體驗差


解決方式:父層明確定義空間

<!-- ✅ 正確:用容器切出空間 -->
<section class="hero">
  <div class="hero__img-box">
    <img class="hero__img" src="box.png" />
  </div>
</section>
/* 父層明確定義版面高度 */
.hero {
  position: relative;
  height: 450px;  /* 明確告訴瀏覽器:這區塊佔 450px */
  margin-bottom: 250px;
}

/* 圖片容器:定義它在父層中的位置和寬度 */
.hero__img-box {
  position: absolute;
  width: 45%;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}

/* 圖片:填滿容器即可 */
.hero__img {
  width: 100%;
  height: 100%;
}

改善後的執行流程:

  1. 瀏覽器遇到 .hero,立刻畫出 450px 高的區塊

  2. 遇到 .hero__img-box,用 absolute 定位在內部

  3. 圖片下載完成,填入容器

  4. 不需要 reflow,版面穩定


<div class="header__logo-zone">
  <div class="header__logo">
    <img class="header__logo-img" src="./public/logo.png" alt="網站 logo" />
  </div>
  <p class="header__logo-text">ZERO TYPE</p>
</div>
/* 父層:定義整個 logo 區域的大小和位置 */
.header__logo-zone {
  display: inline-block;
  margin: 15px 80px 30px 150px;
  text-align: center;
}

/* 圖片容器:切出 60px 寬的空間給 logo */
.header__logo {
  width: 60px;
  margin: 0 auto;
}

/* 圖片:填滿固定空間 */
.header__logo-img {
  width: 100%;
  height: auto;
}

空間結構圖:

┌─ .header__logo-zone ──────────────────┐
│  [上 padding 15px]                     │
│                                        │
│  ┌─ .header__logo (60px) ───────┐   │
│  │   <img> 填滿這個 60px 的空間   │   │
│  └────────────────────────────────┘   │
│                                        │
│  ZERO TYPE (文字)                      │
└────────────────────────────────────────┘

關鍵思考:

  • logo-zone:控制整個區域的 margin 和對齊

  • logo:切出圖片固定的 60px 空間

  • img:只負責填滿那 60px


原則 2:子元素視覺與尺寸都不超過父元素

問題場景:子元素超出父層

<div class="container">
  <img src="large-image.png" />
</div>
.container {
  width: 300px;
}

.container img {
  /* 沒有設定 width,圖片以原始尺寸顯示 */
  /* 假設原始尺寸是 800px,會超出 container */
}

問題視覺化:

┌─ .container (300px) ──┐
│                        │
│ ┌─ <img> 800px ────────────────────────┐
│ │                      │                │
│ └──────────────────────┘────────────────┘
└────────────────────────┘
          ↑
     超出 500px

解決方式:子元素尺寸相對於父層

.container {
  width: 300px;
}

.container img {
  width: 100%;     /* 永遠不會超過 300px */
  height: auto;    /* 保持圖片比例 */
}

執行結果:

┌─ .container (300px) ──┐
│ ┌─ <img> (300px) ───┐ │
│ │   圖片被縮小到     │ │
│ │   符合容器寬度     │ │
│ └────────────────────┘ │
└────────────────────────┘

實際案例:Hero 主視覺圖片

版面需求:

  • Hero 區塊總高度 450px

  • 左側文字佔 50% 寬度

  • 右側圖片佔 45% 寬度

  • 圖片垂直置中

/* 第一層:父層定義整體空間 */
.hero {
  position: relative;
  height: 450px;
  margin-bottom: 250px;
}

/* 第二層:圖片容器定義圖片應佔的空間 */
.hero__img-box {
  position: absolute;
  width: 45%;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}

/* 第三層:圖片填滿容器 */
.hero__img {
  width: 100%;
  height: 100%;
}

空間結構圖:

┌─ .hero (height: 450px) ────────────────────────┐
│                                                 │
│  ┌─ .hero__intro ──┐  ┌─ .hero__img-box ────┐│
│  │ (width: 50%)    │  │ (width: 45%)         ││
│  │ IDEAS?          │  │  ┌─ <img> ────────┐ ││
│  │ This is just... │  │  │   填滿容器      │ ││
│  │ [TRY IT NOW!]   │  │  │   100% × 100%  │ ││
│  └─────────────────┘  │  └─────────────────┘ ││
│                       └──────────────────────┘│
└─────────────────────────────────────────────────┘

原則 3:圖片等空元素用 <div> 包裹,避免破圖跑版

核心概念:Replaced Element 的特殊性

<img> 是 replaced element(置換元素),它的特性: developer.mozilla

  • 內容由外部資源決定(圖片檔案)

  • 尺寸預設由圖片原始大小決定

  • 載入狀態會影響版面(載入前/後/失敗)

其他 replaced elements:<video>, <iframe>, <embed>, <object> github


問題場景:直接對圖片定位

<!-- ❌ 錯誤:直接對 img 使用 absolute -->
<div class="hero">
  <img class="hero__img" src="box.png" />
</div>
.hero {
  position: relative;
  /* 沒有定義高度 */
}

.hero__img {
  position: absolute;
  width: 45%;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

問題 1:圖片載入前

┌─ .hero (高度 0px) ─┐
│                     │  ← 沒設定高度,且 img 還沒載入
└─────────────────────┘
下方內容緊貼著

問題 2:圖片載入後

┌─ .hero (被撐高到 300px) ─────┐
│         ┌─ img (300px) ─────┐│
│         │                    ││
│         └────────────────────┘│
└───────────────────────────────┘
下方內容被推下 300px (版面跳動!)

問題 3:圖片載入失敗

┌─ .hero (高度又變回 0px) ─┐
│ [破圖 icon]              │
└──────────────────────────┘
版面又跳回去 (再次跳動!)

解決方式:用 div 切出空間,圖片填充

<!-- ✅ 正確:三層結構 -->
<div class="hero">                    <!-- 1. 版面骨架 -->
  <div class="hero__img-box">         <!-- 2. 圖片空間 -->
    <img class="hero__img" src="box.png" />  <!-- 3. 圖片內容 -->
  </div>
</div>
/* 第一層:版面骨架 */
.hero {
  position: relative;
  height: 450px;
}

/* 第二層:圖片空間切割 */
.hero__img-box {
  position: absolute;
  width: 45%;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}

/* 第三層:圖片填充 */
.hero__img {
  width: 100%;
  height: 100%;
}

載入流程分析:

圖片載入前/後/失敗都穩定:

┌─ .hero (height: 450px) ───────────────┐
│                                        │
│  ┌─ .hero__img-box ─────────────────┐│
│  │  [載入中 / 顯示圖片 / 破圖 icon] ││
│  │  但 .hero 始終保持 450px 高度    ││
│  └───────────────────────────────────┘│
│                                        │
└────────────────────────────────────────┘
版面大小不變,沒有跳動

為什麼要分三層?責任分離原則

層級 1:.hero(版面骨架層)

責任:定義整個區塊在頁面中的空間

.hero {
  position: relative;  /* 成為子元素定位的參考點 */
  height: 450px;       /* 佔據固定高度,穩定版面 */
  margin-bottom: 250px;
}

層級 2:.hero__img-box(空間切割層)

責任:在骨架內部切出「圖片應該佔的位置和大小」

.hero__img-box {
  position: absolute;
  width: 45%;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}

層級 3:.hero__img(內容填充層)

責任:把圖片內容填入預留的空間

.hero__img {
  width: 100%;   /* 填滿父層 */
  height: 100%;
}

Part 2: CSS 命名結構重構 - BEM 方法

BEM 核心概念

BEM = Block__Element--Modifier getbem

Block__Element--Modifier
  • Block(區塊):獨立的頁面元件

    • 例:.header, .hero, .footer

  • Element(元素):Block 的組成部分,用 __ 連接 hackmd

    • 例:.header__logo, .header__nav, .footer__copyright

  • Modifier(修飾符):Block 或 Element 的不同狀態,用 -- 連接 hackmd

    • 例:.button--disabled, .nav-item--active


判斷是否需要加 class 的流程

這個元素需要 CSS 樣式嗎?
  ├─ 是 → 加 class
  └─ 否
      └─ 這個元素需要 JS 操作嗎?
          ├─ 是 → 加 class
          └─ 否 → 不用加 class(純語意容器)

Block 還是 Element?決策流程

這個元件會在多個地方重用嗎?
  ├─ 是 → 獨立成 Block (.button, .card, .modal)
  └─ 否 → 當作 Element (.hero__button, .footer__social-links)

實際重構案例 1:Header 區塊

Before:

<header>
  <div class="logo">
    <div class="logo-img"><img src="..." /></div>
    <p class="logo-text">ZERO TYPE</p>
  </div>
  <nav>
    <ul class="header-nav">
      <li><span class="home-btn">Home</span></li>
header { ... }               /* ❌ tag selector */
.logo { ... }                /* ❌ 沒有明確從屬關係 */
header nav { ... }           /* ❌ 後代選擇器 */
.header-nav li { ... }       /* ❌ 選擇器層級過深 */

After:

<header class="header">
  <div class="header__logo-zone">
    <div class="header__logo">
      <img class="header__logo-img" src="..." />
    </div>
    <p class="header__logo-text">ZERO TYPE</p>
  </div>
  <nav>
    <ul class="header__nav">
      <li class="header__nav-item">Home</li>
.header { ... }
.header__logo-zone { ... }
.header__logo { ... }
.header__logo-img { ... }
.header__nav-item { ... }
.header__nav-item:hover { color: orange; }

改善重點:

  • 移除 tag selector,全部用 class

  • 移除後代選擇器 .logo-img img

  • 用 __ 明確表達從屬關係 hackmd


Modifier 的正確用法

錯誤寫法:

<li class="header__nav-home">Home</li>
<li class="header__nav-list">Feature</li>

這樣看起來像兩個不同的元素,而非同一元素的不同狀態。

正確寫法:

<li class="header__nav-item header__nav-item--active">Home</li>
<li class="header__nav-item">Feature</li>
.header__nav-item {
  color: gray;
  list-style-type: none;
  display: inline-block;
  margin-right: 100px;
}

.header__nav-item--active {
  color: orange;
}

為什麼要這樣寫: hackmd

  • 所有選單項目共用 .header__nav-item 的基礎樣式

  • 只有被選中的項目額外加上 --active 的顏色

  • 未來修改「所有選單」或「選中狀態」都只改一個 class


重構案例 2:Hero 區塊(複雜結構)

Before:

<section class="hero">
  <div class="hero-intro">
    <h1>IDEAS?</h1>
    <h2>This is just a placeholder.</h2>
    <button class="try-it-btn">
      <h2>TRY IT NOW!</h2>
    </button>
  </div>
  <div class="hero-img">
    <img src="..." />
  </div>
</section>
section.hero .hero-intro h1 { ... }  /* ❌ 4層選擇器 */
.hero-img img { ... }                /* ❌ 後代選擇器 */

After:

<section class="hero">
  <div class="hero__intro">
    <h1 class="hero__title">IDEAS?</h1>
    <h2 class="hero__subtitle">This is just a placeholder.</h2>
    <p class="hero__text">...</p>
    <div class="hero__try-it">
      <button class="hero__try-it-btn">TRY IT NOW!</button>
      <p class="hero__try-it-hint">Don't worry it's for free.</p>
    </div>
  </div>
  <div class="hero__img-box">
    <img class="hero__img" src="..." />
  </div>
</section>
.hero__title { font-size: 50px; ... }
.hero__subtitle { font-size: 30px; ... }
.hero__try-it-btn { ... }
.hero__img { width: 100%; height: 100%; }

改善重點:

  1. 所有標題標籤都加上語意化 class

  2. 按鈕從獨立 Block 改為 .hero__try-it

  3. 圖片容器和圖片分別命名

  4. 移除所有 4 層以上的選擇器 hackmd


重構前後對比

重構前

重構後

section.hero .hero-intro h1

.hero__title

footer .social-media-link li img

.footer__social-media-icon

.header-nav li span

.header__nav-item

權重降低、可讀性提升 valoremreply


Part 3: 解決圖片超出 <a> 容器的問題

問題描述

在 footer 的社群媒體圖示區塊,<img> 被 <a> 標籤包住,但 <a> 容器的高度比 <img> 還小,導致圖片超出容器範圍。

原因分析

  • <img> 預設是 inline 元素,會受文字排版規則影響 developer.mozilla

  • Inline 元素的對齊基準是 baseline,下方會有預留空隙 cnblogs

  • 當 <img> 設定了 width 但 <a> 容器沒有明確高度時,容器可能無法正確被撐開

視覺問題:

┌─ <a> 容器 ──────┐
│                  │
│ ┌─ <img> ─────┐ │
│ │              │ │
│ │              │ │  ← 圖片底部超出
│ └──────────────┘ │
│   ↓ baseline 預留空隙
└──────────────────┘

解決方案

.footer__social-media-icon {
  width: 30px;
  display: block;  /* 將圖片改為 block 元素 */
}

為什麼這樣有效?

  1. display: block 讓 <img> 從 inline 變成 block 元素 developer.mozilla

  2. Block 元素會正常撐開父容器的高度

  3. 不再受 baseline 和 line-height 等文字排版規則干擾 cnblogs

  4. <a> 容器的尺寸會完全由 <img> 決定

修正後:

┌─ <a> 容器 ──────┐
│ ┌─ <img> ─────┐ │
│ │              │ │
│ │              │ │
│ └──────────────┘ │  ← 完美包住
└──────────────────┘

適用場景

  • 圖示按鈕(<a> 或 <button> 裡只有圖片)

  • 純圖片連結,需要容器完全包住圖片

  • 任何「父容器應該被子元素圖片完整撐開」的情境


常見問題 Q&A

Q1: 什麼時候一定要用三層結構?

必須用三層(骨架 > 容器 > 圖片):

  • 圖片是外部資源(網路圖片、使用者上傳)

  • 圖片尺寸不確定

  • 需要複雜定位(absolute, transform)

  • 需要響應式調整

可以簡化成兩層:

  • 圖片尺寸固定且可控(如 icon)

  • 簡單的 block 排列

  • 不需要複雜定位


Q2: 為什麼 .hero__img-box 不用設定 height?

因為 .hero__img 設定了 height: 100%,形成:

  • .hero__img-box 的高度由內部 .hero__img 決定

  • .hero__img 的高度是 100%(相對於 .hero__img-box)

這看起來是「循環參照」,但實際上:

  • .hero__img 的原始高度(由圖片檔案決定)會先算出

  • .hero__img-box 被撐開到這個高度

  • .hero__img 再填滿 .hero__img-box

結果:圖片保持原始比例,同時填滿容器


Q3: BEM 的 Element 可以嵌套嗎?

不建議超過兩層 __: hackmd

/* ❌ 不要這樣 */
.header__nav__item__link { ... }

/* ✅ 應該這樣 */
.header__nav-item { ... }
.header__nav-link { ... }

BEM 建議扁平化命名,即使 HTML 結構有多層嵌套,CSS class 也應該保持簡單。


Q4: 為什麼不能只用兩層(.hero + .hero__img)?

如果省略中間層:

.hero__img {
  position: absolute;
  width: 45%;
  height: 100%;  /* = 450px,固定寬高比 */
  right: 0;
}

問題:

  • width: 45% + height: 100% = 寬高比固定

  • 如果圖片原始比例不是 45:100,會變形

加上 .hero__img-box 中間層:

  • 圖片比例自適應

  • 不會變形

  • 版面骨架穩定


BEM 重構檢查清單

HTML 檢查

  • [ ] 每個需要樣式的元素都有 class

  • [ ] Block 名稱獨立且語意清楚

  • [ ] Element 用 __ 連接

  • [ ] Modifier 用 -- 連接

  • [ ] 沒有混用 - 和 __

CSS 檢查

  • [ ] 沒有 tag selector(除了 reset)

  • [ ] 沒有後代選擇器超過 2 層

  • [ ] 每個 class 可以獨立解讀

  • [ ] HTML 和 CSS 的 class 名稱完全對應

  • [ ] 同一 Block 的所有 Element 都用相同前綴


重構成果總結

空間管理改善

  • 從依賴子元素推擠 → 父層明確定義空間

  • 從子元素可能超出 → 子元素相對父層尺寸

  • 從直接定位圖片 → 三層結構穩定版面

命名系統改善

  • 從 4 層選擇器 → 單一 class

  • 從混亂命名 → 系統化 BEM 命名

  • 從難以重用 → 模組化結構

可維護性提升

  • 版面穩定,不會因圖片載入而跳動

  • 命名可預測,看 class 就知道位置

  • 選擇器權重降低,避免樣式衝突 valoremreply


延伸學習資源

官方文件

實務指南


下一步行動

  1. 複習三個原則,找出你目前程式碼中還沒套用的地方

  2. 試著改寫一個區塊,用三層結構重構

  3. 學習 Flexbox,從 MDN 的 Flexbox Guide 開始

  4. 記錄「為什麼」,不只是「怎麼做」


筆記日期:2026-02-12
學習情境:ZERO TYPE Landing Page 完整重構
技術標籤:#CSS #BoxModel #BEM #ReplacedElements #LayoutManagement
重構結果: 完整符合 BEM 規範,版面穩定可維護