
在 React TypeScript 项目中使用 Web3.js 连接 MetaMask 的完整实战教程【免费下载链接】web3.jsCollection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions.项目地址: https://gitcode.com/gh_mirrors/we/web3.js本文基于 web3.js 仓库官方文档metamask-react.md编写面向希望在自己的 React 前端dApp中接入 MetaMask 钱包的开发者。你将掌握如何在 React TypeScript 项目中安装并初始化 Web3.js、用 MetaMask 注入的以太坊 Provider 建立连接、请求账户授权、用账户签名消息并通过ecRecover验证签名者身份。全套流程将从官方教程代码与仓库源码两个层面展开保证可复制、可运行。教程背景与总体流程本教程是 「用 Vanilla JavaScript 连接 MetaMask」 教程的延续前者使用纯 JavaScript HTML本教程改用React TypeScript实现同等能力核心 API 与交互逻辑保持一致。整体分六个步骤推进检查前置条件初始化 React 项目并添加 Web3.js将 MetaMask 作为 Web3.js 的 Provider请求 MetaMask 账户访问权限使用 MetaMask 账户签名消息验证签名所用的账户每一步都会以「替换src/App.tsx文件内容」的方式增量演进最终得到一个功能完整的 dApp 演示页面。Step 1: 前置条件本教程默认你具备基础的命令行操作能力并熟悉 React 与 Node.js。开始之前请确保Node.js 及其包管理器 npm 已安装建议使用当前稳定版本$: node -v # your version may be different, but its best to use the current stable version v18.16.1 $: npm -v 9.5.1MetaMask 已作为浏览器扩展安装并完成了创建账户的流程例如设置密码、核对助记词。Step 2: 初始化 React 项目并添加 Web3.js使用 Create React App 的 TypeScript 模板初始化项目并进入项目目录npx create-react-app web3-metamask-react --template typescript cd web3-metamask-react添加 Web3.js 依赖npm i web3web3包是本仓库的聚合包从 packages/web3/src/index.ts 可以看到它统一导出并重导出了Web3、Web3Eth、Contract、Personal、Net以及web3-types等核心类型见export { Web3 }、export * from web3-types等语句因此业务代码只需一条import { Web3 } from web3即可使用全部能力。Step 3: 将 MetaMask 作为 Web3.js 的 ProviderMetaMask 会把以太坊 Provider 以ethereum属性的形式注入到全局Window对象上。为了让 TypeScript 编译器认识这个新增属性修改src/react-app-env.d.tsimport { MetaMaskProvider } from web3; /// reference typesreact-scripts / declare global { interface Window { ethereum: MetaMaskProvider; } }这里用到的MetaMaskProvider类型定义在仓库的 packages/web3-types/src/web3_base_provider.ts 中。从源码可以看到它扩展了SimpleProvider提供request方法并声明了on/removeListenerconnect、disconnect、message、chainChanged、accountsChanged事件以及isMetaMask: boolean属性——这正是教程代码中window.ethereum.isMetaMask判断的底层类型依据。将src/App.tsx的内容替换为以下代码import { useEffect, useState } from react; import { Web3 } from web3; function App() { const [web3, setWeb3] useStateWeb3 | null(null); const [warning, setWarning] useStatestring | null(null); const [provider, setProvider] useStatestring | null(null); const [chainId, setChainId] useStatestring | null(null); const [latestBlock, setLatestBlock] useStatestring | null(null); useEffect(() { // ensure that there is an injected the Ethereum provider if (window.ethereum) { // use the injected Ethereum provider to initialize Web3.js setWeb3(new Web3(window.ethereum)); // check if Ethereum provider comes from MetaMask if (window.ethereum.isMetaMask) { setProvider(Connected to Ethereum with MetaMask.); } else { setProvider(Non-MetaMask Ethereum provider detected.); } } else { // no Ethereum provider - instruct user to install MetaMask setWarning(Please install MetaMask); } }, []); useEffect(() { async function getChainId() { if (web3 null) { return; } // get chain ID and populate placeholder setChainId(Chain ID: ${await web3.eth.getChainId()}); } async function getLatestBlock() { if (web3 null) { return; } // get latest block and populate placeholder setLatestBlock(Latest Block: ${await web3.eth.getBlockNumber()}); // subscribe to new blocks and update UI when a new block is created const blockSubscription await web3.eth.subscribe(newBlockHeaders); blockSubscription.on(data, block { setLatestBlock(Latest Block: ${block.number}); }); } getChainId(); getLatestBlock(); }, [web3]); return ( div idwarn style{{ color: red }} {warning} /div div idprovider{provider}/div div idchainId{chainId}/div div idlatestBlock{latestBlock}/div / ); } export default App;代码解读App组件定义了若干用于展示网络信息的占位 state并提供了两个useEffect钩子来填充它们。第一个useEffect依赖为空数组仅挂载时执行检查是否存在注入式 Provider。关于注入式 Provider 的定义可参见 providers 指南它由第三方通常是钱包或浏览器注入除了提供网络连接还往往提供一个或多个账户且必须兼容 EIP-1193并支持实时事件订阅。若发现注入的 Provider就用它构造新的Web3实例连接以太坊网络随后通过window.ethereum.isMetaMask判断该 Provider 是否来自 MetaMask 并展示结果若没有找到注入的 Provider则提示用户安装 MetaMask。第二个useEffect依赖web3在web3就绪后调用web3.eth.getChainId()与web3.eth.getBlockNumber()填充占位符并通过web3.eth.subscribe(newBlockHeaders)建立新区块订阅在有新块产生时实时更新「最新区块」的显示。从源码实现看new Web3(window.ethereum)之所以能直接接受注入的 Provider是因为 packages/web3/src/web3.ts 中构造函数签名接受string | SupportedProvidersEthExecutionAPI | Web3ContextInitOptions其中SupportedProviders就包含 EIP-1193 兼容的 Provider 类型而 packages/web3-core/src/utils.ts 中的isMetaMaskProvider类型守卫会进一步检查 Provider 是否具备request方法且为 AsyncFunction以及isMetaMask标志与本教程的判断逻辑一一对应。启动 React 应用npm start浏览器必须是装有 MetaMask 扩展的那个浏览器会自动打开页面。若一切配置正确页面应显示已通过 MetaMask 连接以太坊网络并列出 Chain ID以太坊主网默认值为1和最新区块号当新区块产生时区块号会随之变化。Step 4: 请求 MetaMask 账户访问权限将src/App.tsx的内容替换为以下代码注意高亮标记的新增部分import { useEffect, useState } from react; import { Web3 } from web3; function App() { const [web3, setWeb3] useStateWeb3 | null(null); const [warning, setWarning] useStatestring | null(null); const [provider, setProvider] useStatestring | null(null); const [chainId, setChainId] useStatestring | null(null); const [latestBlock, setLatestBlock] useStatestring | null(null); const [accountButtonDisabled, setAccountButtonDisabled] useStateboolean(false); const [accounts, setAccounts] useStatestring[] | null(null); const [connectedAccount, setConnectedAccount] useStatestring | null(null); useEffect(() { // ensure that there is an injected the Ethereum provider if (window.ethereum) { // use the injected Ethereum provider to initialize Web3.js setWeb3(new Web3(window.ethereum)); // check if Ethereum provider comes from MetaMask if (window.ethereum.isMetaMask) { setProvider(Connected to Ethereum with MetaMask.); } else { setProvider(Non-MetaMask Ethereum provider detected.); } } else { // no Ethereum provider - instruct user to install MetaMask setWarning(Please install MetaMask); setAccountButtonDisabled(true); } }, []); useEffect(() { async function getChainId() { if (web3 null) { return; } // get chain ID and populate placeholder setChainId(Chain ID: ${await web3.eth.getChainId()}); } async function getLatestBlock() { if (web3 null) { return; } // get latest block and populate placeholder setLatestBlock(Latest Block: ${await web3.eth.getBlockNumber()}); // subscribe to new blocks and update UI when a new block is created const blockSubscription await web3.eth.subscribe(newBlockHeaders); blockSubscription.on(data, block { setLatestBlock(Latest Block: ${block.number}); }); } getChainId(); getLatestBlock(); }, [web3]); // click event for Request MetaMask Accounts button async function requestAccounts() { if (web3 null) { return; } // request accounts from MetaMask await window.ethereum.request({ method: eth_requestAccounts }); document.getElementById(requestAccounts)?.remove(); // get list of accounts const allAccounts await web3.eth.getAccounts(); setAccounts(allAccounts); // get the first account and populate placeholder setConnectedAccount(Account: ${allAccounts[0]}); } return ( div idwarn style{{ color: red }} {warning} /div div idprovider{provider}/div div idchainId{chainId}/div div idlatestBlock{latestBlock}/div div idconnectedAccount{connectedAccount}/div div button onClick{() requestAccounts()} idrequestAccounts disabled{accountButtonDisabled} Request MetaMask Accounts /button /div / ); } export default App;本步骤的组件新增了两块内容一个展示 MetaMask 账户地址的占位符以及一个用于向 MetaMask 请求账户的按钮。若没有找到注入的 Provider该按钮会被禁用disabled。新定义的requestAccounts函数是按钮的点击处理器通过window.ethereum.request({ method: eth_requestAccounts })向 MetaMask 发起账户授权请求请求获批后移除按钮避免重复点击调用web3.eth.getAccounts()获取账户地址列表取第一个账户填充到页面上。MetaMask 支持管理多个账户本教程只使用第一个账户。eth_requestAccounts是标准的 RPC 调用授权结果会被 MetaMask 记忆——后续再次请求账户时无需重复确认。回到装有 MetaMask 的浏览器页面应已自动刷新并显示新按钮。点击「Request MetaMask Accounts」会唤起 MetaMask接受通知后页面上即显示账户地址。Step 5: 使用 MetaMask 账户签名消息将src/App.tsx的内容替换为以下代码注意高亮标记的新增部分import { useEffect, useState } from react; import { Web3 } from web3; function App() { const [web3, setWeb3] useStateWeb3 | null(null); const [warning, setWarning] useStatestring | null(null); const [provider, setProvider] useStatestring | null(null); const [chainId, setChainId] useStatestring | null(null); const [latestBlock, setLatestBlock] useStatestring | null(null); const [accountButtonDisabled, setAccountButtonDisabled] useStateboolean(false); const [accounts, setAccounts] useStatestring[] | null(null); const [connectedAccount, setConnectedAccount] useStatestring | null(null); const [messageToSign, setMessageToSign] useStatestring | null(null); const [signingResult, setSigningResult] useStatestring | null(null); useEffect(() { // ensure that there is an injected the Ethereum provider if (window.ethereum) { // use the injected Ethereum provider to initialize Web3.js setWeb3(new Web3(window.ethereum)); // check if Ethereum provider comes from MetaMask if (window.ethereum.isMetaMask) { setProvider(Connected to Ethereum with MetaMask.); } else { setProvider(Non-MetaMask Ethereum provider detected.); } } else { // no Ethereum provider - instruct user to install MetaMask setWarning(Please install MetaMask); setAccountButtonDisabled(true); } }, []); useEffect(() { async function getChainId() { if (web3 null) { return; } // get chain ID and populate placeholder setChainId(Chain ID: ${await web3.eth.getChainId()}); } async function getLatestBlock() { if (web3 null) { return; } // get latest block and populate placeholder setLatestBlock(Latest Block: ${await web3.eth.getBlockNumber()}); // subscribe to new blocks and update UI when a new block is created const blockSubscription await web3.eth.subscribe(newBlockHeaders); blockSubscription.on(data, block { setLatestBlock(Latest Block: ${block.number}); }); } getChainId(); getLatestBlock(); }, [web3]); // click event for Request MetaMask Accounts button async function requestAccounts() { if (web3 null) { return; } // request accounts from MetaMask await window.ethereum.request({ method: eth_requestAccounts }); document.getElementById(requestAccounts)?.remove(); // get list of accounts const allAccounts await web3.eth.getAccounts(); setAccounts(allAccounts); // get the first account and populate placeholder setConnectedAccount(Account: ${allAccounts[0]}); } // click event for Sign Message button async function signMessage() { if (web3 null || accounts null || messageToSign null) { return; } // sign message with first MetaMask account const signature await web3.eth.personal.sign(messageToSign, accounts[0], ); setSigningResult(signature); } return ( div idwarn style{{ color: red }} {warning} /div div idprovider{provider}/div div idchainId{chainId}/div div idlatestBlock{latestBlock}/div div idconnectedAccount{connectedAccount}/div div button onClick{() requestAccounts()} idrequestAccounts disabled{accountButtonDisabled} Request MetaMask Accounts /button /div div input onChange{e { setMessageToSign(e.target.value); }} idmessageToSign placeholderMessage to Sign disabled{connectedAccount null} / button onClick{() signMessage()} idsignMessage disabled{connectedAccount null} Sign Message /button div idsigningResult{signingResult}/div /div / ); } export default App;本步骤新增了签名输入区一个文本输入框、一个「Sign Message」按钮和一个签名结果占位符。这些输入在页面取得 MetaMask 账户授权之前保持禁用状态disabled{connectedAccount null}授权成功后才启用。signMessage函数是「Sign Message」按钮的点击处理器核心调用是web3.eth.personal.sign(messageToSign, accounts[0], )第一个参数待签名消息取自输入框内容第二个参数用于签名的账户地址本教程使用第一个 MetaMask 账户第三个参数解密账户的密码短语passphrase因为本例中账户由 MetaMask 托管所以传空字符串。对应的方法实现在 packages/web3-eth-personal/src/personal.ts 的public async sign(data: HexString, address: Address, passphrase: string)。当签名请求经由注入式 Provider 转发给 MetaMask 时MetaMask 会弹出通知由用户确认签名确认后返回签名结果并填充到页面占位符中。回到 MetaMask 浏览器点击「Request MetaMask Accounts」这次无需再接受任何通知账户地址显示后签名输入框即变为可用。输入一条消息例如Hello, Web3.js!并点击「Sign Message」接受 MetaMask 的签名通知后签名结果会出现在输入框下方。Step 6: 验证签名所用的账户将src/App.tsx的内容替换为以下代码注意高亮标记的新增部分import { useEffect, useState } from react; import { Web3 } from web3; function App() { const [web3, setWeb3] useStateWeb3 | null(null); const [warning, setWarning] useStatestring | null(null); const [provider, setProvider] useStatestring | null(null); const [chainId, setChainId] useStatestring | null(null); const [latestBlock, setLatestBlock] useStatestring | null(null); const [accountButtonDisabled, setAccountButtonDisabled] useStateboolean(false); const [accounts, setAccounts] useStatestring[] | null(null); const [connectedAccount, setConnectedAccount] useStatestring | null(null); const [messageToSign, setMessageToSign] useStatestring | null(null); const [signingResult, setSigningResult] useStatestring | null(null); const [originalMessage, setOriginalMessage] useStatestring | null(null); const [signedMessage, setSignedMessage] useStatestring | null(null); const [signingAccount, setSigningAccount] useStatestring | null(null); useEffect(() { // ensure that there is an injected the Ethereum provider if (window.ethereum) { // use the injected Ethereum provider to initialize Web3.js setWeb3(new Web3(window.ethereum)); // check if Ethereum provider comes from MetaMask if (window.ethereum.isMetaMask) { setProvider(Connected to Ethereum with MetaMask.); } else { setProvider(Non-MetaMask Ethereum provider detected.); } } else { // no Ethereum provider - instruct user to install MetaMask setWarning(Please install MetaMask); setAccountButtonDisabled(true); } }, []); useEffect(() { async function getChainId() { if (web3 null) { return; } // get chain ID and populate placeholder setChainId(Chain ID: ${await web3.eth.getChainId()}); } async function getLatestBlock() { if (web3 null) { return; } // get latest block and populate placeholder setLatestBlock(Latest Block: ${await web3.eth.getBlockNumber()}); // subscribe to new blocks and update UI when a new block is created const blockSubscription await web3.eth.subscribe(newBlockHeaders); blockSubscription.on(data, block { setLatestBlock(Latest Block: ${block.number}); }); } getChainId(); getLatestBlock(); }, [web3]); // click event for Request MetaMask Accounts button async function requestAccounts() { if (web3 null) { return; } // request accounts from MetaMask await window.ethereum.request({ method: eth_requestAccounts }); document.getElementById(requestAccounts)?.remove(); // get list of accounts const allAccounts await web3.eth.getAccounts(); setAccounts(allAccounts); // get the first account and populate placeholder setConnectedAccount(Account: ${allAccounts[0]}); } // click event for Sign Message button async function signMessage() { if (web3 null || accounts null || messageToSign null) { return; } // sign message with first MetaMask account const signature await web3.eth.personal.sign(messageToSign, accounts[0], ); setSigningResult(signature); } // click event for Recover Account button async function recoverAccount() { if (web3 null || originalMessage null || signedMessage null) { return; } // recover account from signature const account await web3.eth.personal.ecRecover(originalMessage, signedMessage); setSigningAccount(account); } return ( div idwarn style{{ color: red }} {warning} /div div idprovider{provider}/div div idchainId{chainId}/div div idlatestBlock{latestBlock}/div div idconnectedAccount{connectedAccount}/div div button onClick{() requestAccounts()} idrequestAccounts disabled{accountButtonDisabled} Request MetaMask Accounts /button /div div input onChange{e { setMessageToSign(e.target.value); }} idmessageToSign placeholderMessage to Sign disabled{connectedAccount null} / button onClick{() signMessage()} idsignMessage disabled{connectedAccount null} Sign Message /button div idsigningResult{signingResult}/div /div div input onChange{e { setOriginalMessage(e.target.value); }} idoriginalMessage placeholderOriginal Message disabled{connectedAccount null} / input onChange{e { setSignedMessage(e.target.value); }} idsignedMessage placeholderSigned Message disabled{connectedAccount null} / button onClick{() recoverAccount()} idrecoverAccount disabled{connectedAccount null} Recover Account /button div idsigningAccount{signingAccount}/div /div / ); } export default App;与上一步类似本步骤新增了一组用于「签名验证」的输入两个输入框原始消息、签名和一个「Recover Account」按钮以及一个存放恢复结果的占位符。这些输入同样在取得账户授权之前处于禁用状态。recoverAccount函数是「Recover Account」按钮的点击处理器核心调用是web3.eth.personal.ecRecover(originalMessage, signedMessage)第一个参数未签名的原始消息取自第一个输入框第二个参数签名结果取自第二个输入框。对应的方法实现在 packages/web3-eth-personal/src/personal.ts 的public async ecRecover(signedData: HexString, signature: string)。它从签名中恢复出签名者的账户地址并填充到页面占位符中。验证流程先像之前一样请求 MetaMask 账户并签名一条消息然后把「原始消息」和「签名」注意要带上开头的0x前缀分别粘贴到新输入框中点击「Recover Account」。若一切正常页面下方会显示签名所用账户的地址且该地址应与页面上方展示的连接账户地址一致。结论与关键要点本教程演示了 Web3.js 与 MetaMask 的完整集成包括使用 MetaMask 注入式 Provider 以及使用 MetaMask 账户签名消息。核心要点总结如下使用注入式 Provider直接用window.ethereum构造new Web3(window.ethereum)即可。Web3构造函数支持 EIP-1193 兼容的 Provider源码依据见 packages/web3/src/web3.ts 与 packages/web3-core/src/utils.ts 中的类型与类型守卫实现。请求账户授权调用window.ethereum.request({ method: eth_requestAccounts })用户确认后即可通过web3.eth.getAccounts()获取账户地址列表。签名与验证web3.eth.personal.sign(message, address, )请求 MetaMask 对消息签名web3.eth.personal.ecRecover(originalMessage, signedMessage)从签名中恢复签名者地址涉及 MetaMask 账户的操作都会由 MetaMask 弹出通知请求用户确认。类型化基础window.ethereum的类型为 packages/web3-types/src/web3_base_provider.ts 中定义的MetaMaskProvider其isMetaMask属性可用于判断 Provider 是否来自 MetaMask。关于注入式 Provider 的更深入背景EIP-1193 兼容性、事件订阅支持等可继续阅读 Web3 Providers 指南。若希望在不使用 React 的纯 HTML/JavaScript 环境中实现同样功能请参考 Vanilla JavaScript 版教程。【免费下载链接】web3.jsCollection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions.项目地址: https://gitcode.com/gh_mirrors/we/web3.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考