Heroku文件系统在React+Express项目中图片无法显示问题咨询
嘿,这个问题我碰到过好多次了,咱一点点拆解清楚——其实核心还是Heroku那个**临时文件系统(ephemeral filesystem)**的特性,只是两种项目的部署和访问逻辑不一样,导致表现出来的问题不同。
先搞懂:纯Express项目的“正常”其实是“暂时正常”
你说纯Express项目上传图片后能正常显示,其实只是运气好——因为Heroku的dyno只要不重启(比如没休眠、没部署更新、没缩放),你上传的文件就还存在于当前dyno的文件系统里。这时候你从Express的静态服务请求图片,自然能拿到。但如果等个30分钟(Heroku空闲dyno会休眠),或者重新部署一次,你上传的图片肯定就没了,只是你还没遇到这种情况而已。
那React+Express项目为什么上传后直接看不到?
这就和两种项目的部署、访问逻辑差异有关了,常见的原因有这几个:
1. 前后端部署架构的问题
情况A:前后端分开部署(前端在Vercel/Netlify,后端在Heroku)
这种情况下,你前端上传图片到Heroku后端后,可能犯了一个小错误:前端请求图片用的是相对路径(比如/uploads/xxx.jpg),但这个路径指向的是前端的服务器(比如Vercel的域名),而不是后端的Heroku域名。所以前端其实是在请求前端服务器上的uploads文件夹,那肯定找不到文件啊!但因为前端服务器可能返回了SPA的index.html(200状态码),所以控制台不会报404,只是图片加载失败。
情况B:前后端打包在一起部署(React build后放到Express里)
这种情况常见的坑是静态服务的配置顺序错了。比如你先把React的build文件夹设为静态资源,再配置uploads文件夹的静态服务:
// 错误顺序:先加载React静态资源 app.use(express.static(path.join(__dirname, 'client/build'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
这时候当你请求/uploads/xxx.jpg时,Express会先去client/build文件夹里找,找不到就返回React的index.html(因为SPA的路由 fallback 配置),所以请求返回的是200,但内容是HTML不是图片,自然显示不出来,控制台也不会报错。
2. React SPA的路由特性坑了你
纯Express是多页应用,页面跳转后路径大多是根路径,用相对路径uploads/xxx.jpg没问题。但React是单页应用,如果你的当前路由是/dashboard这种非根路径,用相对路径../uploads/xxx.jpg很容易拼错,导致请求路径不对,同样拿不到图片。
3. 上传路径和静态服务路径不匹配
本地开发时,你用./uploads当上传目录没问题,但部署到Heroku后,Node的工作目录可能和你本地不一样(比如因为打包方式、Heroku的文件结构),导致multer上传的文件实际不在Express静态服务指向的目录里。比如你用__dirname + '/uploads',但Heroku上的__dirname可能是/app,而你静态服务指向的是/app/client/uploads,那文件自然找不到。
怎么临时解决?(最终还是要上云存储)
先给你几个临时能让图片显示的办法,但记住:Heroku的临时文件系统永远不适合存用户上传的文件,最终还是要迁移到AWS S3、Cloudinary这类云存储服务。
- 如果是前后端分开部署:把图片的请求URL改成后端的完整域名,比如
https://your-heroku-app.herokuapp.com/uploads/xxx.jpg,别用相对路径。 - 如果是一起部署:调整静态服务的顺序,先加载
uploads的静态服务,再加载React的静态服务:// 正确顺序:先处理uploads的请求 app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // 再处理React的静态资源和路由 app.use(express.static(path.join(__dirname, 'client/build'))); // SPA路由fallback app.get('*', (req, res) => { res.sendFile(path.join(__dirname+'/client/build/index.html')); }); - 用绝对路径配置multer的上传目录,还要确保目录存在(因为Heroku重启后目录会消失):
const fs = require('fs'); const path = require('path'); const uploadsDir = path.join(__dirname, 'uploads'); // 递归创建目录,避免Heroku重启后目录不存在 fs.mkdirSync(uploadsDir, { recursive: true }); const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadsDir); }, filename: (req, file, cb) => { cb(null, Date.now() + '-' + file.originalname); } }); const upload = multer({ storage: storage }); - React里用绝对路径请求图片,比如
/uploads/xxx.jpg,别用相对路径,或者在前端环境变量里配置后端API域名,动态拼接完整URL。
最后再敲个警钟
纯Express项目的“正常”真的只是暂时的,只要dyno一重启,你上传的图片就没了。所以不管哪种项目,只要涉及用户上传的文件,一定要用专门的云存储服务,这才是长久之计。
内容的提问来源于stack exchange,提问作者Elano

