系列文章: 前端工程師的 Modern Web 實踐之道 - Day 12
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆
在前一篇文章中,我們探討了 API 設計與前端整合的最佳實踐。今天我們將深入探討現代響應式設計的革命性技術:Container Queries,以及其他現代化布局技術。這些技術將徹底改變我們設計響應式介面的方式。
還在為一個組件在側邊欄和主內容區表現不一致而煩惱嗎?每次調整布局都要修改一堆 Media Queries?今天我們來聊聊如何用現代技術徹底解決這些問題。
/* 傳統做法:基於視窗寬度 */
.card {
  display: flex;
  flex-direction: column;
}
@media (min-width: 768px) {
  .card {
    flex-direction: row;
  }
}
/* 問題:
   - 無法知道卡片實際的可用空間
   - 在側邊欄中可能需要垂直排列
   - 在主內容區可能需要水平排列
   - 需要為每個使用場景寫不同的規則
*/
實際場景的痛點:
<!-- 同一個 Card 組件在不同容器中 -->
<div class="sidebar">     <!-- 寬度 300px -->
  <div class="card">...</div>
</div>
<div class="main-content"> <!-- 寬度 900px -->
  <div class="card">...</div>
</div>
<!-- 傳統方案需要:
  1. 為 sidebar 內的 card 添加特殊類名
  2. 或使用複雜的選擇器
  3. 或寫大量條件判斷的 CSS
-->
/* 現代做法:基於容器寬度 */
.card-container {
  container-type: inline-size;
  container-name: card;
}
.card {
  display: flex;
  flex-direction: column;
}
/* 當容器寬度 >= 500px 時 */
@container card (min-width: 500px) {
  .card {
    flex-direction: row;
  }
}
/* 優勢:
   - 組件根據自身容器調整
   - 完全可複用
   - 不依賴頁面布局
   - 真正的組件化響應式設計
*/
Grid 的強大之處:
/* 傳統做法需要大量計算和嵌套 */
.traditional-layout {
  /* 複雜的 float、position 組合 */
}
/* Grid 一行搞定 */
.modern-layout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
}
/* 智能間距分配 */
.flex-layout {
  display: flex;
  gap: 1rem;          /* 現代間距控制 */
  flex-wrap: wrap;    /* 自動換行 */
}
.flex-item {
  flex: 1 1 300px;    /* 彈性基準 300px */
}
讓我們打造一個真正智能的卡片組件,能根據容器寬度自動調整布局:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Container Queries 實戰</title>
  <style>
    /* 1. 基礎樣式重置 */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      line-height: 1.6;
      padding: 2rem;
      background: #f5f5f5;
    }
    /* 2. 定義容器查詢 */
    .card-wrapper {
      container-type: inline-size;
      container-name: card-container;
      margin-bottom: 2rem;
    }
    /* 3. 卡片基礎樣式 */
    .smart-card {
      background: white;
      border-radius: 8px;
      overflow: hidden;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      transition: all 0.3s ease;
    }
    .smart-card:hover {
      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
      transform: translateY(-2px);
    }
    /* 4. 內容布局 - 預設垂直 */
    .card-content {
      display: flex;
      flex-direction: column;
    }
    .card-image {
      width: 100%;
      height: 200px;
      object-fit: cover;
    }
    .card-body {
      padding: 1.5rem;
    }
    .card-title {
      font-size: 1.25rem;
      font-weight: 600;
      margin-bottom: 0.5rem;
      color: #333;
    }
    .card-description {
      color: #666;
      margin-bottom: 1rem;
    }
    .card-meta {
      display: flex;
      gap: 1rem;
      font-size: 0.875rem;
      color: #999;
    }
    /* 5. Container Query: 小容器 (300px+) */
    @container card-container (min-width: 300px) {
      .card-title {
        font-size: 1.125rem;
      }
    }
    /* 6. Container Query: 中等容器 (500px+) - 改為水平布局 */
    @container card-container (min-width: 500px) {
      .card-content {
        flex-direction: row;
      }
      .card-image {
        width: 40%;
        height: auto;
        min-height: 250px;
      }
      .card-body {
        width: 60%;
        padding: 2rem;
      }
      .card-title {
        font-size: 1.5rem;
      }
    }
    /* 7. Container Query: 大容器 (700px+) - 增強視覺效果 */
    @container card-container (min-width: 700px) {
      .card-body {
        padding: 2.5rem;
      }
      .card-title {
        font-size: 1.75rem;
        margin-bottom: 1rem;
      }
      .card-description {
        font-size: 1.125rem;
        line-height: 1.8;
      }
      .card-meta {
        font-size: 1rem;
        margin-top: 1.5rem;
      }
    }
    /* 8. 示範不同寬度的容器 */
    .demo-container {
      display: grid;
      gap: 2rem;
      margin-top: 2rem;
    }
    .narrow {
      max-width: 350px;
    }
    .medium {
      max-width: 600px;
    }
    .wide {
      max-width: 900px;
    }
    /* 9. 標籤樣式 */
    .container-label {
      display: inline-block;
      padding: 0.5rem 1rem;
      background: #3b82f6;
      color: white;
      border-radius: 4px;
      font-size: 0.875rem;
      font-weight: 500;
      margin-bottom: 1rem;
    }
    /* 10. 響應式圖片最佳化 */
    .card-image {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }
  </style>
</head>
<body>
  <h1 style="margin-bottom: 2rem;">Container Queries 實戰示範</h1>
  <div class="demo-container">
    <!-- 窄容器示範 -->
    <div>
      <div class="container-label">窄容器 (350px)</div>
      <div class="card-wrapper narrow">
        <article class="smart-card">
          <div class="card-content">
            <img src="https://picsum.photos/400/300" alt="範例圖片" class="card-image">
            <div class="card-body">
              <h2 class="card-title">智能響應式卡片</h2>
              <p class="card-description">
                這個卡片會根據容器寬度自動調整布局,在窄容器中垂直排列。
              </p>
              <div class="card-meta">
                <span>📅 2025-01-15</span>
                <span>👤 作者</span>
              </div>
            </div>
          </div>
        </article>
      </div>
    </div>
    <!-- 中等容器示範 -->
    <div>
      <div class="container-label">中等容器 (600px)</div>
      <div class="card-wrapper medium">
        <article class="smart-card">
          <div class="card-content">
            <img src="https://picsum.photos/400/300" alt="範例圖片" class="card-image">
            <div class="card-body">
              <h2 class="card-title">智能響應式卡片</h2>
              <p class="card-description">
                在中等容器中,卡片自動切換為水平布局,圖片和內容並排顯示。
              </p>
              <div class="card-meta">
                <span>📅 2025-01-15</span>
                <span>👤 作者</span>
                <span>💬 24 留言</span>
              </div>
            </div>
          </div>
        </article>
      </div>
    </div>
    <!-- 寬容器示範 -->
    <div>
      <div class="container-label">寬容器 (900px)</div>
      <div class="card-wrapper wide">
        <article class="smart-card">
          <div class="card-content">
            <img src="https://picsum.photos/400/300" alt="範例圖片" class="card-image">
            <div class="card-body">
              <h2 class="card-title">智能響應式卡片</h2>
              <p class="card-description">
                在寬容器中,字體和間距都會增大,提供更舒適的閱讀體驗。同時保持完美的視覺比例。
              </p>
              <div class="card-meta">
                <span>📅 2025-01-15</span>
                <span>👤 作者</span>
                <span>💬 24 留言</span>
                <span>❤️ 128 喜歡</span>
              </div>
            </div>
          </div>
        </article>
      </div>
    </div>
  </div>
  <script>
    // 動態顯示容器實際寬度
    const updateContainerWidths = () => {
      document.querySelectorAll('.card-wrapper').forEach(wrapper => {
        const width = wrapper.offsetWidth;
        const label = wrapper.previousElementSibling;
        if (label && label.classList.contains('container-label')) {
          label.textContent = label.textContent.split('(')[0] + `(${width}px)`;
        }
      });
    };
    window.addEventListener('resize', updateContainerWidths);
    updateContainerWidths();
  </script>
</body>
</html>
建立一個完整的響應式網格系統,支援自動填充和智能間距:
/**
 * 現代化 Grid 布局系統
 * 特性:自動響應、智能間距、無需媒體查詢
 */
/* 1. 基礎網格容器 */
.grid-auto {
  display: grid;
  gap: var(--grid-gap, 1.5rem);
  /* 自動填充:最小 250px,最大 1fr */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
/* 2. 響應式列數控制 */
.grid-2 {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}
.grid-3 {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
.grid-4 {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
/* 3. 不等寬網格(聖杯布局)*/
.holy-grail-layout {
  display: grid;
  gap: 2rem;
  /*
     auto: 側邊欄自適應內容
     1fr: 主內容佔據剩餘空間
  */
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "sidebar-left main sidebar-right"
    "footer footer footer";
  min-height: 100vh;
}
/* 響應式聖杯布局 */
@media (max-width: 768px) {
  .holy-grail-layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "sidebar-left"
      "sidebar-right"
      "footer";
  }
}
/* 4. 複雜網格:不規則布局 */
.masonry-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  grid-auto-rows: 20px; /* 小單元高度 */
  gap: 1rem;
}
.masonry-item {
  /* 根據內容高度佔據不同行數 */
}
.masonry-item.small {
  grid-row: span 10;  /* 200px */
}
.masonry-item.medium {
  grid-row: span 15;  /* 300px */
}
.masonry-item.large {
  grid-row: span 20;  /* 400px */
}
/* 5. Grid 配合 Container Queries */
.adaptive-grid {
  container-type: inline-size;
  display: grid;
  gap: 1rem;
}
/* 容器小於 400px:單列 */
@container (max-width: 400px) {
  .adaptive-grid {
    grid-template-columns: 1fr;
  }
}
/* 容器 400px - 700px:雙列 */
@container (min-width: 400px) and (max-width: 700px) {
  .adaptive-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
/* 容器大於 700px:三列 */
@container (min-width: 700px) {
  .adaptive-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
/* 6. 進階:子網格 (Subgrid) */
.parent-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
}
.child-grid {
  display: grid;
  /* 繼承父網格的列定義 */
  grid-template-columns: subgrid;
  grid-column: span 3;
  gap: 1rem;
}
/* 7. 實用工具類 */
.full-width {
  grid-column: 1 / -1;  /* 佔據所有列 */
}
.span-2 {
  grid-column: span 2;
}
.span-3 {
  grid-column: span 3;
}
/* 8. 智能對齊 */
.grid-center {
  place-items: center;  /* 水平和垂直居中 */
}
.grid-start {
  place-items: start;
}
.grid-stretch {
  place-items: stretch;
}
完整 HTML 示例:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>現代 Grid 布局系統</title>
  <style>
    /* 引入上面的 CSS */
    /* 示範樣式 */
    .demo-card {
      background: white;
      border-radius: 8px;
      padding: 2rem;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }
    .demo-card h3 {
      margin-bottom: 1rem;
      color: #333;
    }
    .demo-card p {
      color: #666;
      line-height: 1.6;
    }
  </style>
</head>
<body>
  <!-- 自動響應網格 -->
  <section class="grid-auto" style="padding: 2rem;">
    <div class="demo-card small">
      <h3>卡片 1</h3>
      <p>自動調整寬度的網格項目</p>
    </div>
    <div class="demo-card medium">
      <h3>卡片 2</h3>
      <p>內容較多的卡片會自動擴展高度</p>
    </div>
    <div class="demo-card large">
      <h3>卡片 3</h3>
      <p>Grid 會自動計算最佳列數</p>
    </div>
    <div class="demo-card small">
      <h3>卡片 4</h3>
      <p>無需媒體查詢</p>
    </div>
  </section>
  <!-- 聖杯布局 -->
  <div class="holy-grail-layout">
    <header style="grid-area: header;">
      <h1>Header</h1>
    </header>
    <aside style="grid-area: sidebar-left;">
      <nav>左側導航</nav>
    </aside>
    <main style="grid-area: main;">
      <article>主要內容</article>
    </main>
    <aside style="grid-area: sidebar-right;">
      <div>右側廣告</div>
    </aside>
    <footer style="grid-area: footer;">
      <p>Footer</p>
    </footer>
  </div>
</body>
</html>
現代化的圖片載入和顯示策略:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>響應式圖片最佳實踐</title>
  <style>
    /* 1. 基礎響應式圖片 */
    .responsive-img {
      width: 100%;
      height: auto;
      display: block;
    }
    /* 2. 保持比例的圖片容器 */
    .img-container {
      position: relative;
      width: 100%;
      /* 16:9 比例 */
      aspect-ratio: 16 / 9;
      overflow: hidden;
      background: #f0f0f0;
    }
    .img-container img {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      /* 延遲載入的淡入效果 */
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    .img-container img.loaded {
      opacity: 1;
    }
    /* 3. Art Direction:不同螢幕顯示不同裁切 */
    .art-direction-container {
      container-type: inline-size;
    }
    /* 4. 載入狀態 */
    .img-container::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 40px;
      height: 40px;
      margin: -20px 0 0 -20px;
      border: 3px solid #f3f3f3;
      border-top: 3px solid #3b82f6;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
    .img-container.loaded::before {
      display: none;
    }
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    /* 5. 圖片網格布局 */
    .image-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 1rem;
      padding: 2rem;
    }
    /* 6. 模糊載入效果 */
    .blur-load {
      position: relative;
      background-size: cover;
      background-position: center;
    }
    .blur-load::before {
      content: '';
      position: absolute;
      inset: 0;
      backdrop-filter: blur(20px);
      transition: opacity 0.3s ease;
    }
    .blur-load.loaded::before {
      opacity: 0;
    }
    /* 7. 不同比例的圖片 */
    .square {
      aspect-ratio: 1 / 1;
    }
    .portrait {
      aspect-ratio: 3 / 4;
    }
    .landscape {
      aspect-ratio: 16 / 9;
    }
    .ultrawide {
      aspect-ratio: 21 / 9;
    }
  </style>
</head>
<body>
  <!-- 1. 基礎響應式圖片 -->
  <section>
    <h2>基礎響應式圖片</h2>
    <img
      src="image-small.jpg"
      srcset="
        image-small.jpg 400w,
        image-medium.jpg 800w,
        image-large.jpg 1200w
      "
      sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
      alt="響應式圖片"
      class="responsive-img"
      loading="lazy"
    >
  </section>
  <!-- 2. Art Direction:不同裁切 -->
  <section>
    <h2>Art Direction</h2>
    <picture>
      <!-- 行動裝置:直式裁切 -->
      <source
        media="(max-width: 767px)"
        srcset="portrait-small.jpg"
      >
      <!-- 平板:方形裁切 -->
      <source
        media="(max-width: 1023px)"
        srcset="square-medium.jpg"
      >
      <!-- 桌面:橫式裁切 -->
      <img
        src="landscape-large.jpg"
        alt="不同裝置顯示不同裁切"
        class="responsive-img"
      >
    </picture>
  </section>
  <!-- 3. 現代圖片格式 -->
  <section>
    <h2>現代圖片格式(WebP/AVIF)</h2>
    <picture>
      <!-- 優先使用 AVIF -->
      <source
        type="image/avif"
        srcset="image.avif"
      >
      <!-- 其次使用 WebP -->
      <source
        type="image/webp"
        srcset="image.webp"
      >
      <!-- 最後使用 JPEG 作為後備 -->
      <img
        src="image.jpg"
        alt="現代圖片格式"
        class="responsive-img"
      >
    </picture>
  </section>
  <!-- 4. 延遲載入的圖片網格 -->
  <section>
    <h2>延遲載入圖片網格</h2>
    <div class="image-grid">
      <div class="img-container square">
        <img
          data-src="https://picsum.photos/400/400"
          alt="圖片 1"
          loading="lazy"
          class="lazy-img"
        >
      </div>
      <div class="img-container landscape">
        <img
          data-src="https://picsum.photos/800/450"
          alt="圖片 2"
          loading="lazy"
          class="lazy-img"
        >
      </div>
      <div class="img-container portrait">
        <img
          data-src="https://picsum.photos/600/800"
          alt="圖片 3"
          loading="lazy"
          class="lazy-img"
        >
      </div>
    </div>
  </section>
  <script>
    // 圖片延遲載入與淡入效果
    class LazyImageLoader {
      constructor() {
        this.images = document.querySelectorAll('.lazy-img');
        this.options = {
          root: null,
          rootMargin: '50px',
          threshold: 0.01
        };
        this.observer = new IntersectionObserver(
          this.handleIntersection.bind(this),
          this.options
        );
        this.init();
      }
      init() {
        this.images.forEach(img => {
          this.observer.observe(img);
        });
      }
      handleIntersection(entries) {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadImage(entry.target);
            this.observer.unobserve(entry.target);
          }
        });
      }
      loadImage(img) {
        const src = img.dataset.src;
        if (!src) return;
        // 建立新的圖片物件以預載
        const loader = new Image();
        loader.onload = () => {
          img.src = src;
          img.classList.add('loaded');
          img.parentElement.classList.add('loaded');
        };
        loader.onerror = () => {
          console.error(`Failed to load image: ${src}`);
          img.parentElement.classList.add('error');
        };
        loader.src = src;
      }
    }
    // 初始化延遲載入
    document.addEventListener('DOMContentLoaded', () => {
      new LazyImageLoader();
    });
    // 模糊載入效果(進階版)
    class BlurImageLoader {
      constructor(element) {
        this.element = element;
        this.lowResSrc = element.dataset.lowres;
        this.highResSrc = element.dataset.highres;
        this.loadImages();
      }
      loadImages() {
        // 先載入低解析度版本
        if (this.lowResSrc) {
          this.element.style.backgroundImage = `url(${this.lowResSrc})`;
        }
        // 然後載入高解析度版本
        const img = new Image();
        img.onload = () => {
          this.element.style.backgroundImage = `url(${this.highResSrc})`;
          this.element.classList.add('loaded');
        };
        img.src = this.highResSrc;
      }
    }
    // 使用範例
    document.querySelectorAll('.blur-load').forEach(el => {
      new BlurImageLoader(el);
    });
  </script>
</body>
</html>
核心概念: Container Queries 是響應式設計的革命性技術,讓組件真正可複用。傳統 Media Queries 基於視窗,Container Queries 基於容器,更符合組件化開發。
關鍵技術:
auto-fit 和 minmax 實現真正的自適應布局aspect-ratio 屬性簡化圖片比例控制loading="lazy")提升載入效能實踐要點:
container-type: inline-size 定義容器查詢環境✅ 推薦做法:
srcset 和 sizes 提供多種解析度aspect-ratio 防止內容跳動(CLS)❌ 避免陷阱:
container-type 的元素上使用 @container
漸進增強: 如何在不支援 Container Queries 的瀏覽器提供後備方案?
效能考量: Container Queries 對渲染效能有什麼影響?如何最佳化?
實踐挑戰: 嘗試將現有專案的 Media Queries 重構為 Container Queries,測量程式碼減少量和可維護性提升。