miot-sdk RN插件,數(shù)據(jù)流與狀態(tài)管理

本文以 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 initProtocolStateFromCloudget_propertiesemit('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_propertiescallMethodFromCloud,以及內部 mitt 事件總線
  • protocol.jsPROP_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. 依賴關系圖(誰引用誰)

hooks文件依賴關系

依賴要點

  • protocol.js 在最底層:只導出常量,被 deviceProtocol.jsproductionState.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 上的方法)→ mittDeviceProtocol 里監(jiān)聽 → 查 PROP_LISTset_properties 等。

3. 以另外一種工程(多一個層次)

在另外一個工程中,使用的是 deviceProtocol/useProtocol.js,其設計略有不同:不經(jīng)過 productionState,而是用 useProtocol(routeName) 在頁面級維護 protocolState,同樣內部 new DeviceProtocol + 同樣的 emit('protocolSetProperty') 鏈路。

依賴關系仍然為:

useProtocol.jsprotocol.js + deviceProtocol.js(+ 頁面?zhèn)?setIsloadingRef 等)。

4. 一句話記憶

  • protocol.js:字典(屬性/動作編號表)。
  • deviceProtocol.js:按字典跟設備/云端打交道 + 事件總線。
  • productionState.js(微波)或 useProtocol.js(其他機器):把「狀態(tài) + 發(fā)送方法」接到 React/MobX 上給界面用;mockState / useMiotSdk / useDeviceData 則是旁路工具,不在主協(xié)議鏈上。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容