商用網站架設實務

LESSON-10 · solution

CSS 進階:Mobile First 與 Media Query

這一頁不是直接複製答案;先逐檔看差異、修改原因與驗收證據,再把需要的改動帶回自己的資料夾。 這頁用 可觀察證據anchor 指出下一個動作。

Starter3 個小成果
Checkpoint一個核心缺口
Solution4 個差異

開始前確認

這一頁的結果:390px、768px 與桌機都能開啟並保持可讀;DevTools 可看到實際 grid-template-columns,且 scrollWidth <= clientWidth;Tab 可依序操作連結、欄位與按鈕,focus 不會消失

工作方式:先把這一階段複製到自己的工作資料夾,開啟 files/index.html,記下修改前的畫面、DOM、Console 或 Network狀態。

會用到的檔案:index.htmlstyle.cssmain.js

本堂焦點:Mobile First 與 Media Query 讓服務型網站適應不同寬度

照這個順序做

一次只做一張卡。完成後先重新整理入口,再留下證據,才進入下一張。

差異 1

先讓最窄的 viewport 成為可閱讀、可操作的基底,再增加寬版規則。

目的

先讓最窄的 viewport 成為可閱讀、可操作的基底,再增加寬版規則。

修改檔案

style.css

程式碼位置

TODO-1:service-grid 與 contact-layout

現在要做:先對照差異,再檢查 在 style.css 的「TODO-1:service-grid 與 contact-layout」對照這項變更:.service-grid, .contact-layout { grid-template-columns: 1fr; }

貼上位置:style.css 的 TODO-1:service-grid 與 contact-layout 區塊

.service-grid,
.contact-layout {
  grid-template-columns: 1fr;
}

預期結果:390px 下服務卡與聯絡區塊都各自只有一欄,且沒有水平捲軸。

驗收證據:DevTools 390px 畫面;選取兩個 Grid container,在 Computed 讀到 grid-template-columns。

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 2

用 breakpoint 表達內容何時有足夠空間,而不是猜測裝置名稱。

目的

breakpoint 表達內容何時有足夠空間,而不是猜測裝置名稱。

修改檔案

style.css

程式碼位置

TODO-2:@media (min-width: 760px)

現在要做:先對照差異,再檢查 在 style.css 的「TODO-2:@media (min-width: 760px)」對照這項變更:@media (min-width: 760px) { .service-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .contact-layout { grid-template-columns: 1fr 1fr; } }

貼上位置:style.css 的 TODO-2:@media (min-width: 760px) 區塊

@media (min-width: 760px) {
  .service-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .contact-layout { grid-template-columns: 1fr 1fr; }
}

預期結果:390px 保持單欄;768px 以上服務卡變三欄、聯絡區塊變雙欄。

驗收證據:390px 與 768px 的截圖,以及兩個 Grid containerComputed 欄位值。

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 3

把畫面觀察轉成實際數值,但仍以 DevTools 為主要驗收工具。

目的

把畫面觀察轉成實際數值,但仍以 DevTools 為主要驗收工具。

修改檔案

main.js

程式碼位置

measureLayout

現在要做:先對照差異,再檢查 在 main.js 的「measureLayout」對照這項變更:const viewportWidth = window.innerWidth; const documentWidth = document.documentElement.scrollWidth; const columns = getComputedStyle(serviceGrid).gridTemplateColumns; console.log({ viewportWidth, documentWidth, columns });

貼上位置:main.js 的 measureLayout 區塊

const viewportWidth = window.innerWidth;
const documentWidth = document.documentElement.scrollWidth;
const columns = getComputedStyle(serviceGrid).gridTemplateColumns;
console.log({ viewportWidth, documentWidth, columns });

預期結果:按下檢查按鈕後,狀態文字與 Console 顯示目前 viewport、文件寬度、水平溢位與服務卡欄位計算值。

驗收證據:Console 的物件值、頁面 status 文字,以及 DevTools Computed 的交叉比對。

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 4

用 scrollWidth 與 Elements 找到真正超出 viewport 的元素,再修正根因而不是用 overflow:hidden 隱藏症狀。

目的

scrollWidthElements 找到真正超出 viewport 的元素,再修正根因而不是用 overflow:hidden 隱藏症狀。

修改檔案

style.css

程式碼位置

CHECKPOINT-1:.page-shell 固定寬度

現在要做:先對照差異,再檢查 在 style.css 的「CHECKPOINT-1:.page-shell 固定寬度」對照這項變更:.page-shell { width: min(100% - 2rem, 72rem); }

貼上位置:style.css 的 CHECKPOINT-1:.page-shell 固定寬度 區塊

.page-shell {
  width: min(100% - 2rem, 72rem);
}

預期結果:390px 下 document.documentElement.scrollWidth <= document.documentElement.clientWidth,頁面可水平閱讀與操作。

驗收證據:修正前後各記錄一次 scrollWidth、clientWidth 與 390px 畫面;修正後應看不到水平捲軸。

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

階段檔案

完整檔案收在可展開的卡片中;先完成上方微步驟,再用這裡查閱與複製。檔案位置要和目前的相對路徑一致。

開啟 solution 的可執行入口

index.html入口 HTML:提供六張服務卡、聯絡區塊、表單欄位與選用的測量按鈕。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <title>LESSON-10 Solution|Mobile First 與 Media Query 完整結果</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header"><a class="brand" href="index.html">安心維護</a><span>LESSON-10 Solution</span></header>
  <main class="page-shell">
    <p class="eyebrow">Solution · 服務型網站響應式版面</p>
    <h1>Mobile First 與 Media Query 完整結果</h1>
    <p class="page-description">這份檔案已完成單欄基底、760px 寬版規則、流動容器與實際測量工具;請逐檔對照修改原因。</p>
    <section aria-labelledby="services-title"><h2 id="services-title">服務項目</h2><div class="service-grid">
        <article class="service-card"><h3>網站規劃</h3><p>整理網站內容與頁面結構。</p></article>
        <article class="service-card"><h3>視覺設計</h3><p>建立清楚一致的視覺層次。</p></article>
        <article class="service-card"><h3>前端製作</h3><p>把版面做成可操作的網頁。</p></article>
        <article class="service-card"><h3>內容整理</h3><p>讓訪客快速理解服務內容。</p></article>
        <article class="service-card"><h3>響應式調整</h3><p>讓不同寬度都維持可讀性。</p></article>
        <article class="service-card"><h3>上線檢查</h3><p>確認連結、鍵盤與窄版畫面。</p></article>
    </div></section>
    <section class="contact-layout" aria-labelledby="contact-title"><article class="panel"><h2 id="contact-title">聯絡資訊</h2><p>這個說明欄會和右側表單一起依可用寬度改變欄數。</p><a href="#contact-form">跳到表單</a></article><form class="panel" id="contact-form"><h2>留言表單</h2><label for="name">姓名</label><input id="name" name="name" autocomplete="name"><label for="message">留言</label><textarea id="message" name="message" rows="4"></textarea><button type="button">示範按鈕外觀(本堂不送出資料)</button></form></section>
    <section class="result-card" aria-labelledby="evidence-title"><h2 id="evidence-title">目前寬度觀察工具(選用)</h2><p id="responsive-status" role="status">尚未測量。請先用 DevTools 調整寬度,再按按鈕查看數值。</p><button id="measure-layout" type="button">檢查目前版面</button></section>
  </main>
  <script src="main.js" defer></script>
</body>
</html>
style.cssCSS 樣式:控制流動容器、Grid 欄位、breakpointfocus 與 checkpoint 缺口。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

:root { font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans TC", sans-serif; color: #17324d; background: #f5f8fb; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; line-height: 1.7; }
.site-header { width: min(100% - 2rem, 72rem); margin: 1rem auto 0; padding: .75rem 0; display: flex; justify-content: space-between; gap: 1rem; align-items: center; } .site-header a { color: #155e75; }
.page-shell { width: min(100% - 2rem, 72rem); margin: 2rem auto; padding: 1.5rem; border: 1px solid #cbd8e4; border-radius: 20px; background: #fff; }
.eyebrow { color: #176b87; font-weight: 800; } .page-description { color: #52657d; }
.service-grid, .contact-layout { display: grid; gap: 1rem; grid-template-columns: 1fr; }
.service-card, .panel, .result-card { padding: 1rem; border: 1px solid #d8e0ea; border-radius: 14px; background: #fbfdff; } .service-card h3, .panel h2, .result-card h2 { margin-top: 0; }
label { display: block; margin-top: .8rem; font-weight: 700; } input, textarea { width: 100%; padding: .65rem; border: 1px solid #aebdca; border-radius: 8px; font: inherit; }
button, a { font: inherit; } button { margin-top: 1rem; padding: .65rem .9rem; border: 0; border-radius: 9px; background: #176b87; color: #fff; cursor: pointer; }
button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid #f1ab2c; outline-offset: 3px; }
.result-card { margin-top: 2rem; }
@media (min-width: 760px) { .service-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .contact-layout { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .site-header { align-items: flex-start; flex-direction: column; } }
main.js選用測量工具:讀取 viewportscrollWidth、clientWidth 與 computed 欄位,不輸出硬編碼通過訊息。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

const status = document.querySelector("#responsive-status");
const measureButton = document.querySelector("#measure-layout");
const serviceGrid = document.querySelector(".service-grid");
function measureLayout() {
  if (!status || !serviceGrid) return;
  const viewportWidth = window.innerWidth;
  const documentWidth = document.documentElement.scrollWidth;
  const viewportDocumentWidth = document.documentElement.clientWidth;
  const columns = getComputedStyle(serviceGrid).gridTemplateColumns;
  const hasHorizontalOverflow = documentWidth > viewportDocumentWidth;
  status.textContent = "目前 viewport:" + viewportWidth + "px;服務卡欄數計算值:" + columns + ";水平溢位:" + (hasHorizontalOverflow ? "有" : "無") + "。";
  console.log({ viewportWidth, documentWidth, viewportDocumentWidth, columns, hasHorizontalOverflow });
}
measureButton?.addEventListener("click", measureLayout);

完成驗收

下一步

Solution 對照完成後,回到 README 整理證據,並準備下一堂課的先備知識。

回到 README,整理本堂證據並銜接下一堂