本文以 miot-sdk RN插件 項目為例,完整展示從設備協(xié)議初始化、首次數(shù)據(jù)拉取,到 MobX 狀態(tài)驅動 UI,以及與米家宿主(Native)交互的全部核心代碼。
一、協(xié)議層初始化與首次數(shù)據(jù)拉?。ê暾a)
1. 建立 MobX 設備狀態(tài)樹
// productionState.js(關鍵片段)
import { createDeviceStore } from './deviceStore';
import { PROP_LIST } from './deviceProtocol/protocol';
function initDefaultValue() {
const defaultValues = {};
Object.keys(PROP_LIST).forEach(key => {
defaultValues[key] = PROP_LIST[key][2]; // 默認值取數(shù)組第三項
});
return defaultValues;
}
const state = {
deviceState: createDeviceStore(initDefaultValue()),
componentState: createComponentStore(),
};
function createDeviceStore(defaultValues) {
const store = makeAutoObservable({
...defaultValues,
// 其他 observable 屬性、action 等
});
// 關鍵:掛載協(xié)議方法
addMQTTMethod(store);
return store;
}
2. addMQTTMethod:掛載協(xié)議單例和發(fā)送方法
// productionState.js
import DeviceProtocol from './deviceProtocol/deviceProtocol';
export function addMQTTMethod(store) {
// 創(chuàng)建協(xié)議單例(傳入 store 引用)
const protocol = new DeviceProtocol(store);
// 將發(fā)送方法掛載到 store 上,供 UI 調用
store.sendSetProperty = (params) => {
protocol.deviceProtocolEmitter.emit('protocolSetProperty', params);
};
store.sendAction = (params) => {
protocol.deviceProtocolEmitter.emit('protocolAction', params);
};
}
3. DeviceProtocol 構造函數(shù)(首次初始化)
// deviceProtocol.js
class DeviceProtocol {
constructor(store) {
if (DeviceProtocol.instance) {
return DeviceProtocol.instance;
}
this.deviceState = store;
this.initDeviceProtocol();
DeviceProtocol.instance = this;
}
initDeviceProtocol() {
this.initProtocolMap(); // 建立 siid.piid → 業(yè)務字段映射
this.createEventEmitter(); // 創(chuàng)建 mitt 事件總線
this.subscribeMessage(); // 訂閱設備上報
// 設備在線則立即拉取首屏數(shù)據(jù)(leading: true)
if (Device.isOnline) {
this.debounceInitProtocolState(); // 內部使用 lodash.debounce,leading: true
}
}
initProtocolMap() {
this.protocolMap = new Map();
Object.entries(PROP_LIST).forEach(([key, value]) => {
const [siid, piid] = value;
this.protocolMap.set(`prop.${siid}.${piid}`, key);
});
}
createEventEmitter() {
this.deviceProtocolEmitter = mitt(); // 創(chuàng)建內部事件總線
// 監(jiān)聽 setState 事件,直接寫入 MobX
this.deviceProtocolEmitter.on('setState', (newState) => {
this.deviceState.setState(newState);
});
// 監(jiān)聽協(xié)議動作,調用云端方法
this.deviceProtocolEmitter.on('protocolSetProperty', (params) => {
this.callSetProperty(params);
});
// ... 類似 protocolAction
}
}
?? 關于 emit 和事件總線
上述代碼中的 deviceProtocolEmitter = mitt() 創(chuàng)建了一個極簡的發(fā)布/訂閱事件總線。
-
emit('setState', toRefreshData)表示:在該總線上觸發(fā)名為'setState'的事件,并攜帶toRefreshData作為參數(shù)。 - 誰在監(jiān)聽?
createEventEmitter()中注冊的on('setState', (state) => { this.deviceState.setState(state) })會收到這條消息,并執(zhí)行 MobX 的setState更新 store。
為什么要用 emit 而不直接調用 setState?
因為協(xié)議層有多個不同來源(訂閱回調、get_properties 成功回調、sendAction 失敗后的刷新等)需要更新設備狀態(tài)。統(tǒng)一使用 emit('setState', data) 發(fā)事件,再由唯一監(jiān)聽者 onSetState 負責寫入 MobX,可以解耦數(shù)據(jù)來源與存儲邏輯,避免到處持有 store 引用。同時,同一個 emitter 也用于發(fā)送 protocolAction、protocolSetProperty 等控制指令,保持內部通信一致。
4. subscribeMessage:訂閱設備推送(完整代碼)
// deviceProtocol.js
subscribeMessage() {
// 監(jiān)聽原生層推送的消息
const { remove } = DeviceEvent.deviceReceivedMessages.addListener(
(device, messages = new Map()) => {
const toRefreshData = {};
messages.forEach((value, key) => {
// key 格式如 "prop.2.1",通過 protocolMap 轉為業(yè)務字段名
if (this.protocolMap.has(key)) {
toRefreshData[this.protocolMap.get(key)] = value;
}
});
if (Object.keys(toRefreshData).length > 0) {
this.deviceProtocolEmitter.emit('setState', toRefreshData);
}
},
);
this.removeSubscription = remove;
// 向原生層注冊訂閱(告訴云端需要推送哪些屬性和事件)
const subscribeData = this.createSubscribeData(); // 從 PROP_LIST/EVENT_LIST 生成
Device.getDeviceWifi().subscribeMessages(subscribeData)
.catch(err => console.error('訂閱失敗', err));
}
createSubscribeData() {
const props = Object.values(PROP_LIST).map(([siid, piid]) => `prop.${siid}.${piid}`);
const events = Object.values(EVENT_LIST).map(([siid, eiid]) => `event.${siid}.${eiid}`);
return [...props, ...events];
}
5. 首次拉取設備屬性(initProtocolStateFromCloud)
// deviceProtocol.js
async initProtocolStateFromCloud() {
// 篩選需要主動拉取的屬性(規(guī)則:數(shù)組長度≤3 或 第4項為 true)
const toFetch = Object.entries(PROP_LIST)
.filter(([_, def]) => def.length <= 3 || def[3] === true)
.map(([key, [siid, piid]]) => ({ siid, piid, key }));
// 分批,每批最多30個
const batchSize = 30;
const batches = [];
for (let i = 0; i < toFetch.length; i += batchSize) {
batches.push(toFetch.slice(i, i + batchSize));
}
const results = await Promise.all(batches.map(batch => {
const params = batch.map(({ siid, piid }) => ({ siid, piid }));
return Device.getDeviceWifi().callMethodFromCloud('get_properties', params);
}));
const newState = {};
results.forEach(res => {
res.result.forEach(item => {
const key = toFetch.find(({ siid, piid }) =>
siid === item.siid && piid === item.piid
)?.key;
if (key) newState[key] = item.value;
});
});
newState.loadingVisible = false;
this.deviceProtocolEmitter.emit('setState', newState);
}
首屏數(shù)據(jù)閉環(huán)示意:
createDeviceStore → addMQTTMethod → new DeviceProtocol →
subscribeMessage (監(jiān)聽+訂閱) → debounceInitProtocolState (leading) →
initProtocolStateFromCloud → get_properties → emit('setState') →
deviceState.setState → UI 通過 observer 自動刷新
二、與米家宿主的交互(Native 上下文代碼示例)
1. 插件入口
// index.js
import { Package, DarkMode } from 'miot';
import Root from './root';
DarkMode.preparePluginOwnDarkMode();
Package.entry(Root, () => console.log('root loaded'));
2. 獲取設備信息與調用宿主功能
// main/page/DeviceInfo/index.js
import { Device, Host } from "miot";
// 設備名稱展示及修改
{
title: '設備名稱',
showArrow: Device.isOwner,
rightExtend: miotSdkProp.Device.name, // 動態(tài)名稱
onPress: () => {
if (Device.isOwner) {
Host.ui.openChangeDeviceName(); // 調用宿主改名界面
}
}
},
{
title: '設備類型',
rightExtend: Device.device.displayName // 靜態(tài)展示名
},
{
title: '設備編號',
rightExtend: Device.deviceID
}
3. 退出插件
// main/page/index.js
<HeaderBackButton onPress={Package.exit} />
三、MobX 狀態(tài)管理與界面使用(含完整 Hook 和組件示例)
1. 定義全局 Store 與 Context
// productionState.js
import React, { createContext, useContext } from 'react';
const state = {
deviceState: createDeviceStore(initDefaultValue()),
componentState: createComponentStore(),
};
const StateContext = createContext(state);
export const StateContextProvider = ({ children }) => (
<StateContext.Provider value={state}>{children}</StateContext.Provider>
);
const useStores = () => {
const stores = useContext(StateContext);
if (!stores) throw new Error('useStores must be used within a StoreProvider');
return stores;
};
export const useDeviceState = () => useStores().deviceState;
export const useComponentState = () => useStores().componentState;
2. 在根組件注入 Provider
// root.js
import { StateContextProvider } from './main/useHooks/productionState';
export default function Root() {
return (
<SafeAreaProvider>
<StateContextProvider>
<MenuProvider>
<App />
</MenuProvider>
</StateContextProvider>
</SafeAreaProvider>
);
}
3. 界面讀取狀態(tài)(完整組件示例)
示例 A:根據(jù)設備狀態(tài)切換頁面
// main/page/Home/index.js
import { observer } from 'mobx-react-lite';
import { useDeviceState, useComponentState } from '../../useHooks/productionState';
const PageSwitcher = observer(({ navigation }) => {
const deviceState = useDeviceState();
const componentState = useComponentState();
if ([3, 4].includes(deviceState.deviceStatus) ||
(deviceState.notification && deviceState.deviceStatus === 1)) {
return <WorkingStatus />;
}
if ([0, 1, 2, 5, 6, 400].includes(deviceState.deviceStatus)) {
return <DeviceController navigation={navigation} />;
}
return null;
});
示例 B:動態(tài)控制 Tab 禁用狀態(tài)
// main/page/index.js
const App = observer((props) => {
const deviceState = useDeviceState();
const tabList = [
{ name: '設備' },
{ name: '菜譜', disabled: [400, 5, 6].includes(deviceState.deviceStatus) }
];
// ... 渲染 Tabs
});
四、下發(fā)指令(寫屬性 / 調動作)完整代碼
// 任意 UI 組件中
const SomeControl = observer(() => {
const deviceState = useDeviceState();
const turnOnLight = () => {
deviceState.sendSetProperty({
name: 'light', // 必須與 PROP_LIST 中的 key 一致
value: 1,
resolveCallback: () => console.log('成功'),
rejectCallback: (err) => console.error(err),
});
};
const startWork = () => {
deviceState.sendAction({
name: 'startWork', // 必須與 EVENT_LIST 中的 key 一致
value: { mode: 2 },
resolveCallback: () => {},
rejectCallback: () => {},
});
};
return <Button onPress={turnOnLight} title="開燈" />;
});
內部調用鏈:
sendSetProperty → deviceProtocolEmitter.emit('protocolSetProperty') →
callSetProperty → Device.getDeviceWifi().callMethodFromCloud('set_properties', params)
五、協(xié)議字段定義(PROP_LIST 示例)
// main/useHooks/deviceProtocol/protocol.js
export const PROP_LIST = {
'deviceStatus': [2, 1, 400], // [siid, piid, 默認值]
'errorCode': [2, 2, 0],
'workMode': [2, 3, 0],
'temperature1': [2, 5, 0],
'power': [2, 7, 0],
'step': [2, 8, 0],
'toWorkConfig': [2, 9, ''], // 字符串默認值
// 若需要首次主動拉取,可加第4個參數(shù) true,如:
'someImportant': [3, 1, 0, true],
};
六、完整數(shù)據(jù)流總結圖(文字版)
| 方向 | 觸發(fā)點 | 核心代碼 | 終點 |
|---|---|---|---|
| 首次拉取 | debounceInitProtocolState |
initProtocolStateFromCloud → get_properties → emit('setState')
|
MobX deviceState
|
| 設備上報 | DeviceEvent.deviceReceivedMessages |
subscribeMessage 監(jiān)聽 → protocolMap 轉換 → emit('setState')
|
MobX deviceState
|
| UI 下發(fā) |
sendSetProperty / sendAction
|
callMethodFromCloud('set_properties'/'action') |
云端 |
| UI 響應 |
observer 組件使用 useDeviceState()
|
MobX 自動通知重渲染 | 界面刷新 |
關鍵文件索引:
-
productionState.js– MobX store、Context、addMQTTMethod -
deviceProtocol.js– 協(xié)議單例、訂閱、get_properties、callMethodFromCloud,以及內部mitt事件總線 -
protocol.js–PROP_LIST/EVENT_LIST與 SIID/PIID 映射
七、useHooks 目錄角色與依賴關系
為了更清晰地理解各模塊職責,下面列出 main/useHooks 下主要文件的作用及其依賴關系。
1. 目錄角色一覽
| 文件 / 目錄 | 作用(一句話) |
|---|---|
deviceProtocol/protocol.js |
純配置:屬性表 PROP_LIST、事件表 EVENT_LIST 及相關名稱常量,不依賴本目錄其它業(yè)務文件。 |
deviceProtocol/deviceProtocol.js |
協(xié)議實現(xiàn):DeviceProtocol 類,負責訂閱/拉取/下發(fā),內部用 protocol.js 做 siid/piid 映射,用 mitt 發(fā)事件。 |
productionState.js |
全局設備狀態(tài)(MobX):創(chuàng)建 store、掛 sendSetProperty / sendAction / setState,內部 new DeviceProtocol(store),頁面用 useDeviceState()。 |
mockState.js |
本地 Mock 用的 MobX store,與真實協(xié)議解耦(主要配合調試/演示)。 |
useMiotSdk/index.js |
封裝 MIOT Device / 設備改名監(jiān)聽等小 hook,不直接依賴 deviceProtocol。 |
useDeviceData.js |
占位/未完成的小 hook,與其它模塊幾乎無耦合。 |
2. 依賴關系圖(誰引用誰)

依賴要點:
-
protocol.js在最底層:只導出常量,被deviceProtocol.js和productionState.js引用。 -
deviceProtocol.js在中間:只依賴protocol.js(以及miot、mitt、RN 等外部包),實現(xiàn)「讀屬性 / 寫屬性 / 動作」的具體邏輯。 -
productionState.js在最上層(業(yè)務入口之一):把 MobX store 交給DeviceProtocol,并在 store 上掛sendSetProperty/sendAction(內部是deviceProtocolEmitter.emit(...))。
數(shù)據(jù)流可以理解為:
UI → sendSetProperty(store 上的方法)→ mitt → DeviceProtocol 里監(jiān)聽 → 查 PROP_LIST → set_properties 等。
3. 以另外一種工程(多一個層次)
在另外一個工程中,使用的是 deviceProtocol/useProtocol.js,其設計略有不同:不經(jīng)過 productionState,而是用 useProtocol(routeName) 在頁面級維護 protocolState,同樣內部 new DeviceProtocol + 同樣的 emit('protocolSetProperty') 鏈路。
依賴關系仍然為:
useProtocol.js → protocol.js + deviceProtocol.js(+ 頁面?zhèn)?setIsloadingRef 等)。
4. 一句話記憶
-
protocol.js:字典(屬性/動作編號表)。 -
deviceProtocol.js:按字典跟設備/云端打交道 + 事件總線。 -
productionState.js(微波)或useProtocol.js(其他機器):把「狀態(tài) + 發(fā)送方法」接到 React/MobX 上給界面用;mockState/useMiotSdk/useDeviceData則是旁路工具,不在主協(xié)議鏈上。