为什么生成了 dist,项目还是可能跑不起来?从一个 React + Express + MySQL 项目看部署闭环
全栈项目部署时,最容易出现的误判是:前端、后端都生成了 dist,就等于上线完成。这个判断少了几项关键条件:前端请求地址、后端运行时环境、MySQL 授权和服务器进程必须彼此匹配。本文通过 Future Capsule 的实际代码结构,串起页面、API、数据库和生产部署之间的关系。命令与云服务器步骤来自源码配置,运行未验证。
先建立一个正确的心智模型
部署不是把整个项目目录搬到服务器,而是把不同类型的产物交给不同的运行环境:
前端 src --Vite--> client/dist --静态服务器--> 浏览器
后端 src --tsc--> server/dist --Node.js--> Express API
↓
MySQL
所以:
client/dist是浏览器资源;server/dist是 Node.js 代码;node_modules是运行依赖;.env是运行时配置;- MySQL 和网络权限属于项目之外,但决定项目能否启动。
先看后端:为什么数据库失败会让服务看起来没启动
后端入口做了一个明确选择:先验证数据库,再开放 HTTP 服务。
const startServer = async () => {
try {
await testConnection();
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
} catch (error: any) {
console.error('Failed to start server:');
console.error(error.message);
process.exit(1);
}
};
这个顺序意味着,只要 testConnection() 失败,app.listen 就不会执行。于是下面这类错误本质上是数据库访问问题,而不是端口监听问题:
Host '某个来源 IP' is not allowed to connect to this MySQL server
需要检查的是:
- 数据库地址和端口是否正确;
- MySQL 是否监听了远程地址;
- 云安全组和系统防火墙是否允许访问;
- 数据库用户是否授权了当前来源主机;
- 目标数据库和表是否存在。
编译成功并不能证明这些条件成立。
再看前端:localhost 为什么是部署陷阱
项目 API 服务中的默认配置是:
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
这段代码在本地开发很方便,但在生产环境容易造成误解。在通常的浏览器访问场景中,localhost 指向当前浏览器所在的设备:
用户浏览器 --localhost:3001--> 用户电脑
而不是:
用户浏览器 --localhost:3001--> 云服务器
因此,生产打包前应把 client/.env 设置为后端真实地址:
VITE_API_URL=http://你的后端域名或公网IP:3001
然后重新执行 npm run build。这是因为 Vite 环境变量会在构建阶段进入前端静态资源;只在服务器上修改一个前端运行时 .env,不会自动改变已经生成的 JavaScript。
这套业务是怎样流转的?
项目的两个主要接口都挂载在 /api/capsules:
首页加载
→ useInfiniteScroll
→ GET /api/capsules?page=1&limit=20
→ 分页查询 MySQL
→ 返回 data、hasMore、total
提交胶囊
→ CapsuleForm
→ POST /api/capsules
→ 校验内容和未来时间
→ INSERT capsules
GET 接口使用页码和条数计算偏移量,并按照创建时间倒序排列。前端瀑布流根据窗口宽度切换为 1、2 或 3 列;未解锁卡片每秒计算倒计时,到点后刷新列表。
真正重要的是解锁判断发生在服务端:
const isUnlocked = new Date(capsule.unlock_time) <= now;
content: isUnlocked ? capsule.content : null
这比只在前端隐藏内容可靠,因为未解锁正文根本不会出现在接口响应中。
dist 里应该有什么?
后端执行 npm run build 后,TypeScript 按 tsconfig.json 将 src 输出到 dist。后端生产上传可以采用:
server/
├─ dist/
├─ package.json
├─ package-lock.json
└─ .env
本地 Windows 的 node_modules 不建议直接上传到 Linux 服务器。服务器上进入后端目录重新安装生产依赖:
npm install --omit=dev
npm run start
但如果服务器要重新执行 npm run build,就需要安装包含 TypeScript 的开发依赖。
前端只上传 client/dist 中的内容,并让网站根目录直接看到:
index.html
assets/
如果访问公网 IP 看到的是宝塔默认页面,通常说明网站根目录仍指向默认目录,或者上传时多套了一层 dist。
一张排错表,比重复打包更有用
| 问题 | 说明 | 处理方向 |
|---|---|---|
找不到 tsc | 编译器依赖没有安装 | server 中执行 npm install |
| MySQL 不允许来源主机 | 账号来源权限不足 | 授权、白名单、安全组、防火墙 |
| 前端请求 localhost | 构建时 API 地址错误 | 修改 VITE_API_URL 后重新构建 |
| 显示宝塔默认页 | 网站根目录或上传层级错误 | 让根目录直接包含 index.html |
找不到 dist/app.js | 后端构建产物缺失 | 检查 build 输出和上传位置 |
一套可执行的部署顺序
本地构建
cd server
npm install
npm run build
cd ../client
npm install
npm run build
上传
- 后端:
dist、package.json、锁文件和.env; - 前端:
client/dist的内容; - 不上传本地
node_modules。
服务器启动
cd server
npm install --omit=dev
npm run start
生产环境可使用 PM2 管理 Node.js 进程:
pm2 start dist/app.js --name future-capsule-server
pm2 save
上线验收
浏览器能打开正确 index.html
↓
前端请求的不是错误的 localhost
↓
后端端口可访问
↓
后端启动日志显示数据库连接成功
↓
GET/POST /api/capsules 正常
最后一个判断:不要把构建问题和运行问题混在一起
npm run build 失败,优先看 TypeScript、Vite 和依赖安装;npm run start 失败,优先看 dist/app.js、.env、Node.js 依赖和数据库;网页显示默认页,优先看 Nginx/宝塔网站根目录;网页能打开但接口失败,优先看 VITE_API_URL、后端端口和 CORS。
这个项目的经验可以迁移到很多全栈应用:dist 只回答“代码产物在哪里”,并不回答“服务能否连接外部依赖”。只有构建、配置、网络、权限和进程全部闭环,部署才算真正完成。
运行未验证:本文示例命令、数据库授权和云服务器配置需在目标环境逐项确认。
标签: 前端部署、Node.js、MySQL、Vite、TypeScript