系列文章: 前端工程師的 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,測量程式碼減少量和可維護性提升。