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

前言

我们希望所有前端操作(例如账户或网络的切换)引起的状态变化都可以同步到后端以及所有打开的页面(例如同时打开了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例如需要连接钱包,授权签名等操作弹出的独立页面

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

js 复制代码
//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. 前端发起状态变更

用户操作触发

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

Redux Action 定义

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

2. 通过 backgroundConnection 调用后台

js 复制代码
// 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. 后台接收和处理

后台接收请求

js 复制代码
// 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 处理状态变更

js 复制代码
// 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. 通知所有前端页面

后台发送通知

js 复制代码
// 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],
    });
  };

前端接收通知

js 复制代码
// 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事件。

javascript 复制代码
// 在 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端。

javascript 复制代码
// 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 的状态变化:

javascript 复制代码
// 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:

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

完整流程

  1. 控制器状态变化

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

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

    javascript 复制代码
    // 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' 事件

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

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

    javascript 复制代码
    // 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 状态

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

    javascript 复制代码
    // 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

相关推荐
终端域名4 小时前
信用机制的发展与货币演进
区块链
选择不变4 小时前
反阶持仓筹码副图指标,三红做多持股技术及指标案例
区块链·炒股技巧·短线指标·炒股指标·翻倍密码系统
运维开发王义杰5 小时前
Ethereum:智能合约开发者的“瑞士军刀”OpenZeppelin
web3·区块链·智能合约
Joker时代6 小时前
LOOP Finance:一场 Web3 共和国中的金融制度实验
金融·web3·区块链
ithadoop1 天前
Solidity智能合约开发全攻略
区块链·智能合约
allenXer1 天前
Flask全栈入门:打造区块链艺术品交易所
python·flask·区块链
余_弦1 天前
区块链钱包开发(十一)—— 构建安全高可用的钱包数据持久化策略
区块链
加速财经1 天前
WEEX从注册到首单:新手入门完整操作流程指南
区块链
不可描述的两脚兽1 天前
学习笔记《区块链技术与应用》第六天 问答 匿名技术 零知识证明
笔记·学习·区块链