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

源码: 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

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

相关推荐
x007xyz2 分钟前
🚀🚀🚀前端的无限可能-纯Web实现的字幕视频工具 FlyCut Caption
前端·openai·音视频开发
前端Hardy3 分钟前
HTML&CSS: 在线电子签名工具
前端·javascript·canvas
前端Hardy13 分钟前
告别抽象!可视化动画带你学习算法——选择排序
前端·javascript·css
毕设十刻15 分钟前
基于vue的考研信息系统6kv17(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
望获linux22 分钟前
论文解读:利用中断隔离技术的 Linux 亚微秒响应性能优化
java·linux·运维·前端·arm开发·数据库·性能优化
brzhang35 分钟前
ChatGPT Pulse来了:AI 每天替你做研究,这事儿你该高兴还是该小心?
前端·后端·架构
泉城老铁1 小时前
springboot+vue 文件下载,实现大文件的分片压缩和下载,避免内存溢出
前端·spring boot·后端
用户203735549811 小时前
Vue+Node+MongoDB高级全栈开发视频教程 完整版
前端
我是天龙_绍1 小时前
setup 函数 和 setup 语法糖
前端
泉城老铁1 小时前
Spring Boot和Vue.js项目中实现文件压缩下载功能
前端·spring boot·后端