You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js dapp本地可连Metamask 部署至Vercel后连接失败

Next.js DApp Vercel部署后MetaMask无法连接问题修复

问题现象

基于Next.js开发的DApp在localhost本地环境运行时,MetaMask可自动完成钱包连接,但部署至Vercel后连接钱包功能失效。

  • 本地运行正常效果:本地运行正常效果
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 10:42:22