区块链钱包开发(十二)—— 前后端状态同步机制

83 阅读3分钟

前言

我们希望所有前端操作(例如账户或网络的切换)引起的状态变化都可以同步到后端以及所有打开的页面(例如同时打开了popup和全屏页,在popup上的操作引起的变化要实时同步到全屏页)。

同时我们也希望所有的控制器内部状态的主动变化例如账户余额,gas费等,都可以在UI界面实时同步显示。我们这一节将讨论如何实现这一前后端双向状态同步机制。

本章涉及到的源码:

github.com/MetaMask/co… github.com/MetaMask/me…

github.com/MetaMask/me…

github.com/MetaMask/me… github.com/MetaMask/me… github.com/MetaMask/me…

建立前后端通信通道

前端页面一共有三种形式:

  • popup页面:点击浏览器扩展的主操作页面
  • fullscreen页面:popup页面的全屏网页形式
  • notification页面:DAPP例如需要连接钱包,授权签名等操作弹出的独立页面

当页面加载时会建立与后端的通信通道:

//ui.js

start().catch(log.error);

// 启动 UI 主流程,建立与后台的通信
async function start() {
  // 获取当前窗口类型(popup、notification、fullscreen等)
  const windowType = getEnvironmentType();

  // 连接浏览器扩展后台,创建专属端口
  const extensionPort = browser.runtime.connect({ name: windowType });

  // 用 PortStream 封装端口,便于后续多路复用
  const connectionStream = new PortStream(extensionPort);

  // 创建多路复用流
  const subStreams = connectSubstreams(connectionStream);

  // 创建 UI 与后台控制器通信的 JSON-RPC 客户端
  const backgroundConnection = metaRPCClientFactory(subStreams);

  // 绑定 UI 与后台的同步机制,监听后台推送的状态变更
  connectToBackground(backgroundConnection, handleStartUISync);
}

// 绑定 UI 与后台的同步机制,处理后台推送的通知
export const connectToBackground = (backgroundConnection) => {
  // 设置全局 backgroundConnection,供 UI 其他模块使用
  setBackgroundConnection(backgroundConnection);

  // 监听后台的 JSON-RPC 通知
  backgroundConnection.onNotification(async (data) => {
    const { method } = data;
    if (method === 'sendUpdate') {
      // 后台主动推送最新状态,UI 用 redux action 更新本地状态
      const store = await reduxStore.promise;
      store.dispatch(actions.updateMetamaskState(data.params[0]));
    } else {
      // 未处理的通知类型,抛出异常便于调试
      throw new Error(
        `Internal JSON-RPC Notification Not Handled:\n\n ${JSON.stringify(
          data,
        )}`,
      );
    }
  });
};

这里会建立与后台的通信通道backgroundConnection,前端监听后端发出的sendUpdate事件然后更新redux状态。关于通信流的建立如果有疑问可以参考第四章节的内容。同时前端也可以通过backgroundConnection调用后端暴露的一系列方法。

前端状态同步到后端

1. 前端发起状态变更

用户操作触发

// 例如:用户点击切换账户
const handleAccountSwitch = (address) => {
  dispatch(actions.setSelectedAccount(address));
};

Redux Action 定义

// ui/store/actions.js
export const setSelectedAccount = (address) => {
    // 调用后台方法
    await submitRequestToBackground('setSelectedInternalAccount', [accountId]);
    // 拉取同步最新状态
    await forceUpdateMetamaskState(dispatch);
  };
};

2. 通过 backgroundConnection 调用后台

// metaRPCClientFactory.ts 处理
async send(payload) {
  return new Promise((resolve, reject) => {
    this.requests.set(payload.id, { resolve, reject });
    this.backgroundConnection.write(payload);
  });
}

// 实际发送的 JSON-RPC 请求
{
  "jsonrpc": "2.0",
  "id": 123,
  "method": "setSelectedAccount", 
  "params": ["0x1234..."]
}

3. 后台接收和处理

后台接收请求

// metamask-controller.js
// 调用注册的api
setSelectedAccount: (address) => {
  const account = this.accountsController.getAccountByAddress(address);
  if (account) {
    this.accountsController.setSelectedAccount(account.id);
  } else {
    throw new Error(`No account found for address: ${address}`);
  }
},

Controller 处理状态变更

// PreferencesController.ts
async setSelectedAccount(address) {
  // 1. 验证参数
  if (!address || typeof address !== 'string') {
    throw new Error('Invalid address');
  }
  
  // 2. 更新内存状态
  this.state.selectedAccount = address;
  
  // 3. 触发状态变更事件
  this.emit('stateChange', this.state);
}

4. 通知所有前端页面

后台发送通知

// metamask-controller.js

  const patchStore = new PatchStore(this.memStore);
  // 控制器内部变化触发update事件(上一节讲过)
  this.on('update', handleUpdate);
  
  const handleUpdate = () => {
    // 收集发生变化的状态,通知所有连接的页面
    const patches = patchStore.flushPendingPatches();
    outStream.write({
      jsonrpc: '2.0',
      method: 'sendUpdate',
      params: [patches],
    });
  };

前端接收通知

// ui/index.js - connectToBackground
backgroundConnection.onNotification(async (data) => {
  const { method } = data;
  if (method === 'sendUpdate') {
    // 更新本地 Redux store
    const store = await reduxStore.promise;
    store.dispatch(actions.updateMetamaskState(data.params[0]));
  }
});

完整流程图

sequenceDiagram
  participant User as 用户操作
  participant UI as 前端UI (Redux)
  participant BGConn as backgroundConnection
  participant BG as 后台Controller
  participant Patch as PatchStore/通知

  User->>UI: dispatch(actions.setSelectedAccount(address))
  UI->>BGConn: submitRequestToBackground('setSelectedInternalAccount', [accountId])
  BGConn->>BG: 发送JSON-RPC请求 setSelectedAccount
  BG->>BG: 验证参数,更新内存状态
  BG->>BG: emit('stateChange', this.state)
  BG->>Patch: PatchStore收集变更
  BG->>BG: on('update', handleUpdate)
  BG->>Patch: patchStore.flushPendingPatches()
  Patch->>BGConn: outStream.write({method:'sendUpdate', params:[patches]})
  BGConn->>UI: onNotification({method:'sendUpdate', params:[patches]})
  UI->>UI: store.dispatch(updateMetamaskState(patches))
  UI->>BGConn: forceUpdateMetamaskState(dispatch)  
  BGConn->>BG: getState
  BG->>BGConn: 返回全量最新状态
  BGConn->>UI: updateMetamaskState(全量最新状态)
  UI->>UI: store.dispatch(updateMetamaskState(全量最新状态))

后端状态同步到前端

关键环节解析

1. 控制器状态更新

钱包的所有控制器基于 base-controller,使用消息系统进行通信,每当控制器状态发生变化都通过消息总线messagingSystem发出stateChange事件。

// 在 BaseController.ts

update(callback) {
  const oldState = { ...this.state };
  
  // 更新状态
  callback(this.state);
  
  // 发布状态变化事件
  this.messagingSystem.publish(
    `${this.name}:stateChange`,
    this.state
  );
}

2. ComposableObservableStore 状态聚合

ComposableObservableStore 订阅各个控制器的状态变化,config代表所有控制器的集合,我们在初始化时订阅消息总线的stateChange事件,onStateChange方法会调用真正的处理函数把状态发送到UI端。

// ComposableObservableStore.js
updateStructure(config) {
  // ...
  for (const key of Object.keys(config)) {
    // ...
    this.controllerMessenger.subscribe(
      `${store.name}:stateChange`,
      (state) => {
        // ...
        this.#onStateChange(key, updatedState);
      },
    );
  }
  // ...
}

3. PatchStore 生成补丁

PatchStore 监听 ComposableObservableStore 的状态变化:

// PatchStore.ts
constructor(observableStore: ComposableObservableStore) {
  // ...
  this.observableStore.on('stateChange', this.listener);
}

private _onStateChange({ oldState, newState }) {
  // ...
  const patches = this._generatePatches(oldState, sanitizedNewState);
  // ...
  for (const patch of patches) {
    const path = patch.path.join('.');
    this.pendingPatches.set(path, patch);
  }
}

4. 向 UI 发送更新

setupControllerConnection 中设置的 handleUpdate 函数负责将补丁发送到 UI:

// metamask-controller.js
const handleUpdate = () => {
  // ...
  const patches = patchStore.flushPendingPatches();
  outStream.write({
    jsonrpc: '2.0',
    method: 'sendUpdate',
    params: [patches],
  });
};

完整流程

  1. 控制器状态变化

    // 例如在某个控制器中
    this.update((state) => {
      state.someProperty = newValue;
    });
    
  2. 发布 stateChange 事件

    // 在 BaseController.js
    update(callback) {
      // 更新状态...
      this.messagingSystem.publish(`${this.name}:stateChange`, this.state);
    }
    
  3. ComposableObservableStore 的 #onStateChange 被调用

    // ComposableObservableStore.js
    #onStateChange(controllerKey, newState) {
      const oldState = this.getState()[controllerKey];
      this._putState(newState);
      this.emit('update', newState);
      this.emit('stateChange', { oldState, newState, controllerKey });
    }
    
  4. MetaMaskController 监听到 'update' 事件

    // // MetaMaskController 构造函数中
    this.memStore.on('update', this.sendUpdate.bind(this));
    
  5. 调用 sendUpdate 方法

    // MetaMaskController 构造函数中
    this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200);
     privateSendUpdate() {
      // 发出 update 事件
      this.emit('update', this.getState());
    }
    
  6. handleUpdate 方法处理状态更新

    // metamask-controller.js
    
    this.on('update', handleUpdate);
    
    const patchStore = new PatchStore(this.memStore);
    const handleUpdate = () => {
      if (!isStreamWritable(outStream) || !uiReady) {
        return;
      }
      const patches = patchStore.flushPendingPatches();
      outStream.write({
        jsonrpc: '2.0',
        method: 'sendUpdate',
        params: [patches],
      });
    };
    
  7. UI 层接收更新并更新 Redux 状态

    // index.js
    backgroundConnection.onNotification((data) => {
      if (data.method === 'sendUpdate') {
        reduxStore.dispatch(actions.updateMetamaskState(data.params[0]));
      }
    });
    
  8. React 组件重新渲染

    // React 组件通过 connect 或 hooks 连接到 Redux store
    const mapStateToProps = (state) => ({
      accounts: state.metamask.accounts,
      // ...其他状态
    });
    

完整流程图

flowchart TD
    A["Controller 内部调用 update()方法"] -- 发布 --> B(["controllerName:stateChange 事件"])
    B -- 触发 --> C["执行ComposableObservableStore.onStateChange方法"]
    C --> D["this.updateState({ [controllerKey]: newState })更新内存中的controller状态"]
    C -- 发布 --> E(["stateChange 事件"])
    C -- 发布 --> Z(["update 事件"])
    E -- 触发 --> F["执行PatchStore._onStateChange方法"] 
    Z -- 触发 --> I["执行metamask-controller.privateSendUpdate方法"]
    F --> G["捕获状态变化并生成状态补丁"]
    I -- 发布 --> J(["update事件"])
    J -- 触发 --> K["执行handleUpdate方法"]
    K --> L["patchStore.flushPendingPatches获取状态补丁"]
    L --> M["outStream.write(patch)发送状态变化到UI"]
    M --> N["backgroundConnection.onNotification UI接收到后端通知"]
    N --> O["更新redux状态"]
    O --> P["渲染到UI"]
    n1["metamask-controller.js初始化"] --> n2["this.memStore = new ComposableObservableStore(controllers)"] & n4["this.memStore.subscribe(this.sendUpdate.bind(this))"] & n6["初始化完成"]
    n2 -- 订阅 --> B
    n3["Controller 内部状态改变"] --> A
    n4 -- 订阅 --> Z
    n5["执行后台脚本(background.js)"] --> n1
    n6 --> n7["metamask-controller.on('update', handleUpdate)"]
    n7 -- 订阅 --> J
    L -.-> G
    n8("开始") --> n3

    style n8 fill:#00C853

学习交流请添加vx: gh313061

下期预告:构建BlockTracker