在過去幾天,我們完成了所有基礎建設:我們有了能思考的「大腦」(JS)、能展示的「身體」(HTML/CSS),以及連接兩者的「神經系統」(DOM)。我們甚至學會了如何監聽事件,讓掃描按鈕在被點擊時,能在控制台裡說「Hello」。
但是,一個真正的應用程式,不只是對用户的操作做出「回應」,它更需要根據未知的、動態的資料來**「創造」**新的介面。
我們的「通用 BLE 偵錯工具」之所以強大,就在於它的「通用」二字。我們事先不知道即將連接的藍牙裝置是什麼品牌、有什麼功能。它可能有心率服務,也可能有電量服務;它的某個特徵可能是可讀的,另一個則可能是只能訂閱的。
因此,我們的介面絕不能寫死在 HTML 裡。它必須由 JavaScript 在獲取到裝置資訊後,即時、動態地生成。今天,我們就要來打造這個專案的視覺化核心——一個能夠根據藍牙特徵屬性,動態生成對應 UI 元素(如讀取按鈕、寫入框)的「UI 工廠」。這不僅考驗我們對 DOM 的操作,更是對閉包概念的終極實戰!
要讓 JavaScript 從無到有地創造出 HTML 元素,只需要遵循三個簡單的步驟。
document.createElement('標籤名')
- 創造功能:這個方法就像一個 3D 列印機,你告訴它要創造什麼零件('div', 'p', 'button' 等),它就會在 JavaScript 的記憶體中創造出一個對應的元素。
重點:此刻,這個新元素只存在於記憶體中,使用者在網頁上是看不到它的。
// 創造一個 div 元素
const myDiv = document.createElement('div');
// 創造一個 button 元素
const myButton = document.createElement('button');
element.屬性 = '值'
- 設定功能:剛創造出來的元素是空白的。這一步就是對這個「零件」進行加工,設定它的外觀、內容和身份。
常用屬性:
.textContent
: 設定元素內部的文字內容。
.id
: 設定元素唯一的 ID。
.className
: 設定元素的 CSS class,方便我們用之前寫好的樣式來美化它。
// 設定 myDiv 的 class
myDiv.className = 'service-card'; // 假設我們在 style.css 裡定義了這個 class
// 設定 myButton 的文字
myButton.textContent = '讀取數值';
父層元素.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>
現在,讓我們把「三劍客」組合起來,寫一個專門用來生成「服務 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.name
是 null
或 undefined
(不存在),就使用後面的 '未知服務'
當作預設值。現在,重新整理你的 index.html
,你應該能看到一張漂亮的服務卡片出現在頁面上了!
這才是今天的重頭戲。一個服務卡片下面,需要顯示它所包含的各個特徵,以及可以對這些特徵做的操作(讀、寫、訂閱)。
繼續在 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 工廠。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。