昨天,我們打造了一個「UI 工廠」函式 renderCharacteristic
。它就像一位智慧工匠,能夠根據藍牙特徵的「屬性藍圖」(properties
),精準地打造出對應的操作按鈕。點擊這些動態生成的按鈕,閉包的魔法確保了每一個按鈕都清楚地知道自己的身份。
然而,昨天的成果只是一個起點。一個「Read」按鈕,不應該只在控制台裡無聲吶喊,它需要一個地方來展示它讀取到的資料;一個「Write」按鈕,如果沒有輸入框,那它又能寫入什麼呢?
今天,我們的任務就是對昨天的「UI 工廠」進行一次重大升級。我們不再滿足於只生成陽春的按鈕,而是要為每一個特徵,都打造一個功能完備、資訊清晰的「動態互動面板 (Dynamic Interaction Panel)」。
這個面板將是我們工具的靈魂,它會包含:
清晰的資訊展示區。
針對「寫入」操作的數據輸入框。
針對「讀取」和「通知」的數值顯示區。
完成今天的工作後,我們應用的前端互動邏輯就基本大功告成了。這個面板將成為我們與藍牙裝置溝通的唯一窗口,準備好,讓我們開始精裝修吧!
在動手寫程式碼前,我們先在腦中規劃好最終成品的樣貌。對於每一個藍牙特徵,我們希望生成一個像下面這樣的卡片:
這個面板 (Panel) 是一個自給自足的元件,包含了:
標題區:顯示特徵的 UUID。
數值顯示區:一個專門用來顯示 Read
或 Notify
回傳值的地方。
互動區:根據特徵屬性,動態顯示 Read
按鈕、Write
輸入框與按鈕、Subscribe
按鈕。
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
這個變數。這就是閉包的強大之處,讓事件處理函式可以存取並操作它被創建時環境中的所有相關元素。我們的 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;
}
word-wrap: break-word;
:一個很實用的小技巧,避免過長的 UUID 字串撐破我們的版面。
font-family: 'Courier New', ...
:對於顯示數據或程式碼,使用「等寬字體」通常更清晰、更具技術感。
display: flex;
:Flexbox 是現代 CSS 佈局的利器,gap: 10px;
則可以輕鬆設定子元素之間的間距,比傳統的 margin
更方便。
今天,我們將昨天的想法推向了極致!我們不再只是生成按鈕,而是為每個特徵都建構了一個功能完善、資訊清晰的互動面板。我們的 UI 工廠現在可以產出真正專業級的元件了。
至此,我們應用程式的「前端」部分,也就是使用者看得到、摸得著的所有介面和互動邏輯,已經完全準備就緒。我們已經做好了萬全的準備,來接收和展示真實的藍牙數據。
從明天開始,我們將暫時告別 UI 的打造,轉而正面迎擊 Web Bluetooth API。
我們將學習 navigator.bluetooth.requestDevice()
這個核心指令,讓「掃描」按鈕真正彈出瀏覽器的藍牙掃描視窗。我們將第一次看到真實的藍牙裝置名稱出現在我們的世界裡。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。