前言
我们希望所有前端操作(例如账户或网络的切换)引起的状态变化都可以同步到后端以及所有打开的页面(例如同时打开了popup和全屏页,在popup上的操作引起的变化要实时同步到全屏页)。
同时我们也希望所有的控制器内部状态的主动变化例如账户余额,gas费等,都可以在UI界面实时同步显示。我们这一节将讨论如何实现这一前后端双向状态同步机制。
本章涉及到的源码:
github.com/MetaMask/co... 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]));
}
});
完整流程图
后端状态同步到前端
关键环节解析
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],
});
};
完整流程
-
控制器状态变化
javascript// 例如在某个控制器中 this.update((state) => { state.someProperty = newValue; });
-
发布 stateChange 事件
javascript// 在 BaseController.js update(callback) { // 更新状态... this.messagingSystem.publish(`${this.name}:stateChange`, this.state); }
-
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 }); }
-
MetaMaskController 监听到 'update' 事件
javascript// // MetaMaskController 构造函数中 this.memStore.on('update', this.sendUpdate.bind(this));
-
调用 sendUpdate 方法
javascript// MetaMaskController 构造函数中 this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200); privateSendUpdate() { // 发出 update 事件 this.emit('update', this.getState()); }
-
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], }); };
-
UI 层接收更新并更新 Redux 状态
javascript// index.js backgroundConnection.onNotification((data) => { if (data.method === 'sendUpdate') { reduxStore.dispatch(actions.updateMetamaskState(data.params[0])); } });
-
React 组件重新渲染
javascript// React 组件通过 connect 或 hooks 连接到 Redux store const mapStateToProps = (state) => ({ accounts: state.metamask.accounts, // ...其他状态 });
完整流程图
学习交流请添加vx: gh313061
下期预告:构建BlockTracker