我們繼續新的的內容之前,我想先回顧我們過去幾天的內容,其實在前幾天的內容中我們就已經實現了將探索結果動態生成 UI的程式碼
在 Day 22,我們使用 getPrimaryServices()
探索服務後,立刻就整合了 createServiceCard
函式,將服務渲染成了 UI 卡片。
在 Day 23,我們使用 getCharacteristics()
探索特徵後,也立刻整合了 renderCharacteristic
函式,將特徵渲染成了互動面板。
在快速寫完這些環環相扣的程式碼後,我們有必要停下腳步,進行一次全面的「整合回顧」。
因此,今天我們不學習新的 API。相反地,我們將扮演「總工程師」的角色,審視我們已經建好的這條從「探索」到「渲染」的自動化流水線,徹底理解數據是如何在其中流動,以及各個模組是如何完美協作的。這將鞏固我們的知識,為接下來的數據讀寫操作,打下最堅實的理解基礎。
到昨天為止,我們的 scanButton.onclick
函式已經成為了整個專案的引擎。讓我們第一次將它完整的樣貌呈現出來,看看這條流水線的宏偉結構。
//app.js
scanButton.onclick = async () => {
// 步驟 0: 初始化
log('初始化...');
gattContainer.innerHTML = '';
logContainer.innerHTML = '';
// ... 清空 gattProfile ...
try {
// 步驟 1: 請求裝置 (觸發 UI 互動)
log('正在請求藍牙裝置...');
const device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true });
log(`> 已選擇裝置: ${device.name || `ID: ${device.id}`}`);
gattProfile.device = device;
// 步驟 2: 連接 GATT 伺服器
log('正在連接到 GATT 伺服器...');
const server = await device.gatt.connect();
log('> 成功連接到 GATT 伺服器!');
gattProfile.server = server;
statusText.textContent = `已連接至 ${device.name}`;
// 步驟 3: 探索所有服務
log('正在探索服務...');
const services = await server.getPrimaryServices();
log(`> 發現 ${services.length} 個主要服務!`);
// 步驟 4: 遍歷服務,並為每個服務探索其所有特徵
for (const service of services) {
log(`>> 正在處理服務: ${service.uuid}`);
// 4a. 更新資料模型 (服務層)
gattProfile.services[service.uuid] = {
uuid: service.uuid,
instance: service,
uiCard: null, // 先佔位
characteristics: {}
};
// 4b. 渲染 UI (服務層)
const serviceCard = createServiceCard({ uuid: service.uuid, name: `服務` });
gattContainer.appendChild(serviceCard);
gattProfile.services[service.uuid].uiCard = serviceCard;
// 步驟 5: 探索該服務下的所有特徵
log(`---> 正在探索特徵...`);
const characteristics = await service.getCharacteristics();
log(`---> 發現 ${characteristics.length} 個特徵!`);
// 步驟 6: 遍歷特徵
for (const characteristic of characteristics) {
log(`-----> 特徵 UUID: ${characteristic.uuid}`);
// 6a. 更新資料模型 (特徵層)
gattProfile.services[service.uuid].characteristics[characteristic.uuid] = {
uuid: characteristic.uuid,
properties: characteristic.properties,
instance: characteristic,
value: null
};
// 6b. 渲染 UI (特徵層)
renderCharacteristic({
uuid: characteristic.uuid,
properties: characteristic.properties
}, serviceCard);
}
}
log('所有服務與特徵探索完畢!');
} catch(error) {
log(`錯誤: ${error.message}`);
}
};
上面的程式碼看似複雜,但數據的流動路徑其實非常清晰,就像一條河的流向:
源頭 (API)
requestDevice
, getPrimaryServices
, getCharacteristics
這些非同步 API 就像是河流的源頭,它們從「真實世界」中捕獲原始的藍牙數據(device
, service
, characteristic
物件)。水庫 (Data Model - gattProfile
)
在得到原始數據後,我們做的第一件事,就是將這些數據進行結構化處理,並存入我們的「中央資料庫」——gattProfile
物件。
gattProfile
物件就像一個巨大的水庫,它不關心這些水未來要用來發電還是灌溉,它只負責忠實地、有條理地儲存所有獲取到的資訊。
引水渠道 (Function Call)
在將數據存入「水庫」後,我們立刻呼叫我們的 UI 生成函式(createServiceCard
, renderCharacteristic
)。
我們從「水庫」中取出需要的數據(例如 uuid
, properties
),作為參數,透過這個「引水渠道」傳遞給 UI 工廠。
水龍頭 (UI Factories)
createServiceCard
和 renderCharacteristic
這兩個函式,就像是終端的水龍頭。它們是「純粹的」:它們不關心藍牙,不關心 API,它們唯一的任務就是接收傳入的數據,並根據這些數據,「渲染」出對應的 HTML 元素(DOM 物件)。目的地 (DOM)
appendChild
被掛載到 DOM 上,最終呈現在使用者的螢幕上。這個從「獲取 -> 儲存 -> 渲染」的單向數據流,是現代前端開發中最核心、最穩定的架構模式之一。
透過這次回顧,我們可以清晰地看到,我們下意識地建立了一個非常健康、易於維護的程式碼架構,它依賴於兩大支柱:
支柱一:gattProfile
- 單一事實來源 (Single Source of Truth)
gattProfile
物件中的紀錄。它就是我們應用的「大腦記憶區」。支柱二:UI 工廠 - 數據與視圖分離 (Separation of Concerns)
createServiceCard
或 renderCharacteristic
函式,完全不需要觸碰任何藍牙 API 相關的程式碼,反之亦然。這使得維護和擴展變得非常容易。今天,我們沒有學習新知,而是進行了一次更有價值的全局回顧。
現在,我們的探索階段已經畫上了完美的句號。我們的應用程式已經可以像一個專業的偵錯工具一樣,完整地、動態地、即時地映射出任何一個 BLE 裝置的內部結構。
地圖已經繪製完成,寶箱的位置也已全部標出。從明天開始,我們將正式進入數據互動的激動人心的階段。我們將拿起工具箱中的第一個工具——「讀取」,學習使用 characteristic.readValue()
,第一次真正地從寶箱中取出數據,並讓它顯示在我們的介面上!
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。