Web3.js 前端交互:连接钱包与调用合约

FreeGuideOnline 最新 2026-06-15

环境准备与工具安装

在开始使用 Web3.js 构建去中心化应用(dApp)前端之前,需要准备好基本的开发环境和依赖库。本教程默认你已经拥有一个 Node.js 环境,并会使用 MetaMask 浏览器扩展作为钱包示例。

创建项目并安装 Web3.js

mkdir web3-frontend-demo
cd web3-frontend-demo
npm init -y
npm install web3

安装完成后,可以在 HTML 文件中直接通过 <script> 引入打包后的 web3 库,或者使用模块打包器(如 Webpack、Vite)进行导入。本教程为清晰起见,采用原生 JavaScript 配合浏览器提供的 ESM 方式(使用 CDN 直接引入)演示核心逻辑,你也可以根据自己的构建工具做调整。

浏览器钱包准备

确保浏览器已安装 MetaMask 并创建或导入了一个账户。MetaMask 会自动注入 window.ethereum 对象,Web3.js 可以通过该对象与钱包交互。


Web3.js 核心概念速览

在开始编码前,理解 Web3.js 中最关键的几个对象会帮助你更快上手:

  • Web3 实例:所有交互的入口。通过 new Web3(provider) 创建。
  • Provider(提供器):Web3 与以太坊网络通信的桥梁,MetaMask 注入的 window.ethereum 就是一个 provider。
  • Contract(合约):代表链上部署的智能合约,通过 ABI 和合约地址实例化,可以调用合约方法。
  • ABI(合约接口):描述合约函数和事件的 JSON 数组,Web3.js 用它来编码调用数据并解析返回值。

连接钱包:获取 Provider 并创建 Web3 实例

现代 dApp 开发中,推荐通过 EIP-1193 标准与钱包交互。以下代码演示如何安全地检测 MetaMask 并请求用户授权账户。

// 检测是否安装了 MetaMask
if (typeof window.ethereum !== 'undefined') {
  console.log('MetaMask 已安装!');
} else {
  alert('请先安装 MetaMask 浏览器扩展');
}

// 创建 Web3 实例(使用 MetaMask 提供的 provider)
let web3;
if (window.ethereum) {
  web3 = new Web3(window.ethereum);
} else {
  // 回退到公共 RPC(仅用于读取链上数据,不能发送交易)
  web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_KEY');
}

请求账户授权

当你需要获取用户钱包地址或发送交易时,必须主动请求授权:

async function connectWallet() {
  try {
    // 请求用户授权账户
    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
    const userAccount = accounts[0];
    console.log('已连接账户:', userAccount);
    return userAccount;
  } catch (error) {
    console.error('用户拒绝授权或发生错误:', error);
  }
}

最佳实践:

  • 仅在用户进行需要钱包签名的操作时调用 eth_requestAccounts,避免一进入页面就弹出授权请求。
  • 监听 accountsChanged 事件,以便在用户切换账户时更新 UI。
window.ethereum.on('accountsChanged', function (accounts) {
  console.log('账户切换至:', accounts[0]);
  // 在此更新前端显示的地址
});

实例化智能合约:通过 ABI 和地址

调用合约前,你需要拥有合约的 ABI部署地址。这里以一个简单的存储合约(Store)为例,该合约有一个 retrieve 读取函数和一个 store 写入函数。

合约示例(Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 private storedData;

    function store(uint256 x) public {
        storedData = x;
    }

    function retrieve() public view returns (uint256) {
        return storedData;
    }
}

前端实例化合约

// 合约 ABI(根据合约编译得到)
const abi = [
  {
    "inputs": [{"internalType": "uint256","name": "x","type": "uint256"}],
    "name": "store",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "retrieve",
    "outputs": [{"internalType": "uint256","name": "","type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  }
];

// 合约部署地址(需替换为你部署的地址)
const contractAddress = '0x1234567890abcdef...';

// 实例化合约对象
const simpleStorage = new web3.eth.Contract(abi, contractAddress);

现在 simpleStorage 就可以用来调用合约中的函数了。


调用合约:读取数据与发送交易

Web3.js 将合约函数调用分为两种类型:call(调用)send(发送交易)。对应到以太坊中,就是只读操作(view/pure 函数)和状态更改操作(需要 gas 的交易)。

读取合约数据(call)

对于 retrieve() 这种不修改链上状态的函数,直接使用 .call() 即可,无需用户签名,也不需要 gas 费。

async function readValue() {
  try {
    const result = await simpleStorage.methods.retrieve().call();
    console.log('合约存储的值:', result);
    document.getElementById('valueDisplay').innerText = result;
  } catch (error) {
    console.error('读取失败:', error);
  }
}

说明:

  • methods 对象下包含所有 ABI 中定义的函数,参数直接传入。
  • .call() 返回一个 Promise,解析为合约函数的返回值。
  • 读取操作不需要连接 MetaMask(但使用 MetaMask provider 也不影响),使用公共 RPC 即可。

发送交易修改状态(send)

调用 store(uint256) 时会改变区块链状态,需要用户通过 MetaMask 签名确认交易,并支付 gas 费。必须提供发送者的地址。

async function writeValue(newValue) {
  const accounts = await web3.eth.getAccounts(); // 已授权后无需再次请求
  if (accounts.length === 0) {
    alert('请先连接钱包');
    return;
  }

  try {
    // 发送交易
    const receipt = await simpleStorage.methods.store(newValue).send({
      from: accounts[0],
      // gas 和 gasPrice 可以省略,MetaMask 会估算
    });
    console.log('交易成功!交易哈希:', receipt.transactionHash);
    // 更新 UI 显示新值
    readValue();
  } catch (error) {
    console.error('交易失败或用户取消:', error);
  }
}

关键点:

  • send() 触发 MetaMask 弹出窗口,等待用户确认。
  • 该方法返回一个交易收据(receipt)对象,包含交易哈希、区块号等信息。
  • 如果用户拒绝或交易失败,Promise 会 rejected,需要做好错误处理。
  • 可以通过 .on('transactionHash', callback) 监听交易哈希生成,以提供更好的用户体验。

事件监听(可选)

如果你的合约抛出事件,可以在前端进行订阅,实现实时更新。

// 假设合约中有 event Stored(uint256 value);
simpleStorage.events.Stored({
  fromBlock: 'latest'
})
.on('data', (event) => {
  console.log('事件数据:', event.returnValues);
  readValue(); // 自动刷新显示
})
.on('error', console.error);

完整交互示例(HTML + JS)

下面给出一个可直接运行的页面,整合了钱包连接、读取和写入功能。

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Web3.js 前端交互 Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/web3@1.10.0/dist/web3.min.js"></script>
</head>
<body>
  <h2>SimpleStorage dApp</h2>
  <button id="connectBtn">连接钱包</button>
  <p>当前账户:<span id="accountDisplay"></span></p>
  <p>存储的值:<span id="valueDisplay"></span></p>
  <input id="newValue" type="number" placeholder="输入新值">
  <button id="storeBtn">更新值</button>

  <script>
    // 合约 ABI 和地址(需根据实际情况替换)
    const abi = [ /* ... 此处放入完整 ABI ... */ ];
    const contractAddress = '0x...'; // 你的合约地址

    let web3;
    let simpleStorage;
    let userAccount;

    // 初始化(检测 MetaMask)
    if (window.ethereum) {
      web3 = new Web3(window.ethereum);
      simpleStorage = new web3.eth.Contract(abi, contractAddress);

      // 监听账户变化
      window.ethereum.on('accountsChanged', (accounts) => {
        userAccount = accounts[0];
        document.getElementById('accountDisplay').innerText = userAccount || '未连接';
      });
    } else {
      alert('请安装 MetaMask!');
    }

    // 连接钱包
    document.getElementById('connectBtn').addEventListener('click', async () => {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      userAccount = accounts[0];
      document.getElementById('accountDisplay').innerText = userAccount;
      readValue(); // 自动读取当前值
    });

    // 读取值
    async function readValue() {
      try {
        const val = await simpleStorage.methods.retrieve().call();
        document.getElementById('valueDisplay').innerText = val;
      } catch (e) {
        console.error('读取出错', e);
      }
    }

    // 写值
    document.getElementById('storeBtn').addEventListener('click', async () => {
      if (!userAccount) {
        alert('请先连接钱包');
        return;
      }
      const newVal = document.getElementById('newValue').value;
      if (!newVal) return;
      try {
        await simpleStorage.methods.store(newVal).send({ from: userAccount });
        readValue(); // 更新显示
      } catch (e) {
        console.error('交易失败', e);
      }
    });
  </script>
</body>
</html>

常见问题与调试技巧

1. 如何获取合约 ABI?

  • 使用 Remix IDE 编译合约后,可在“Compilation Details”中复制 ABI。
  • 如果使用 Hardhat 或 Truffle,编译后会在 artifacts/build/ 目录下生成包含 ABI 的 JSON 文件。

2. 交易一直处于 pending 状态怎么办?

  • 适当提高 gasPrice,尤其是在网络拥堵时。可以手动设置 send({ from: ..., gasPrice: web3.utils.toWei('50', 'gwei') })
  • 确保 MetaMask 中账户有足够的 ETH 支付 gas。

3. 调用合约方法时报错 “Cannot read properties of undefined (reading 'call')”

  • 检查合约 ABI 是否包含该方法,且函数名拼写完全一致。
  • 确保使用了 methods 对象:contract.methods.myFunction().call()

4. MetaMask 弹出窗口被阻止

  • 要求用户操作(如点击按钮)的上下文中触发 send(),浏览器通常不允许非用户主动触发的弹窗。

进一步学习方向

掌握上述内容后,你已经可以构建基础的 dApp 前端。后续可以深入学习:

  • 使用 ethers.js 作为 Web3.js 的替代库,两者 API 设计理念不同,可按需选择。
  • 集成 React/Vue 等前端框架,管理 Web3 状态与生命周期。
  • 处理大数精度:Web3.js 中数值默认以字符串或 BigNumber 返回,显示时需做格式化。
  • 监听链上事件、轮询交易状态,构建实时更新的 UI。
  • 处理网络切换:监听 chainChanged 事件,适配不同的链 ID。

Web3.js 的官方文档(web3js.readthedocs.io)提供了详尽的 API 说明,建议将其加入书签,随时查阅。