iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Modern Web

Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具系列 第 12

Day 12:專案核心 (1):createElement 動態生成 UI

  • 分享至 

  • xImage
  •  

在過去幾天,我們完成了所有基礎建設:我們有了能思考的「大腦」(JS)、能展示的「身體」(HTML/CSS),以及連接兩者的「神經系統」(DOM)。我們甚至學會了如何監聽事件,讓掃描按鈕在被點擊時,能在控制台裡說「Hello」。

但是,一個真正的應用程式,不只是對用户的操作做出「回應」,它更需要根據未知的、動態的資料來**「創造」**新的介面。

我們的「通用 BLE 偵錯工具」之所以強大,就在於它的「通用」二字。我們事先不知道即將連接的藍牙裝置是什麼品牌、有什麼功能。它可能有心率服務,也可能有電量服務;它的某個特徵可能是可讀的,另一個則可能是只能訂閱的。

因此,我們的介面絕不能寫死在 HTML 裡。它必須由 JavaScript 在獲取到裝置資訊後,即時、動態地生成。今天,我們就要來打造這個專案的視覺化核心——一個能夠根據藍牙特徵屬性,動態生成對應 UI 元素(如讀取按鈕、寫入框)的「UI 工廠」。這不僅考驗我們對 DOM 的操作,更是對閉包概念的終極實戰!


1. DOM 操作三劍客:創造、設定、附加

要讓 JavaScript 從無到有地創造出 HTML 元素,只需要遵循三個簡單的步驟。

A. document.createElement('標籤名') - 創造

  • 功能:這個方法就像一個 3D 列印機,你告訴它要創造什麼零件('div', 'p', 'button' 等),它就會在 JavaScript 的記憶體中創造出一個對應的元素。

  • 重點:此刻,這個新元素只存在於記憶體中,使用者在網頁上是看不到它的。

// 創造一個 div 元素
const myDiv = document.createElement('div');

// 創造一個 button 元素
const myButton = document.createElement('button');

B. element.屬性 = '值' - 設定

  • 功能:剛創造出來的元素是空白的。這一步就是對這個「零件」進行加工,設定它的外觀、內容和身份。

  • 常用屬性

    • .textContent: 設定元素內部的文字內容。

    • .id: 設定元素唯一的 ID。

    • .className: 設定元素的 CSS class,方便我們用之前寫好的樣式來美化它。

// 設定 myDiv 的 class
myDiv.className = 'service-card'; // 假設我們在 style.css 裡定義了這個 class

// 設定 myButton 的文字
myButton.textContent = '讀取數值';

C. 父層元素.appendChild(子層元素) - 附加

  • 功能:這是最關鍵的一步!它將我們在記憶體中創造並設定好的元素,「掛載」到一個已經存在於頁面上的父層元素中。只有執行了這一步,使用者才能真正看到這個新元素

  • 父層元素:通常是我們先前用 getElementById 選取好的容器,例如我們的 gattContainer

// 假設 gattContainer 已經被選取好了
const gattContainer = document.getElementById('gattContainer');

// 將我們剛創造的 myDiv,附加到 gattContainer 裡面
gattContainer.appendChild(myDiv);

// 接著,再將 myButton 附加到 myDiv 裡面
myDiv.appendChild(myButton);

執行完畢後,頁面的 DOM 結構就會變成: <div id="gattContainer"> <div class="service-card"> <button>讀取數值</button> </div> </div>


2. 實戰演練:打造一個顯示「服務」的 UI 卡片

現在,讓我們把「三劍客」組合起來,寫一個專門用來生成「服務 UI 卡片」的函式。

app.js 中加入以下函式:

// --- [4] 動態生成 UI 的函式 ---

/**
 * 根據給定的服務資訊,創建並回傳一個服務 UI 卡片元素
 * @param {object} serviceInfo - 包含服務 UUID 和名稱的物件
 * @returns {HTMLElement} - 創建好的 div 卡片元素
 */
function createServiceCard(serviceInfo) {
  // 1. 創造 (Create)
  const card = document.createElement('div');
  const serviceName = document.createElement('h3');
  const serviceUuid = document.createElement('p');

  // 2. 設定 (Set)
  card.className = 'service-card'; // 套用 CSS 樣式
  serviceName.textContent = serviceInfo.name || '未知服務'; // 如果沒有名稱,就顯示未知服務
  serviceUuid.textContent = `UUID: ${serviceInfo.uuid}`;

  // 3. 附加 (Append) - 先將 h3 和 p 附加到卡片上
  card.appendChild(serviceName);
  card.appendChild(serviceUuid);

  return card; // 回傳這張製作好的卡片
}

// --- 測試我們的函式 ---
// 模擬一個從藍牙裝置掃描到的服務
const mockService = {
  uuid: '0000180d-0000-1000-8000-00805f9b34fb',
  name: 'Heart Rate Service'
};

// 使用我們的工廠函式創建 UI 卡片
const serviceCard = createServiceCard(mockService);

// 最後,把卡片附加到頁面的主容器中
gattContainer.appendChild(serviceCard);
  • || '未知服務':這是一個 JavaScript 的小技巧,意思是如果 serviceInfo.namenullundefined (不存在),就使用後面的 '未知服務' 當作預設值。

現在,重新整理你的 index.html,你應該能看到一張漂亮的服務卡片出現在頁面上了!


3. 專案核心:根據特徵屬性生成對應 UI

這才是今天的重頭戲。一個服務卡片下面,需要顯示它所包含的各個特徵,以及可以對這些特徵做的操作(讀、寫、訂閱)。

繼續在 app.js 中加入更核心的函式:

/**
* 根據特徵資訊,生成對應的 UI 元素 (包含操作按鈕)
* @param {object} charInfo - 包含特徵 UUID 和 properties 的物件
* @param {HTMLElement} serviceCard - 這個特徵所屬的服務卡片元素
*/
function renderCharacteristic(charInfo, serviceCard) {
 // 創造容器和標題
 const charContainer = document.createElement('div');
 charContainer.className = 'char-container';
 const charUuid = document.createElement('p');
 charUuid.textContent = `Characteristic: ${charInfo.uuid}`;

 // 創造按鈕的容器
 const buttonContainer = document.createElement('div');
 buttonContainer.className = 'button-container';

 // ** 核心邏輯:根據 properties 決定要生成哪些按鈕 **
 if (charInfo.properties.read) {
   const readButton = document.createElement('button');
   readButton.textContent = 'Read';
   // 【閉包實戰】
   // 每個按鈕的 onclick 事件都記住了自己是在哪個 charInfo 的上下文中被創建的
   readButton.onclick = () => {
     console.log(`準備從特徵 ${charInfo.uuid} 讀取資料...`);
     // 未來,這裡會呼叫真正的 Web Bluetooth API 讀取函式
   };
   buttonContainer.appendChild(readButton);
 }

 if (charInfo.properties.write) {
   const writeButton = document.createElement('button');
   writeButton.textContent = 'Write';
   writeButton.onclick = () => {
     console.log(`準備向特徵 ${charInfo.uuid} 寫入資料...`);
   };
   buttonContainer.appendChild(writeButton);
 }

 if (charInfo.properties.notify) {
   const notifyButton = document.createElement('button');
   notifyButton.textContent = 'Subscribe';
   notifyButton.onclick = () => {
     console.log(`準備訂閱特徵 ${charInfo.uuid} 的通知...`);
   };
   buttonContainer.appendChild(notifyButton);
 }

 // 將所有元素附加到 DOM
 charContainer.appendChild(charUuid);
 charContainer.appendChild(buttonContainer);
 serviceCard.appendChild(charContainer);
}

// --- 測試我們的核心函式 ---
// 模擬同一服務下的兩個不同特徵
const mockChar1 = {
 uuid: '00002a37-0000-1000-8000-00805f9b34fb',
 properties: { read: false, write: false, notify: true } // 只能訂閱
};
const mockChar2 = {
 uuid: '00002a38-0000-1000-8000-00805f9b34fb',
 properties: { read: true, write: false, notify: false } // 只能讀取
};

// 將這兩個特徵的 UI,渲染到我們剛剛創建的 serviceCard 上
renderCharacteristic(mockChar1, serviceCard);
renderCharacteristic(mockChar2, serviceCard);

重新整理頁面,打開開發者工具(F12)並切換到 Console。你會看到 UI 上第一個特徵只有一個「Subscribe」按鈕,第二個只有一個「Read」按鈕。點擊它們,Console 會精準地印出各自對應的 UUID!這證明了我們的閉包成功地讓每個按鈕都「記住」了自己是誰。


總結與後續

今天我們用「假資料」(mock data) 成功地測試了我們的 UI 工廠。現在,萬事俱備,只欠東風——真實的藍牙資料
明天,我們將把之前的所有知識串連起來:點擊「掃描」按鈕,觸發事件,第一次真正呼叫 Web Bluetooth API,掃描並連接一個真實(或虛擬)的藍牙裝置,並將從裝置中獲取到的真實服務與特徵資料,餵給我們今天打造的 UI 工廠。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
Day 11:網頁的骨架 (DOM):選取核心 UI 區塊
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言