Skip to content

2026-07-13

/ 13 分鐘閱讀

/ You Might Not Need a Framework

DetailsPage 元件:複用既有模式與調試 dataset 命名

上一篇建立了 ProductItem 元件,解決了連結內嵌按鈕的事件衝突,並完成了依 ID 查找商品的邏輯。這篇接續建立 DetailsPage 元件,內容大量沿用之前頁面元件的模式,重點放在一個實際遇到的命名不一致問題,以及如何排查。

DetailsPage 的結構

DetailsPage 是一個頁面層級的元件,因此使用 Shadow DOM,做法和之前的 MenuPage 相同:

javascript
export default class DetailsPage extends HTMLElement {
  constructor() {
    super();

    this.root = this.attachShadow({ mode: "open" });

    const template = document.getElementById("details-page-template");
    const content = template.content.cloneNode(true);
    const styles = document.createElement("style");
    this.root.appendChild(content);
    this.root.appendChild(styles);

    async function loadCSS() {
      const request = await fetch("/components/DetailsPage.css");
      styles.textContent = await request.text();
    }
    loadCSS();
  }

  async renderData() {
    if (this.dataset.productId) {
      this.product = await getProductById(this.dataset.productId);
      this.root.querySelector("h2").textContent = this.product.name;
      this.root.querySelector("img").src = `/data/images/${this.product.image}`;
      this.root.querySelector(".description").textContent = this.product.description;
      this.root.querySelector(".price").textContent = `$ ${this.product.price.toFixed(2)} ea`;
      this.root.querySelector("button").addEventListener("click", () => {
        // TODO addToCart(this.product.id);
        app.router.go('/order');
      });
    } else {
      alert("Invalid Product ID");
    }
  }

  connectedCallback() {
    this.renderData();
  }
}

customElements.define("details-page", DetailsPage);

constructor 的內容和之前的頁面元件幾乎一致:建立 Shadow DOM、克隆 template、用 Fetch 載入外部 CSS 並注入。connectedCallback 中只呼叫了 renderData,把渲染邏輯獨立成一個方法,便於閱讀。

renderData 的核心邏輯

renderData 檢查 this.dataset.productId 是否存在,存在的話呼叫 getProductById 取得商品資料(這個函式如果選單尚未載入,內部會先 await loadData()),然後逐一把商品的名稱、圖片、描述和價格填入 template 對應的元素中,並為「Add to Cart」按鈕掛上點擊事件(加入購物車的邏輯目前先留 TODO,先導向訂單頁)。

排查一個實際的命名不一致問題

把程式碼貼上後,畫面跳出了「Invalid Product ID」的警示,代表 this.dataset.productIdundefined

問題出在哪裡?回頭檢查 Router 中建立 details-page 元素時所設定的 dataset 屬性名稱:

javascript
pageElement.dataset.id = paramId;

這裡設定的屬性叫做 id,對應到 DOM 中就是 data-id,而不是 data-product-id。但在 DetailsPage 元件中讀取的卻是 this.dataset.productId,兩者名稱不一致,自然讀不到值。

排查的方式是直接打開 DevTools 檢查 DOM,確認實際渲染出來的 attribute 名稱(data-id),再回去比對程式碼中讀取的屬性名稱是否一致。

修正的方式是統一命名,把 Router 中設定 dataset 的那一行改成:

javascript
pageElement.dataset.productId = paramId;

這樣 dataset.productId 就能對應到 data-product-id,與 DetailsPage 元件中讀取的屬性名稱一致。

小結

這次的問題不是邏輯錯誤,而是命名不一致導致的資料對不上。這提醒了一個實務上的習慣:在不同檔案之間透過 dataset 傳遞資料時,屬性名稱必須在設定端和讀取端保持完全一致,DevTools 的 Elements 面板是排查這類問題最直接的工具。

修正後,從首頁點選商品就能正確進入該商品的詳細頁,畫面上會顯示對應的名稱、圖片、描述和價格,並有一個目前還沒完整實作的「Add to Cart」按鈕(會先把使用者導向訂單頁)。

複習

在 DetailsPage.js 元件中使用 Shadow DOM 的目的是什麼?

Shadow DOM 用來建立一個與主文件 DOM 分離的封裝 DOM 樹,有助於隔離樣式,避免元件之間的樣式衝突

在 DetailsPage 元件中,用什麼方法來處理資料的渲染?

使用 renderData 方法,檢查 dataset 中是否設定了商品 ID,並渲染對應的商品詳細資訊

載入元件的 template 和樣式經過哪些步驟?

元件會克隆 template、載入 template 的內容,並套用 CSS 樣式,藉此建立元件的視覺結構

從首頁選取商品時會發生什麼動作?

從首頁選取商品時,使用者會被導向該特定商品的詳細頁面

小測驗

DetailsPage.js 元件中使用了什麼 DOM 技術? Shadow DOM
connectedCallback() 方法的主要用途是什麼? 渲染 template 並載入樣式

此文章是 FrontendMasters 上的 Vanilla JS: You Might Not Need a Framework 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 28 篇 / 共 31 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash