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

193 阅读4分钟

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

// 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);
};

通信流程

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

实际调用示例

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

2. UI 核心模块详解

2.1 ui/store - 状态管理中心

actions.ts - 后台方法桥接

// 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 配置

// 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 - 数据选择与派生

选择器的作用

// 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 友好的格式
  • 依赖管理:声明式地管理数据依赖关系

选择器层次结构

// 基础选择器
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 的作用

// 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 与选择器的协作

// 选择器:纯数据获取
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 模式的作用

// 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. 完整的数据流示例

场景:用户添加新账户

// 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

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