iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day 13:專案核心 (2):根據特徵屬性,打造動態互動面板

  • 分享至 

  • xImage
  •  

昨天,我們打造了一個「UI 工廠」函式 renderCharacteristic。它就像一位智慧工匠,能夠根據藍牙特徵的「屬性藍圖」(properties),精準地打造出對應的操作按鈕。點擊這些動態生成的按鈕,閉包的魔法確保了每一個按鈕都清楚地知道自己的身份。

然而,昨天的成果只是一個起點。一個「Read」按鈕,不應該只在控制台裡無聲吶喊,它需要一個地方來展示它讀取到的資料;一個「Write」按鈕,如果沒有輸入框,那它又能寫入什麼呢?

今天,我們的任務就是對昨天的「UI 工廠」進行一次重大升級。我們不再滿足於只生成陽春的按鈕,而是要為每一個特徵,都打造一個功能完備、資訊清晰的「動態互動面板 (Dynamic Interaction Panel)」。

這個面板將是我們工具的靈魂,它會包含:

  • 清晰的資訊展示區。

  • 針對「寫入」操作的數據輸入框

  • 針對「讀取」和「通知」的數值顯示區

完成今天的工作後,我們應用的前端互動邏輯就基本大功告成了。這個面板將成為我們與藍牙裝置溝通的唯一窗口,準備好,讓我們開始精裝修吧!


1. 藍圖升級:規劃「特徵互動面板」

在動手寫程式碼前,我們先在腦中規劃好最終成品的樣貌。對於每一個藍牙特徵,我們希望生成一個像下面這樣的卡片:

這個面板 (Panel) 是一個自給自足的元件,包含了:

  1. 標題區:顯示特徵的 UUID。

  2. 數值顯示區:一個專門用來顯示 ReadNotify 回傳值的地方。

  3. 互動區:根據特徵屬性,動態顯示 Read 按鈕、Write 輸入框與按鈕、Subscribe 按鈕。


2. 重構 renderCharacteristic 函式

我們的核心任務,就是重構昨天的 renderCharacteristic 函式,讓它能生成上面規劃的完整面板。

app.js 中,找到昨天的 renderCharacteristic 函式,並用下面的全新版本替換它:

/**
 * [重構版] 根據特徵資訊,生成一個功能完整的互動面板
 * @param {object} charInfo - 包含特徵 UUID 和 properties 的物件
 * @param {HTMLElement} serviceCard - 這個特徵所屬的服務卡片元素
 */
function renderCharacteristic(charInfo, serviceCard) {
  // 1. 建立面板整體容器
  const panel = document.createElement('div');
  panel.className = 'char-panel'; // 新的 CSS class

  const title = document.createElement('h4');
  title.textContent = `Characteristic: ${charInfo.uuid}`;
  panel.appendChild(title);

  // 2. 建立數值顯示區
  const valueContainer = document.createElement('div');
  valueContainer.className = 'char-value-container';
  valueContainer.innerHTML = '<strong>Value:</strong> <span class="char-value">--</span>';
  panel.appendChild(valueContainer);
  // 我們需要稍後能更新這個值,所以先選取它
  const valueSpan = valueContainer.querySelector('.char-value');

  // 3. 建立互動區 (按鈕和輸入框)
  const actionContainer = document.createElement('div');
  actionContainer.className = 'action-container';

  // 根據屬性,動態生成互動 UI
  if (charInfo.properties.read) {
    const readButton = document.createElement('button');
    readButton.textContent = 'Read';
    readButton.onclick = () => {
      console.log(`TODO: Read from ${charInfo.uuid}`);
      // 模擬讀取到一個值並更新 UI
      valueSpan.textContent = `[${Math.floor(Math.random() * 100)}] (mock)`;
    };
    actionContainer.appendChild(readButton);
  }

  if (charInfo.properties.write || charInfo.properties.writeWithoutResponse) {
    const writeInput = document.createElement('input');
    writeInput.type = 'text';
    writeInput.placeholder = 'Enter value to write';

    const writeButton = document.createElement('button');
    writeButton.textContent = 'Write';
    writeButton.onclick = () => {
      // 從輸入框讀取使用者輸入的值
      const valueToWrite = writeInput.value;
      console.log(`TODO: Write "${valueToWrite}" to ${charInfo.uuid}`);
      alert(`Simulating write: "${valueToWrite}"`);
    };
    actionContainer.appendChild(writeInput);
    actionContainer.appendChild(writeButton);
  }

  if (charInfo.properties.notify) {
    const notifyButton = document.createElement('button');
    notifyButton.textContent = 'Subscribe';
    notifyButton.onclick = () => {
      console.log(`TODO: Subscribe to ${charInfo.uuid}`);
      valueSpan.textContent = 'Subscribed! Waiting for data...';
    };
    actionContainer.appendChild(notifyButton);
  }

  // 4. 將互動區附加到面板,並將整個面板附加到服務卡片上
  panel.appendChild(actionContainer);
  serviceCard.appendChild(panel);
}

程式碼解析

  • valueContainer.innerHTML = ...

    • .innerHTML 是一個比 .textContent 更強大的屬性,它允許你直接寫入一段 HTML 字串來建立內部結構。我們用它快速地建立了一個帶有 <strong><span> 標籤的區塊。
  • valueContainer.querySelector('.char-value')

    • 我們昨天學過 document.querySelector 是從整個文件尋找。而 element.querySelector 則是只在該元素內部尋找。這裡,我們在剛建立的 valueContainer 內部,精準地找到了那個 class 為 char-value<span> 元素,並存到 valueSpan 變數中,方便稍後更新它的內容。
  • writeInput.value

    • 這是讀取 <input> 輸入框中目前使用者所輸入內容的標準方法。
  • charInfo.properties.writeWithoutResponse

    • 有些藍牙寫入操作是不需要裝置回覆確認的,我們也把它考慮進來。
  • 閉包再次發威

    • 注意看 readButton.onclick 函式,它不僅記得 charInfo,現在還能記得 valueSpan 這個變數。這就是閉包的強大之處,讓事件處理函式可以存取並操作它被創建時環境中的所有相關元素。

3. 為新面板添加 CSS 樣式

我們的 HTML 結構升級了,CSS 也必須跟上。

打開 style.css 檔案,在最下面加入以下樣式:

/* --- 特徵面板樣式 --- */

.char-panel {
  background-color: #f9f9f9; /* 比服務卡片更淺一點的背景 */
  border: 1px solid #eee;
  border-radius: 4px;
  padding: 10px;
  margin-top: 10px; /* 和上一個特徵或服務標題保持距離 */
}

.char-panel h4 {
  margin-top: 0;
  font-size: 14px;
  word-wrap: break-word; /* UUID太長時自動換行 */
}

.char-value-container {
  background-color: #e9ecef;
  padding: 8px;
  border-radius: 4px;
  font-family: 'Courier New', Courier, monospace; /* 使用等寬字體顯示數值 */
  margin-bottom: 10px;
}

.action-container {
  display: flex; /* 使用 Flexbox 佈局,讓裡面的元素更靈活 */
  gap: 10px;     /* 元素之間的間距 */
  align-items: center; /* 垂直置中對齊 */
}

.action-container input[type="text"] {
  flex-grow: 1; /* 讓輸入框自動填滿剩餘空間 */
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

CSS 解析

  • word-wrap: break-word;:一個很實用的小技巧,避免過長的 UUID 字串撐破我們的版面。

  • font-family: 'Courier New', ...:對於顯示數據或程式碼,使用「等寬字體」通常更清晰、更具技術感。

  • display: flex;:Flexbox 是現代 CSS 佈局的利器,gap: 10px; 則可以輕鬆設定子元素之間的間距,比傳統的 margin 更方便。


總結與後續

今天,我們將昨天的想法推向了極致!我們不再只是生成按鈕,而是為每個特徵都建構了一個功能完善、資訊清晰的互動面板。我們的 UI 工廠現在可以產出真正專業級的元件了。

至此,我們應用程式的「前端」部分,也就是使用者看得到、摸得著的所有介面和互動邏輯,已經完全準備就緒。我們已經做好了萬全的準備,來接收和展示真實的藍牙數據。

從明天開始,我們將暫時告別 UI 的打造,轉而正面迎擊 Web Bluetooth API

我們將學習 navigator.bluetooth.requestDevice() 這個核心指令,讓「掃描」按鈕真正彈出瀏覽器的藍牙掃描視窗。我們將第一次看到真實的藍牙裝置名稱出現在我們的世界裡。

那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
Day 12:專案核心 (1):createElement 動態生成 UI
下一篇
Day 14:非同步的挑戰:理解探索未知裝置的複雜性
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言