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

React+Apollo Client+GraphQL部署Heroku后仅本地运行时可用问题求助

问题:React+Apollo Client+GraphQL应用部署到Heroku后仅本地运行时可用

本地开发时应用完全正常,但部署到Heroku后,只有同时在本地运行该应用时才能正常访问功能。尝试过多种方式配置Apollo Client的uri:固定本地地址、根据生产环境切换为Heroku地址、使用PORT变量拼接地址,均未解决问题。

前端app.js代码

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { useState } from 'react';
import {
  Route,
  Routes,
  BrowserRouter as Router,
  Outlet,
} from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import HomeScreen from './screens/HomeScreen';
import Container from 'react-bootstrap/Container';
import Navbar from 'react-bootstrap/Navbar';
import Nav from 'react-bootstrap/Nav';
import { LinkContainer } from 'react-router-bootstrap';
import Button from 'react-bootstrap/Button';
import AboutScreen from './screens/AboutScreen';
import AdminScreen from './screens/AdminScreen';

// Construct main GraphQL API endpoint
let uri = 'http://localhost:5000/graphql';
if (process.env.NODE_ENV === 'production') {
  uri = `https://denver-judo.herokuapp.com/api/graphql`;
}
console.log(process.env.NODE_ENV);
console.log(uri);
const client = new ApolloClient({
  uri: uri,
  cache: new InMemoryCache(),
});
function App() {
  const location = window.location.pathname;
  const [sidebarIsOpen, setSidebarIsOpen] = useState(false);

  return (
    <ApolloProvider client={client}>
      <Router>
        <div
          className={
            sidebarIsOpen
              ? 'site-container active-cont d-flex flex-column'
              : 'site-container d-flex flex-column'
          }
        >
          <ToastContainer position="top-center" limit={1} />
          <header>
            <Navbar
              expand="lg"
              className={`navbar navbar-dark ${
                location === '/' ? 'navbar-custom' : 'navbar-custom-light'
              }`}
            >
              <Container>
                <LinkContainer to="/">
                  <Navbar.Brand>
                    <img
                      className="ms-4"
                      src={
                        location === '/'
                          ? 'assets/img/DJ-logo-black.png'
                          : 'assets/img/DJ-logo-white.png'
                      }
                      alt="Denver Judo"
                      style={{ width: '60px' }}
                    />{' '}
                  </Navbar.Brand>
                </LinkContainer>
                <h1
                  className={
                    location === '/'
                      ? 'navTitle-dark text-center'
                      : 'navTitle text-center'
                  }
                >
                  Denver Judo
                </h1>

                <Button
                  style={{
                    backgroundColor: location === '/' ? '#d9d9d9' : '#262626',
                  }}
                  onClick={() => setSidebarIsOpen(!sidebarIsOpen)}
                  className="me-4"
                >
                  <i
                    className="fa fa-bars fa-lg"
                    style={{
                      color: location === '/' ? '#262626' : '#d9d9d9',
                    }}
                  ></i>
                </Button>
              </Container>
            </Navbar>
          </header>
          <div
            className={
              sidebarIsOpen
                ? 'active-nav side-navbar d-flex justify-content-between flex-wrap flex-column'
                : 'side-navbar d-flex justify-content-between flex-wrap flex-column'
            }
          >
            <Nav className="flex-column text-white w-100 p-2">
              <Nav.Item>
                <strong>Denver Judo</strong>
              </Nav.Item>
              <LinkContainer to="/" onClick={() => (this.expand = '')}>
                <Nav.Link
                  className={
                    location === '/' ? 'nav-text nav-text-bold' : 'nav-text'
                  }
                >
                  Home
                </Nav.Link>
              </LinkContainer>
              <LinkContainer to="/schedule" onClick={() => (this.expand = '')}>
                <Nav.Link
                  className={
                    location === '/schedule'
                      ? 'nav-text nav-text-bold'
                      : 'nav-text'
                  }
                >
                  Schedule
                </Nav.Link>
              </LinkContainer>
              <LinkContainer to="/about" onClick={() => (this.expand = '')}>
                <Nav.Link
                  className={
                    location === '/about'
                      ? 'nav-text nav-text-bold'
                      : 'nav-text'
                  }
                >
                  About
                </Nav.Link>
              </LinkContainer>
            </Nav>
            <Outlet />
          </div>
          <main>
            <Routes>
              <Route index element={<HomeScreen />} />
              <Route path="/about" element={<AboutScreen />} />
              <Route path="_admin/*" element={<AdminScreen />} />
            </Routes>
          </main>
          <footer>
            <div className="text-center">
              Denver Judo
              <br />
              All rights reserved
            </div>
          </footer>
        </div>
      </Router>
    </ApolloProvider>
  );
}

export default App;

后端server.js代码

const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const path = require('path');
const dotenv = require('dotenv');
const { typeDefs, resolvers } = require('./schemas');
const db = require('./config/connection');
require('dotenv');

dotenv.config();

const PORT = process.env.PORT || 5000;
const app = express();

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

console.log(process.env.NODE_ENV);
if (process.env.NODE_ENV === 'production') {
  app.use(express.static(path.join(__dirname, '../client/build')));

  app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '../client/build/index.html'));
  });
}

// Create a new instance of an Apollo server with the GraphQL schema
const startApolloServer = async (typeDefs, resolvers) => {
  await server.start();
  server.applyMiddleware({ app });

  db.once('open', () => {
    app.listen(PORT, () => {
      console.log(`API server running on port ${PORT}!`);
      console.log(
        `Use GraphQL at http://localhost:${PORT}${server.graphqlPath}`
      );
    });
  });
};

// Call the async function to start the server
startApolloServer(typeDefs, resolvers);

问题排查与解决方案

1. URI路径不匹配

前端生产环境配置的uri为https://denver-judo.herokuapp.com/api/graphql,但后端Apollo Server默认的GraphQL路径是/graphql(从后端启动日志Use GraphQL at http://localhost:${PORT}${server.graphqlPath}可确认),路径多了/api导致请求404。

修复方式:

  • 方案一:修改前端生产环境uri为https://denver-judo.herokuapp.com/graphql
  • 方案二:后端创建ApolloServer时指定路径为/api/graphql:
    const server = new ApolloServer({
      typeDefs,
      resolvers,
    });
    // 应用中间件时指定路径
    server.applyMiddleware({ app, path: '/api/graphql' });
    

2. 使用相对路径避免环境判断

直接使用相对路径替代硬编码地址,自动适配当前域名,无需手动判断环境:

const client = new ApolloClient({
  uri: '/graphql', // 相对路径,自动匹配当前域名
  cache: new InMemoryCache(),
});

这种方式不管是本地开发还是Heroku部署,都会请求当前域名下的/graphql接口,彻底避免环境判断出错的问题。

3. 确认Heroku构建流程

确保Heroku部署时正确执行了前端构建步骤:

  • 检查项目根目录的package.json,添加构建脚本:
    "scripts": {
      "start": "node server/server.js",
      "build": "cd client && npm install && npm run build",
      "heroku-postbuild": "npm run build"
    }
    
    heroku-postbuild会在Heroku部署时自动执行,确保前端build文件被生成,后端能正确托管静态资源。

4. 验证环境变量

React项目中,process.env.NODE_ENV在执行npm run build时会自动被设置为production,如果前端代码中环境判断不生效,可通过构建日志确认是否触发了生产构建。也可以使用REACT_APP_前缀的自定义环境变量来控制uri,但相对路径方案更简洁可靠。

内容的提问来源于stack exchange,提问作者Stephen Scott Moore

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 09:31:06