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
相关产品推荐
相关产品推荐

