区块链钱包开发(二十)—— 前端框架和页面

源码: github.com/MetaMask/me...

0.整体架构

MetaMask UI端采用了分层架构设计,每一层都有明确的职责:

  • UI层:负责用户界面的展示和交互
  • Hooks层:封装业务逻辑和状态管理
  • Selectors层:提供高效的数据访问和派生
  • Ducks层:模块化的状态管理,每个功能域独立
  • Store层:统一的Redux状态管理中心
  • Background Connection:UI与后台的通信桥梁
  • 后台脚本:核心业务逻辑和数据处理
graph TB subgraph "MetaMask UI 架构" subgraph "UI层" A1[账户管理组件] A2[交易确认组件] A3[设置页面组件] A4[代币列表组件] end subgraph "Hooks层" B1[useTokenTracker] B2[useGasEstimates] B3[useNotifications] B4[useAccountBalance] end subgraph "Selectors层" C1[Account Selectors] C2[Transaction Selectors] C3[Network Selectors] C4[UI State Selectors] end subgraph "Ducks层" D1[Metamask Duck] D2[App State Duck] D3[Send Duck] D4[Swaps Duck] D5[Gas Duck] D6[History Duck] end subgraph "Store层" E1[Redux Store] E2[Root Reducer] E3[Middleware] end subgraph "Background Connection" F1[submitRequestToBackground] F2[onNotification] F3[setBackgroundConnection] end subgraph "后台脚本" G1[Controllers] G2[Services] G3[Utils] end end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B4 B1 --> C1 B2 --> C2 B3 --> C3 B4 --> C4 C1 --> D1 C2 --> D3 C3 --> D1 C4 --> D2 D1 --> E2 D2 --> E2 D3 --> E2 D4 --> E2 D5 --> E2 D6 --> E2 E2 --> E1 E1 --> F1 E1 --> F2 E1 --> F3 F1 --> G1 F2 --> G2 F3 --> G3

1.UI → Background 交互机制

核心通信桥梁:background-connection.ts

typescript 复制代码
// metamask-extension/ui/store/background-connection.ts
export const callBackgroundMethod = <R>(
  method: string,           // Background 方法名
  args: unknown[],          // 参数数组
  callback: (error: Error | null, result?: R) => void  // 回调函数
): void => {
  // 通过 postMessage 与 Background 通信
  const message = {
    id: generateId(),
    method,
    args,
    origin: window.location.origin,
  };
  
  // 发送到 Background
  window.postMessage(message, '*');
  
  // 监听 Background 的响应
  window.addEventListener('message', handleResponse);
};

通信流程

css 复制代码
UI 组件 → callBackgroundMethod → postMessage → Background Service Worker
    ↓
Background 处理请求 → 调用对应 Controller → 返回结果
    ↓
Background → postMessage → UI → 回调处理结果

实际调用示例

typescript 复制代码
// 在 UI 中调用 Background 方法
callBackgroundMethod(
  'addNewAccount',           // Background 方法名
  [],                        // 参数
  (error, result) => {       // 回调
    if (error) {
      console.error('添加账户失败:', error);
    } else {
      console.log('新账户地址:', result);
    }
  }
);

2. UI 核心模块详解

2.1 ui/store - 状态管理中心

actions.ts - 后台方法桥接

typescript 复制代码
// metamask-extension/ui/store/actions.ts
export function addNewAccount() {
  return (dispatch: MetaMaskReduxDispatch) => {
    dispatch(showLoadingIndication());  // 显示加载状态
    
    return new Promise<void>((resolve, reject) => {
      // 调用 Background 方法
      callBackgroundMethod('addNewAccount', [], (err) => {
        if (err) {
          dispatch(displayWarning(err));  // 显示错误
          reject(err);
          return;
        }
        
        dispatch(showAccountsPage());     // 跳转账户页
        resolve();
      });
    })
    .then(() => dispatch(hideLoadingIndication()))
    .catch(() => dispatch(hideLoadingIndication()));
  };
}

作用

  • 封装 callBackgroundMethod 调用
  • 管理 UI 状态(加载、错误、成功)
  • 提供 Redux Thunk 接口

store.ts - Redux Store 配置

typescript 复制代码
// metamask-extension/ui/store/store.ts
export function configureStore(initialState = {}) {
  const store = createStore(
    rootReducer,
    initialState,
    compose(
      applyMiddleware(thunk, logger),
      // 其他中间件...
    )
  );
  
  return store;
}

作用

  • 配置 Redux Store
  • 集成中间件(thunk、logger 等)
  • 提供状态管理基础设施

2.2 ui/selectors - 数据选择与派生

选择器的作用

typescript 复制代码
// metamask-extension/ui/selectors/metamask-notifications/metamask-notifications.ts
export const getValidNotificationAccounts = createSelector(
  [getMetamask],  // 依赖选择器
  (metamask): string[] => metamask.subscriptionAccountsSeen  // 派生逻辑
);

// 使用 reselect 进行 memoization
export const getMetamaskNotificationsUnreadCount = createSelector(
  [getMetamaskNotifications],
  (notifications): number => {
    return notifications
      ? notifications.filter((notification) => !notification.isRead).length
      : 0;
  },
);

作用

  • 数据投影:从复杂状态中提取所需数据
  • 性能优化:memoization 避免不必要的重计算
  • 数据转换:将原始状态转换为 UI 友好的格式
  • 依赖管理:声明式地管理数据依赖关系

选择器层次结构

typescript 复制代码
// 基础选择器
const getMetamask = (state: AppState) => state.metamask;

// 派生选择器
export const getMetamaskNotifications = createSelector(
  [getMetamask],
  (metamask): Notification[] => metamask.metamaskNotificationsList,
);

// 复合选择器
export const getUnreadNotifications = createSelector(
  [getMetamaskNotifications],
  (notifications) => notifications.filter(n => !n.isRead)
);

2.3 ui/hooks - 业务逻辑封装

自定义 Hook 的作用

typescript 复制代码
// metamask-extension/ui/hooks/useNotificationAccounts.ts
function useNotificationAccounts() {
  // 使用选择器获取数据
  const accountAddresses = useSelector(getValidNotificationAccounts);
  const internalAccounts = useSelector(getInternalAccounts);
  
  // 业务逻辑:地址映射为账户对象
  const accounts = useMemo(() => {
    return accountAddresses
      .map((addr) => {
        const account = internalAccounts.find(
          (a) => a.address.toLowerCase() === addr.toLowerCase(),
        );
        return account;
      })
      .filter(<T,>(val: T | undefined): val is T => Boolean(val));
  }, [accountAddresses, internalAccounts]);

  return accounts;
}

作用

  • 逻辑复用:封装常用的业务逻辑
  • 状态管理:管理组件内部状态
  • 副作用处理:处理异步操作、事件订阅等
  • 数据转换:将选择器数据转换为组件所需格式

Hook 与选择器的协作

typescript 复制代码
// 选择器:纯数据获取
const getAccountBalance = createSelector(
  [getSelectedAccount, getCurrentNetwork],
  (account, network) => ({ account, network })
);

// Hook:业务逻辑 + 状态管理
function useAccountBalance() {
  const { account, network } = useSelector(getAccountBalance);
  const [balance, setBalance] = useState(null);
  
  useEffect(() => {
    if (account && network) {
      fetchBalance(account.address, network.chainId)
        .then(setBalance);
    }
  }, [account, network]);
  
  return balance;
}

2.4 ui/ducks - 功能模块化

Duck 模式的作用

typescript 复制代码
// metamask-extension/ui/ducks/notifications/notifications.js
// 一个 Duck 包含:actions、reducers、selectors、types

// Actions
export const SHOW_NOTIFICATION = 'notifications/SHOW_NOTIFICATION';
export const HIDE_NOTIFICATION = 'notifications/HIDE_NOTIFICATION';

// Action Creators
export const showNotification = (message) => ({
  type: SHOW_NOTIFICATION,
  payload: message,
});

// Reducer
const initialState = { messages: [] };
export default function notificationsReducer(state = initialState, action) {
  switch (action.type) {
    case SHOW_NOTIFICATION:
      return { ...state, messages: [...state.messages, action.payload] };
    case HIDE_NOTIFICATION:
      return { ...state, messages: state.messages.filter(m => m.id !== action.payload) };
    default:
      return state;
  }
}

// Selectors
export const getNotifications = (state) => state.notifications.messages;

作用

  • 模块化:将相关功能组织在一起
  • 可维护性:降低代码耦合度
  • 可测试性:独立测试功能模块
  • 可重用性:在不同页面间复用功能

3. 完整的数据流示例

场景:用户添加新账户

typescript 复制代码
// 1. UI 组件触发
function AddAccountButton() {
  const dispatch = useDispatch();
  
  const handleClick = () => {
    dispatch(addNewAccount());  // 派发 action
  };
  
  return <button onClick={handleClick}>添加账户</button>;
}

// 2. Action 处理
export function addNewAccount() {
  return (dispatch) => {
    dispatch(showLoadingIndication());  // 更新 UI 状态
    
    return new Promise((resolve, reject) => {
      // 调用 Background
      callBackgroundMethod('addNewAccount', [], (err, result) => {
        if (err) {
          dispatch(displayWarning(err));
          reject(err);
        } else {
          dispatch(showAccountsPage());
          resolve(result);
        }
      });
    });
  };
}

// 3. Background 处理
// app/scripts/metamask-controller.js
async addNewAccount(accountCount, _keyringId) {
  const addedAccountAddress = await this.keyringController.withKeyring(
    { type: KeyringTypes.hd, index: 0 },
    async ({ keyring }) => {
      const [newAddress] = await keyring.addAccounts(1);
      return newAddress;
    },
  );
  
  return addedAccountAddress;
}

// 4. 状态更新
// KeyringController 更新状态 → 触发 stateChange 事件
// UI 通过选择器获取新状态 → 重新渲染

4. 模块间的协作关系

sequenceDiagram participant UI as UI Component participant Hook as Custom Hook participant Selector as Selector participant Duck as Duck (Actions/Reducers) participant Store as Redux Store participant BC as Background Connection participant BG as Background Script participant State as State Update UI->>Hook: 1. 用户操作触发Hook Hook->>Selector: 2. 获取当前状态 Selector->>Store: 3. 从Store读取状态 Store->>Selector: 4. 返回状态数据 Selector->>Hook: 5. 提供状态数据 Hook->>Duck: 6. 派发Action Duck->>Store: 7. dispatch action到Store Store->>Duck: 8. 调用Reducer更新状态 Duck->>BC: 9. 调用后台方法 BC->>BG: 10. 发送请求到后台 BG->>State: 11. 处理业务逻辑 State->>BG: 12. 返回处理结果 BG->>BC: 13. 响应结果 BC->>Duck: 14. 返回后台响应 Duck->>Store: 15. 更新Store状态 Store->>Selector: 16. 通知状态变化 Selector->>Hook: 17. 提供新状态 Hook->>UI: 18. 触发组件重新渲染

5. 总结

MetaMask UI 架构的核心是:

  1. ui/store:状态管理中心,通过 actions 桥接 UI 与 Background
  2. ui/selectors:数据选择与派生,提供 memoized 的数据访问
  3. ui/hooks:业务逻辑封装,管理组件状态和副作用
  4. ui/ducks:功能模块化,组织相关功能代码

这种架构实现了:

  • 关注点分离:UI、状态、逻辑各司其职
  • 性能优化:memoization 避免无效重渲染
  • 可维护性:模块化设计降低耦合度
  • 可测试性:纯函数和独立模块便于测试

学习交流请添加vx: gh313061

下期预告:一次交易的全流程分析

相关推荐
D11_13 小时前
【React】Redux和React
前端·javascript·react.js
卿·静13 小时前
Node.js轻松生成动态二维码
前端·javascript·vscode·node.js·html5
还要啥名字13 小时前
elpis NPM包的抽离
前端
成小白14 小时前
前端实现连词搜索下拉效果
前端·javascript
卸任14 小时前
从0到1搭建react-native自动更新(OTA和APK下载)
前端·react native·react.js
OpenTiny社区14 小时前
OpenTiny NEXT 训练营实操体验 | 四步将你的 Web 应用升级为智能应用
前端·开源·ai编程
持续迷茫14 小时前
lint-staged 中 --verbose 选项的深度解析
前端·git
拜无忧14 小时前
带有“水波纹”或“扭曲”效果的动态边框,进度条
前端
12码力14 小时前
open3d 处理rgb-d深度图
前端
小猪猪屁14 小时前
WebAssembly 从零到实战:前端性能革命完全指南
前端·vue.js·webassembly