为什么生成了 dist,项目还是可能跑不起来?从一个 React + Express + MySQL 项目看部署闭环

0 阅读5分钟

为什么生成了 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

需要检查的是:

  1. 数据库地址和端口是否正确;
  2. MySQL 是否监听了远程地址;
  3. 云安全组和系统防火墙是否允许访问;
  4. 数据库用户是否授权了当前来源主机;
  5. 目标数据库和表是否存在。

编译成功并不能证明这些条件成立。

再看前端: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.jsonsrc 输出到 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

上传

  • 后端:distpackage.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