Web3.js与React构建DApp前端实战:从连接到交互

Web3.js 结合 React 开发 DApp 去中心化应用前端,钱包连接与智能合约交互实战教程封面

DApp前端开发概述

去中心化应用的前端开发与传统Web应用有着本质的不同。传统应用中,前端通过HTTP请求与后端API交互,后端再与数据库通信。而在DApp中,前端直接与区块链上的智能合约交互,所有的数据操作都发生在链上。

这种架构带来了几个独特的挑战:首先,用户需要使用加密钱包而不是传统的用户名密码登录;其次,每次写操作都需要用户签名并支付Gas费用;最后,链上数据的获取需要理解事件机制和索引服务。掌握这些差异,是成为合格DApp开发者的关键。

本教程将带你从零构建一个完整的DApp前端,涵盖钱包连接、余额查询、Token转账等核心功能。你将学会如何将Web3.js与React无缝结合,构建用户体验良好的去中心化应用。

React DApp 前端钱包连接、ERC20 代币转账、链上事件监听区块链开发界面演示

项目初始化

创建React项目

首先创建一个新的React项目,使用Vite作为构建工具可以获得更好的开发体验:

bash

npm create vite@latest my-dapp -- --template react
cd my-dapp
npm install

安装Web3.js和其他必要依赖:

bash

npm install web3 ethers @rainbow-me/rainbowkit wagmi viem
npm install @tanstack/react-query
npm install lucide-react
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

配置Tailwind CSS

更新tailwind.config.js配置:

javascript

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#10b981',
      }
    },
  },
  plugins: [],
}

添加Tailwind指令到index.css:

css

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antiali-serif;
  -moz-osx-font-smoothing: grayscale;
}

钱包连接组件

钱包连接是DApp的门面,一个好的连接体验对用户至关重要。我们将构建一个支持多种钱包的连接组件。

基础Web3Provider设置

使用ethers.js创建Web3Provider是最简单的方式:

javascript

// src/web3/web3Provider.js
import { ethers } from 'ethers';

class Web3Service {
  constructor() {
    this.provider = null;
    this.signer = null;
    this.network = null;
  }

  async connect() {
    if (typeof window.ethereum !== 'undefined') {
      try {
        // 请求钱包连接
        const accounts = await window.ethereum.request({
          method: 'eth_requestAccounts'
        });
        
        // 创建provider
        this.provider = new ethers.BrowserProvider(window.ethereum);
        this.signer = await this.provider.getSigner();
        
        // 获取网络信息
        this.network = await this.provider.getNetwork();
        
        console.log('Connected to:', accounts[0]);
        return {
          account: accounts[0],
          provider: this.provider,
          signer: this.signer,
          network: this.network
        };
      } catch (error) {
        console.error('Connection failed:', error);
        throw error;
      }
    } else {
      throw new Error('MetaMask not installed');
    }
  }

  async disconnect() {
    this.provider = null;
    this.signer = null;
    this.network = null;
  }

  isConnected() {
    return this.provider !== null;
  }
}

export const web3Service = new Web3Service();

这个服务类封装了钱包连接的核心逻辑。使用BrowserProvider(ethers v6的新API)可以自动处理现代钱包的连接请求。

钱包连接组件实现

jsx

// src/components/WalletConnect.jsx
import { useState, useEffect } from 'react';
import { web3Service } from '../web3/web3Provider';
import { formatAddress } from '../utils/format';

export function WalletConnect() {
  const [account, setAccount] = useState(null);
  const [balance, setBalance] = useState(null);
  const [isConnecting, setIsConnecting] = useState(false);
  const [error, setError] = useState(null);

  // 检查是否已连接
  useEffect(() => {
    const checkConnection = async () => {
      if (window.ethereum) {
        const accounts = await window.ethereum.request({
          method: 'eth_accounts'
        });
        
        if (accounts.length > 0) {
          try {
            const { account, signer, provider } = await web3Service.connect();
            setAccount(account);
            
            // 获取余额
            const balance = await provider.getBalance(account);
            setBalance(ethers.utils.formatEther(balance));
          } catch (err) {
            console.error('Auto-connect failed:', err);
          }
        }
      }
    };
    
    checkConnection();
    
    // 监听账户变化
    window.ethereum.on('accountsChanged', handleAccountsChanged);
    window.ethereum.on('chainChanged', handleChainChanged);
    
    return () => {
      if (window.ethereum.removeListener) {
        window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
        window.ethereum.removeListener('chainChanged', handleChainChanged);
      }
    };
  }, []);

  const handleAccountsChanged = async (accounts) => {
    if (accounts.length === 0) {
      setAccount(null);
      setBalance(null);
      await web3Service.disconnect();
    } else if (accounts[0] !== account) {
      setAccount(accounts[0]);
      // 刷新页面以获取新的余额和状态
      window.location.reload();
    }
  };

  const handleChainChanged = () => {
    // 链变化时刷新页面
    window.location.reload();
  };

  const connect = async () => {
    setIsConnecting(true);
    setError(null);
    
    try {
      const { account, provider } = await web3Service.connect();
      setAccount(account);
      
      const balance = await provider.getBalance(account);
      setBalance(ethers.utils.formatEther(balance));
    } catch (err) {
      setError(err.message);
    } finally {
      setIsConnecting(false);
    }
  };

  const disconnect = async () => {
    await web3Service.disconnect();
    setAccount(null);
    setBalance(null);
  };

  if (!account) {
    return (
      <button
        onClick={connect}
        disabled={isConnecting}
        className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-all disabled:opacity-50"
      >
        {isConnecting ? '连接中...' : '连接钱包'}
      </button>
    );
  }

  return (
    <div className="flex items-center gap-4">
      <div className="bg-gray-100 dark:bg-gray-800 rounded-lg px-4 py-2">
        <p className="text-sm text-gray-500">余额</p>
        <p className="font-bold">{parseFloat(balance).toFixed(4)} ETH</p>
      </div>
      
      <div className="bg-blue-50 dark:bg-blue-900/30 rounded-lg px-4 py-2">
        <p className="text-sm text-gray-500">地址</p>
        <p className="font-bold">{formatAddress(account)}</p>
      </div>
      
      <button
        onClick={disconnect}
        className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition-all"
      >
        断开
      </button>
    </div>
  );
}

这个组件处理了连接、断开连接、账户变化监听等完整的钱包交互逻辑。格式化地址的辅助函数可以这样实现:

javascript

// src/utils/format.js
export function formatAddress(address) {
  if (!address) return '';
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
}

export function formatEther(wei, decimals = 4) {
  const ether = parseFloat(ethers.utils.formatEther(wei));
  return ether.toFixed(decimals);
}

export function parseEther(ether) {
  return ethers.utils.parseEther(ether.toString());
}

智能合约交互

连接钱包后,下一步是与智能合约交互。我们将创建一个通用的合约服务类,封装常见的合约操作。

合约服务封装

javascript

// src/web3/contractService.js
import { ethers } from 'ethers';

export class ContractService {
  constructor(contractAddress, abi, signer) {
    this.contract = new ethers.Contract(contractAddress, abi, signer);
    this.address = contractAddress;
  }

  // 只读方法调用
  async call(methodName, ...args) {
    try {
      const result = await this.contract[methodName](...args);
      return result;
    } catch (error) {
      console.error(`Contract call ${methodName} failed:`, error);
      throw error;
    }
  }

  // 写操作(需要签名和Gas)
  async send(methodName, ...args) {
    try {
      const tx = await this.contract[methodName](...args);
      console.log('Transaction sent:', tx.hash);
      
      // 等待交易确认
      const receipt = await tx.wait();
      console.log('Transaction confirmed:', receipt.hash);
      
      return {
        hash: tx.hash,
        receipt,
        success: receipt.status === 1
      };
    } catch (error) {
      console.error(`Contract send ${methodName} failed:`, error);
      throw error;
    }
  }

  // 监听事件
  on(eventName, callback) {
    this.contract.on(eventName, (args) => {
      callback(args);
    });
  }

  // 移除事件监听
  off(eventName, callback) {
    if (callback) {
      this.contract.off(eventName, callback);
    } else {
      this.contract.removeAllListeners(eventName);
    }
  }

  // 获取历史事件
  async getPastEvents(eventName, fromBlock = 0, toBlock = 'latest') {
    return await this.contract.queryFilter(
      eventName,
      fromBlock,
      toBlock
    );
  }
}

ERC20 Token合约交互示例

javascript

// src/contracts/TokenContract.js
import { ContractService } from './contractService';

// ERC20代币ABI(简化版)
const ERC20_ABI = [
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address, uint256) returns (bool)",
  "function allowance(address, address) view returns (uint256)",
  "function approve(address, uint256) returns (bool)",
  "function transferFrom(address, address, uint256) returns (bool)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
  "event Approval(address indexed owner, address indexed spender, uint256 value)"
];

export class TokenContract extends ContractService {
  constructor(address, signer) {
    super(address, ERC20_ABI, signer);
  }

  async getTokenInfo() {
    const [name, symbol, decimals, totalSupply] = await Promise.all([
      this.call('name'),
      this.call('symbol'),
      this.call('decimals'),
      this.call('totalSupply')
    ]);
    
    return {
      name,
      symbol,
      decimals,
      totalSupply: ethers.utils.formatUnits(totalSupply, decimals)
    };
  }

  async getBalance(address) {
    const balance = await this.call('balanceOf', address);
    return balance;
  }

  async transfer(to, amount) {
    const decimals = await this.call('decimals');
    const parsedAmount = ethers.utils.parseUnits(amount.toString(), decimals);
    return await this.send('transfer', to, parsedAmount);
  }

  async approve(spender, amount) {
    const decimals = await this.call('decimals');
    const parsedAmount = ethers.utils.parseUnits(amount.toString(), decimals);
    return await this.send('approve', spender, parsedAmount);
  }
}

Token转账组件

jsx

// src/components/TokenTransfer.jsx
import { useState, useEffect } from 'react';
import { web3Service } from '../web3/web3Provider';
import { TokenContract } from '../contracts/TokenContract';
import { formatAddress, formatEther } from '../utils/format';

const TOKEN_ADDRESS = '0x1234567890123456789012345678901234567890';

export function TokenTransfer() {
  const [tokenContract, setTokenContract] = useState(null);
  const [tokenInfo, setTokenInfo] = useState(null);
  const [balance, setBalance] = useState(null);
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [txHash, setTxHash] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const initContract = async () => {
      if (web3Service.isConnected() && web3Service.signer) {
        const contract = new TokenContract(TOKEN_ADDRESS, web3Service.signer);
        setTokenContract(contract);
        
        // 获取代币信息
        const info = await contract.getTokenInfo();
        setTokenInfo(info);
        
        // 获取余额
        const signer = await web3Service.signer;
        const signerAddress = await signer.getAddress();
        const bal = await contract.getBalance(signerAddress);
        setBalance(formatEther(bal, info.decimals));
      }
    };
    
    initContract();
  }, []);

  const handleTransfer = async (e) => {
    e.preventDefault();
    setError(null);
    setTxHash(null);
    setIsLoading(true);
    
    try {
      const result = await tokenContract.transfer(recipient, amount);
      setTxHash(result.hash);
      
      // 刷新余额
      const signer = await web3Service.signer;
      const signerAddress = await signer.getAddress();
      const bal = await tokenContract.getBalance(signerAddress);
      setBalance(formatEther(bal, tokenInfo.decimals));
      
      // 清空表单
      setRecipient('');
      setAmount('');
    } catch (err) {
      setError(err.reason || err.message);
    } finally {
      setIsLoading(false);
    }
  };

  if (!tokenContract) {
    return <div>请先连接钱包</div>;
  }

  return (
    <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
      <h2 className="text-2xl font-bold mb-4">
        {tokenInfo?.name || 'Token'} ({tokenInfo?.symbol}) 转账
      </h2>
      
      <div className="mb-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
        <p className="text-gray-600 dark:text-gray-300">你的余额</p>
        <p className="text-3xl font-bold">
          {balance || '0'} {tokenInfo?.symbol}
        </p>
      </div>
      
      <form onSubmit={handleTransfer} className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-2">收款地址</label>
          <input
            type="text"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
            placeholder="0x..."
            className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"
            required
          />
        </div>
        
        <div>
          <label className="block text-sm font-medium mb-2">数量</label>
          <input
            type="number"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            placeholder="0.0"
            step="0.0001"
            className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"
            required
          />
        </div>
        
        {error && (
          <div className="p-3 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
            {error}
          </div>
        )}
        
        {txHash && (
          <div className="p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg">
            转账成功!交易哈希: {formatAddress(txHash)}
          </div>
        )}
        
        <button
          type="submit"
          disabled={isLoading}
          className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-all disabled:opacity-50"
        >
          {isLoading ? '处理中...' : '转账'}
        </button>
      </form>
    </div>
  );
}

事件监听与实时更新

区块链的状态变化主要通过事件(Events)来追踪。学会监听和使用事件是DApp开发的核心技能。

事件监听组件

jsx

// src/components/TransactionHistory.jsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { TokenContract } from '../contracts/TokenContract';

export function TransactionHistory({ tokenContract, account }) {
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!tokenContract || !account) return;

    const fetchHistoricalEvents = async () => {
      try {
        // 获取最近的转账事件
        const transferFilter = tokenContract.contract.filters.Transfer(null, account);
        const toMe = await tokenContract.getPastEvents('Transfer', -10000, 'latest');
        
        const transferFromFilter = tokenContract.contract.filters.Transfer(account, null);
        const fromMe = await tokenContract.getPastEvents('Transfer', -10000, 'latest');
        
        // 合并并排序
        const allEvents = [...toMe, ...fromMe]
          .filter(e => 
            e.args.from.toLowerCase() === account.toLowerCase() ||
            e.args.to.toLowerCase() === account.toLowerCase()
          )
          .sort((a, b) => b.blockNumber - a.blockNumber)
          .slice(0, 50);
        
        setEvents(allEvents);
      } catch (error) {
        console.error('Failed to fetch events:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchHistoricalEvents();

    // 监听新事件
    const handleNewTransfer = (from, to, value, event) => {
      if (
        from.toLowerCase() === account.toLowerCase() ||
        to.toLowerCase() === account.toLowerCase()
      ) {
        setEvents(prev => [event, ...prev].slice(0, 50));
      }
    };

    tokenContract.on('Transfer', handleNewTransfer);

    return () => {
      tokenContract.off('Transfer', handleNewTransfer);
    };
  }, [tokenContract, account]);

  if (loading) {
    return <div>加载中...</div>;
  }

  return (
    <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
      <h2 className="text-2xl font-bold mb-4">交易历史</h2>
      
      {events.length === 0 ? (
        <p className="text-gray-500">暂无交易记录</p>
      ) : (
        <div className="space-y-3">
          {events.map((event, index) => {
            const isIncoming = event.args.to.toLowerCase() === account.toLowerCase();
            return (
              <div
                key={`${event.transactionHash}-${event.logIndex}`}
                className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"
              >
                <div>
                  <p className={`font-bold ${isIncoming ? 'text-green-600' : 'text-red-600'}`}>
                    {isIncoming ? '收到' : '转出'}
                  </p>
                  <p className="text-sm text-gray-500">
                    {formatAddress(event.args.from)} → {formatAddress(event.args.to)}
                  </p>
                </div>
                <div className="text-right">
                  <p className={`font-bold ${isIncoming ? 'text-green-600' : 'text-red-600'}`}>
                    {isIncoming ? '+' : '-'}{formatEther(event.args.value, 4)}
                  </p>
                  <p className="text-xs text-gray-400">
                    Block #{event.blockNumber}
                  </p>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

网络切换与链兼容性

现代DApp通常需要支持多个区块链网络。处理网络切换是必备技能。

网络配置

javascript

// src/config/networks.js
export const NETWORKS = {
  mainnet: {
    chainId: 1,
    name: 'Ethereum Mainnet',
    rpcUrl: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID',
    blockExplorer: 'https://etherscan.io'
  },
  sepolia: {
    chainId: 11155111,
    name: 'Sepolia Testnet',
    rpcUrl: 'https://sepolia.infura.io/v3/YOUR_PROJECT_ID',
    blockExplorer: 'https://sepolia.etherscan.io'
  },
  polygon: {
    chainId: 137,
    name: 'Polygon Mainnet',
    rpcUrl: 'https://polygon-rpc.com',
    blockExplorer: 'https://polygonscan.com'
  }
};

// 切换网络函数
export async function switchNetwork(targetChainId) {
  const chainIdHex = `0x${targetChainId.toString(16)}`;
  
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: chainIdHex }]
    });
  } catch (switchError) {
    // 如果网络不存在,添加网络
    if (switchError.code === 4902) {
      const network = Object.values(NETWORKS).find(n => n.chainId === targetChainId);
      if (network) {
        await window.ethereum.request({
          method: 'wallet_addEthereumChain',
          params: [{
            chainId: chainIdHex,
            chainName: network.name,
            nativeCurrency: {
              name: 'ETH',
              symbol: 'ETH',
              decimals: 18
            },
            rpcUrls: [network.rpcUrl],
            blockExplorerUrls: [network.blockExplorer]
          }]
        });
      }
    } else {
      throw switchError;
    }
  }
}

网络切换组件

jsx

// src/components/NetworkSelector.jsx
import { NETWORKS, switchNetwork } from '../config/networks';

export function NetworkSelector({ currentChainId, onSwitch }) {
  const [isOpen, setIsOpen] = useState(false);
  const [isSwitching, setIsSwitching] = useState(false);

  const handleSwitch = async (chainId) => {
    setIsSwitching(true);
    try {
      await switchNetwork(chainId);
      onSwitch?.(chainId);
    } catch (error) {
      console.error('Network switch failed:', error);
    } finally {
      setIsSwitching(false);
      setIsOpen(false);
    }
  };

  const currentNetwork = Object.values(NETWORKS).find(
    n => n.chainId === currentChainId
  );

  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 px-4 py-2 rounded-lg"
      >
        <div className="w-3 h-3 rounded-full bg-green-500"></div>
        <span>{currentNetwork?.name || 'Unknown Network'}</span>
        <ChevronDownIcon className="w-4 h-4" />
      </button>
      
      {isOpen && (
        <div className="absolute top-full mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-xl z-50">
          {Object.values(NETWORKS).map(network => (
            <button
              key={network.chainId}
              onClick={() => handleSwitch(network.chainId)}
              disabled={isSwitching}
              className={`w-full text-left px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700 first:rounded-t-lg last:rounded-b-lg ${
                network.chainId === currentChainId ? 'bg-blue-50 dark:bg-blue-900/30' : ''
              }`}
            >
              {network.name}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

完整的DApp主界面

将所有组件整合到主应用中:

jsx

// src/App.jsx
import { useState, useEffect } from 'react';
import { WalletConnect } from './components/WalletConnect';
import { TokenTransfer } from './components/TokenTransfer';
import { TransactionHistory } from './components/TransactionHistory';
import { NetworkSelector } from './components/NetworkSelector';
import { web3Service } from './web3/web3Provider';
import { TokenContract } from './contracts/TokenContract';

const TOKEN_ADDRESS = '0x1234567890123456789012345678901234567890';

function App() {
  const [account, setAccount] = useState(null);
  const [chainId, setChainId] = useState(null);
  const [tokenContract, setTokenContract] = useState(null);

  useEffect(() => {
    const checkConnection = async () => {
      if (window.ethereum) {
        const accounts = await window.ethereum.request({
          method: 'eth_accounts'
        });
        
        if (accounts.length > 0) {
          setAccount(accounts[0]);
          
          const provider = new ethers.BrowserProvider(window.ethereum);
          const signer = await provider.getSigner();
          setTokenContract(new TokenContract(TOKEN_ADDRESS, signer));
          
          const network = await provider.getNetwork();
          setChainId(Number(network.chainId));
        }
      }
    };
    
    checkConnection();
    
    window.ethereum?.on('accountsChanged', (accounts) => {
      if (accounts.length > 0) {
        setAccount(accounts[0]);
      } else {
        setAccount(null);
        setTokenContract(null);
      }
    });
    
    window.ethereum?.on('chainChanged', () => {
      window.location.reload();
    });
  }, []);

  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
      {/* Header */}
      <header className="bg-white dark:bg-gray-800 shadow-sm">
        <div className="container mx-auto px-4 py-4 flex justify-between items-center">
          <h1 className="text-2xl font-bold">My DApp</h1>
          <div className="flex items-center gap-4">
            {chainId && <NetworkSelector currentChainId={chainId} />}
            <WalletConnect />
          </div>
        </div>
      </header>
      
      {/* Main Content */}
      <main className="container mx-auto px-4 py-8">
        <div className="grid md:grid-cols-2 gap-8">
          {account && <TokenTransfer />}
          {account && tokenContract && (
            <TransactionHistory tokenContract={tokenContract} account={account} />
          )}
        </div>
      </main>
      
      {/* Footer */}
      <footer className="mt-auto py-6 text-center text-gray-500">
        <p>Built with Web3.js and React</p>
      </footer>
    </div>
  );
}

export default App;

总结与最佳实践

DApp前端开发与传统Web开发有显著差异。首先,永远不要假设用户已经安装了钱包,优雅地处理钱包缺失是基本要求。其次,处理加载状态和错误状态,网络请求可能因为区块链拥堵而延迟。

Gas费用的估算和显示对用户体验至关重要。在允许用户提交交易前,最好先估算Gas费用并告知用户。 ethers.js的estimateGas和getFeeData方法可以帮助实现这一点。

事件监听是保持UI与链上状态同步的关键。但要注意,过多的事件监听可能影响性能,应该在组件卸载时及时移除监听器。

网络切换需要处理钱包未安装、网络不支持等各种边界情况。提供一个清晰的网络列表,并确保切换失败时给出友好的错误提示。

最后,安全性是DApp开发的重中之重。永远不要在客户端存储私钥或敏感信息,所有签名操作都应该通过用户钱包确认。对于复杂的合约交互,提供清晰的交易预览,让用户知道他们将要执行什么操作。

掌握了这些核心技能后,你已经具备了开发大多数DApp的能力。随着经验积累,你会逐渐形成自己的最佳实践,构建出更安全、更高效的DApp应用。

相关推荐

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注