Next.js dapp本地可连Metamask 部署至Vercel后连接失败
Next.js DApp Vercel部署后MetaMask无法连接问题修复
问题现象
基于Next.js开发的DApp在localhost本地环境运行时,MetaMask可自动完成钱包连接,但部署至Vercel后连接钱包功能失效。
- 本地运行正常效果:

- Vercel部署后异常效果:

涉及核心代码包括web3 provider配置、页面入口文件、合约工厂实例三个部分。
根本原因
- 违反MetaMask安全规则:原代码在web3配置模块加载时就直接调用
eth_requestAccounts发起连接请求,该方法仅允许在用户主动交互(如点击按钮)的事件回调中触发,禁止页面加载时自动弹出授权窗。本地环境能正常运行是因为提前给localhost授予了永久连接权限,调用时不会触发弹窗所以未被拦截;部署到Vercel的新域名后无提前授权,自动调用直接被钱包拦截,不会弹出连接窗口。 - SSR环境时序问题:Next.js部署到Vercel后会默认执行服务端渲染,
getInitialProps中的逻辑运行在Node.js环境,不存在浏览器window对象,此时代码会直接fallback到Infura只读Provider,客户端水合完成后也不会正确绑定MetaMask注入的实例。
修复步骤
1. 重写web3配置文件
拆分服务端只读实例和客户端钱包连接逻辑,禁止在模块顶层触发钱包连接请求:
import Web3 from "web3" let web3 // 初始化服务端/未连接钱包时使用的只读实例,用于SSR阶段链上数据查询 const getReadOnlyProvider = () => { return new Web3.providers.HttpProvider( "https://rinkeby.infura.io/v3/9fe88971851d4fb1b8501ed1a22abfbd" ) } /** * 客户端钱包连接方法,必须绑定到用户点击事件上触发 */ export const connectWallet = async () => { if (typeof window === "undefined" || !window.ethereum) { alert("请先安装MetaMask钱包") return null } try { await window.ethereum.request({ method: "eth_requestAccounts" }) web3 = new Web3(window.ethereum) return web3 } catch (err) { console.error("钱包连接失败:", err) return null } } // 默认初始化只读实例 web3 = new Web3(getReadOnlyProvider()) export default web3
2. 调整合约调用逻辑
- 服务端渲染阶段(
getInitialProps/getServerSideProps)的只读链上查询,直接使用默认导出的web3实例创建的合约对象即可,走Infura节点查询,不依赖钱包。 - 所有需要签名、发交易的操作,必须在客户端侧等待用户点击按钮触发
connectWallet连接成功后,再用绑定了钱包Provider的web3实例创建合约对象调用,不要直接用顶层导出的只读合约实例发交易。
3. 页面侧增加合规连接逻辑
不要依赖页面自动弹钱包连接,在页面上增加明确的「连接钱包」按钮,将connectWallet方法绑定到按钮点击事件上。
如果需要实现“已授权用户打开页面自动连接”的效果,可以在客户端挂载完成后用eth_accounts方法静默查询已授权账号,不会触发弹窗:
import { useEffect, useState } from 'react' import { Button, Segment } from 'semantic-ui-react' import web3, { connectWallet } from '../ethereum/web3' import factory from "../ethereum/factory" import Layout from "../components/Layout" import Link from "next/link" const CampaignIndex = (props) => { const [currentAccount, setCurrentAccount] = useState('') const renderCampaigns = () => { const items = props.campaigns.map((address) => { return { header: address, description: ( <Link href={`/campaigns/${address}`}> <a>View</a> </Link> ), fluid: true, } }) return <Card.Group items={items} /> } // 仅在客户端执行 useEffect(() => { if (typeof window === 'undefined' || !window.ethereum) return // 静默查询已授权的账号,不弹授权窗 window.ethereum.request({ method: 'eth_accounts' }) .then(accounts => { if (accounts.length > 0) { setCurrentAccount(accounts[0]) web3.setProvider(window.ethereum) } }) // 监听钱包账号、链切换事件 window.ethereum.on('accountsChanged', (accounts) => { setCurrentAccount(accounts[0] || '') }) }, []) const handleConnect = async () => { const connectedWeb3 = await connectWallet() if (connectedWeb3) { const accounts = await connectedWeb3.eth.getAccounts() setCurrentAccount(accounts[0]) } } return ( <Layout> <Segment color="blue"> <h2>Active Campaigns</h2> <Link href="/campaigns/new"> <a> <Button floated="right" content="Add Campaign" icon="add circle" labelPosition="left" primary /> </a> </Link> {/* 未连接钱包时显示连接按钮 */} {!currentAccount && ( <Button floated="right" primary onClick={handleConnect} style={{marginRight: 10}}> 连接钱包 </Button> )} {currentAccount && ( <p floated="right" style={{float: 'right', marginRight: 10}}>当前连接账号:{currentAccount.slice(0,6)}...{currentAccount.slice(-4)}</p> )} {renderCampaigns()} </Segment> </Layout> ) } // getInitialProps逻辑保持不变,只读查询走Infura节点不受影响 CampaignIndex.getInitialProps = async () => { const campaigns = await factory.methods.getDeployedCampaigns().call() return { campaigns: campaigns } } export default CampaignIndex
Vercel部署额外注意点
- 确保部署后的域名使用HTTPS协议:MetaMask不会向HTTP协议的非localhost域名注入Provider,Vercel默认分配的
*.vercel.app域名自带HTTPS,若绑定自定义域名必须配置有效SSL证书。 - 原代码使用的Rinkeby测试网已正式废弃,建议将RPC节点替换为Sepolia测试网节点,避免后续Infura接口停服导致只读查询失效。
- 首次访问新部署的域名时,需要用户手动点击连接按钮、在MetaMask弹窗中手动授权,才能完成连接,不存在绕过用户授权的自动连接方案。
内容的提问来源于stack exchange,提问作者Taras Shulhan
相关产品推荐
相关产品推荐

