Web3.js 前端交互:连接钱包与调用合约
环境准备与工具安装
在开始使用 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 说明,建议将其加入书签,随时查阅。