为一名前端开发者,终于完整开发并部署了一个Next.js全栈项目,涵盖用户登录注册、商品发布、实时聊天等核心功能。部署过程中踩了无数坑,从数据库连接失败到WebSocket频繁断开,从图片上传报错到环境变量不生效,每一个问题都花了不少时间排查。
今天就把整个部署流程、用到的所有工具、遇到的问题及解决方案,完整梳理出来,希望能帮到正在做Next.js全栈部署的小伙伴,少走弯路,一次上线成功!
一、项目基础信息(先明确核心技术栈)
先交代下项目的核心技术栈,方便大家对应自己的项目场景参考:
- 前端框架:Next.js 16(App Router)+ TypeScript
- UI样式:Tailwind CSS
- 数据库:MySQL(免费在线实例)
- ORM工具:Prisma(连接、操作数据库)
- 部署平台:Vercel(Next.js官方推荐,零配置部署)
- 图片存储:Vercel Blob(解决Vercel只读文件系统问题)
- 实时聊天:Pusher(第三方WebSocket服务,避免自建服务器)
- 代码托管:GitHub(版本管理+Vercel自动拉取部署)
项目核心功能:用户登录注册、商品发布/展示、地址管理、一对一实时聊天,是一个完整的小型全栈应用,适合作为简历项目或练手案例。
二、用到的所有网站/工具(全程实战可用)
整个部署过程没有用到复杂的服务器,全部依赖免费工具/网站,新手也能轻松上手,整理了一张清单,一目了然:
| 工具/网站 | 核心作用 | 解决的问题 |
|---|---|---|
| GitHub | 代码托管、版本管理 | 存放项目代码,供Vercel自动拉取部署 |
| Vercel | 全栈项目部署、公网访问 | 实现项目一键上线,提供环境变量、日志排查功能 |
| freesqldatabase.com | 免费在线MySQL数据库 | 提供公网可访问的数据库实例,无需本地搭建 |
| Prisma | ORM工具,连接/操作数据库 | 无需写复杂SQL,自动建表、实现CRUD,防SQL注入 |
| Vercel Blob | 免费文件存储服务 | 解决Vercel只读文件系统,实现图片上传功能 |
| Pusher | 第三方实时消息服务 | 实现稳定WebSocket通信,解决聊天断开(code=1006)问题 |
| phpMyAdmin | 数据库可视化管理 | 查看数据库表结构、排查数据问题 |
三、完整部署流程(一步步跟着来,不踩坑)
部署全程遵循「代码准备 → 数据库配置 → 部署平台配置 → 功能适配 → 问题排查」的逻辑,每一步都对应实际操作,新手可直接照做。
第一步:代码准备与GitHub托管
- 本地完成Next.js全栈项目开发,确保核心功能(登录、商品、聊天)本地能正常运行。
- 在GitHub上新建仓库(比如命名为eslife-0929),将本地项目代码推送到GitHub仓库(注意忽略node_modules、.env等敏感文件)。
- 确认仓库代码完整,包含Prisma配置文件(schema.prisma)、API接口(app/api目录)、前端组件等核心文件。
第二步:数据库配置(MySQL + Prisma)
数据库是全栈项目的核心,这里用免费在线MySQL,无需本地搭建,步骤如下:
-
访问freesqldatabase.com,注册账号后,系统会自动创建一个MySQL数据库,获得核心连接信息:主机(host)、端口(port)、用户名(user)、密码(password)、数据库名(dbname)。
-
在本地项目根目录,配置Prisma:
- 修改schema.prisma文件,定义User、Commodity、Chat等数据表模型(对应项目功能)。
- 在.env.local文件中,配置数据库连接串:DATABASE_URL=mysql://user:password@host:port/dbname
-
本地执行prisma db push命令,Prisma会自动根据schema.prisma在远程MySQL数据库中生成对应的数据表。
-
通过phpMyAdmin登录数据库,确认数据表创建成功,确保本地能正常连接并操作数据库。
第三步:Vercel部署项目(核心步骤)
Vercel是Next.js官方部署平台,零配置、免费、自动CI/CD,是新手部署Next.js项目的首选,步骤如下:
- 访问Vercel官网,登录账号(建议用GitHub账号登录,方便关联仓库)。
- 点击「New Project」,导入GitHub上的项目仓库,Vercel会自动识别Next.js项目,无需手动配置构建命令。
- 配置环境变量:在Vercel项目后台,进入Settings → Environment Variables,添加项目所需的环境变量(重点是DATABASE_URL),并勾选Production、Preview、Development三个环境,确保环境变量全局生效。
- 点击「Deploy」,Vercel会自动拉取GitHub代码、安装依赖、构建项目,等待1-2分钟,部署完成后会生成一个公网可访问的域名(比如eslife-0929.vercel.app)。
第四步:功能适配(图片上传 + WebSocket)
部署完成后,会发现两个核心功能报错(图片上传、聊天),这是Vercel和免费WebSocket测试地址的限制,需要针对性适配:
1. 图片上传适配(解决Vercel只读文件系统)
Vercel的文件系统是只读的,无法将图片存到项目本地(会报EROFS: read-only file system错误),解决方案是使用Vercel Blob免费存储:
- 在Vercel项目后台,进入Storage → Create Store,选择Blob,创建一个存储实例(命名随意,比如eslife-images),选择Public模式(图片需要公网访问)。
- 创建完成后,Vercel会自动生成BLOB_READ_WRITE_TOKEN环境变量,无需手动创建。
- 本地项目安装@vercel/blob依赖(npm install @vercel/blob),修改图片上传API代码,替换成本地文件上传为Vercel Blob上传(代码见文末附录)。
- 将修改后的代码推送到GitHub,Vercel自动重新部署,图片上传功能正常。
2. WebSocket适配(解决聊天断开code=1006)
初期使用公共WebSocket测试地址(如wss://ws.postman-echo.com/raw),频繁断开报错(code=1006),原因是公共测试地址不支持持久聊天,解决方案是使用Pusher第三方实时服务:
- 访问Pusher官网,注册账号后,新建一个Channels应用,获得4个核心密钥:appId、key、secret、cluster。
- 在Vercel环境变量中,添加Pusher相关变量(NEXT_PUBLIC_PUSHER_KEY、NEXT_PUBLIC_PUSHER_CLUSTER、PUSHER_APP_ID、PUSHER_SECRET),勾选三个环境并保存。
- 修改前端聊天组件代码(初始化Pusher、订阅频道、监听消息)和后端发送消息API(触发Pusher消息推送),替换原来的WebSocket连接代码(代码见文末附录)。
- 推送代码到GitHub,Vercel自动部署,聊天功能稳定连接,不再出现断开报错。
第五步:测试验证
部署完成后,访问Vercel生成的公网域名,逐一测试核心功能:
- 登录注册:正常访问,数据能写入数据库
- 商品发布:能正常上传图片、保存商品信息
- 实时聊天:能正常发送、接收消息,无断开报错
- 地址管理:能正常新增、编辑、删除地址
所有功能正常,说明项目部署成功,可公网访问、正常使用。
四、部署过程中遇到的核心问题及解决方案(重点!)
这部分是重点,整理了我部署时遇到的5个核心问题,每个问题都有明确的报错、原因和解决方案,大家遇到相同问题可直接参考:
问题1:Prisma客户端未生成,部署失败(PrismaClientInitializationError)
- 报错信息:Invalid
prisma.user.findUnique()invocation: PrismaClientInitializationError - 原因:Vercel缓存依赖,构建时不会自动执行prisma generate,导致Prisma客户端未生成,无法连接数据库。
- 解决方案:修改package.json的build命令,添加prisma generate,修改后为:"build": "prisma generate && next build",推送代码重新部署即可。
问题2:数据库连接失败,用户名被拒绝访问(%20xxx)
- 报错信息:User 'sql12821786' was denied access on the database '%20sql12821786'
- 原因:DATABASE_URL环境变量中,数据库名前面多了一个空格(%20是URL编码后的空格),导致连接失败。
- 解决方案:重新粘贴无空格的DATABASE_URL连接串,确保前后无空格、无引号,保存后重新部署。
问题3:环境变量未生效(Environment variable not found: DATABASE_URL)
- 报错信息:Environment variable not found: DATABASE_URL,Validation Error Count: 1
- 原因:Vercel环境变量未勾选Production环境,导致线上部署时无法读取环境变量。
- 解决方案:进入Vercel环境变量页面,勾选Production、Preview、Development三个环境,点击Save保存,重新部署。
问题4:图片上传报错(EROFS: read-only file system)
- 报错信息:Error: EROFS: read-only file system, open '/var/task/public/uploads/xxx.png'
- 原因:Vercel服务器文件系统是只读的,无法将图片写入本地public目录。
- 解决方案:使用Vercel Blob云存储,替换图片上传逻辑,具体代码见附录。
问题5:WebSocket频繁断开(code=1006)
- 报错信息:WebSocket 已断开(code=1006),请确认聊天服务已启动
- 原因:使用的公共WebSocket测试地址(如postman-echo)是echo服务,不支持持久聊天,连接会自动断开。
- 解决方案:接入Pusher第三方实时服务,替换WebSocket连接逻辑,具体代码见附录。
五、总结与感悟
从开发到部署,整个过程虽然踩了很多坑,但每解决一个问题,都对Next.js全栈部署有了更深入的理解。其实Next.js全栈部署并不复杂,核心是掌握「环境变量配置、数据库连接、第三方服务适配」这三个关键点。
本次部署全程使用免费工具,无需购买服务器,新手也能轻松上手,最终实现了项目公网访问,所有核心功能正常运行,这个项目也成为了我简历中的一个重要亮点。
最后,整理了两个核心功能的适配代码,大家可直接复制使用,避免重复踩坑。如果大家在部署过程中遇到其他问题,欢迎在评论区交流,一起解决!
附录:核心适配代码(直接复制可用)
1. Vercel Blob图片上传API(app/api/upload/route.ts)
import { put } from '@vercel/blob';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: '请选择文件' }, { status: 400 });
}
// 上传到Vercel Blob,公开访问
const blob = await put(file.name, file, {
access: 'public',
});
// 返回图片URL,用于前端展示和数据库存储
return NextResponse.json({
url: blob.url,
});
} catch (error) {
return NextResponse.json({ error: '上传失败' }, { status: 500 });
}
}
2. Pusher实时聊天适配代码
前端聊天组件(components/ChatWindow.tsx)
import { useEffect, useState } from 'react';
import Pusher from 'pusher-js';
const ChatWindow = ({ userId, targetUserId }: { userId: number; targetUserId: number }) => {
const [messages, setMessages] = useState<Array<{fromUserId: number, content: string, timestamp: string}>>([]);
const [inputContent, setInputContent] = useState('');
useEffect(() => {
// 初始化Pusher
if (!process.env.NEXT_PUBLIC_PUSHER_KEY || !process.env.NEXT_PUBLIC_PUSHER_CLUSTER) return;
const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
});
// 订阅私有聊天频道(两个用户ID组合,保证隐私)
const channelName = `private-chat-${Math.min(userId, targetUserId)}-${Math.max(userId, targetUserId)}`;
const channel = pusher.subscribe(channelName);
// 监听聊天消息
channel.bind('chat-message', (data) => {
setMessages(prev => [...prev, data]);
});
// 组件卸载时清理连接
return () => {
channel.unbind_all();
channel.unsubscribe();
pusher.disconnect();
};
}, [userId, targetUserId]);
// 发送消息
const sendMessage = async () => {
if (!inputContent.trim()) return;
await fetch('/api/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fromUserId: userId, toUserId: targetUserId, content: inputContent }),
});
setInputContent('');
};
return (
{messages.map((msg, index) => (
<div
key={ ${
msg.fromUserId === userId ? 'bg-blue-500 text-white self-end' : 'bg-gray-200 self-start'
}`}
>
{msg.content}{new Date(msg.timestamp).toLocaleTimeString()}
))}
<input
type="text"
value={ onChange={(e) => setInputContent(e.target.value)}
placeholder="输入消息..."
className="flex-1 px-3 py-2 border rounded"
/>
<button onClick={发送
);
};
export default ChatWindow;
后端发送消息API(app/api/chat/send/route.ts)
import { NextRequest, NextResponse } from 'next/server';
import Pusher from 'pusher';
// 初始化Pusher后端实例
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID as string,
key: process.env.PUSHER_KEY as string,
secret: process.env.PUSHER_SECRET as string,
cluster: process.env.PUSHER_CLUSTER as string,
useTLS: true, // 开启加密连接,避免安全问题
});
export async function POST(request: NextRequest) {
try {
const { fromUserId, toUserId, content } = await request.json();
// 定义聊天频道(与前端一致,保证消息能正确推送)
const channelName = `private-chat-${Math.min(fromUserId, toUserId)}-${Math.max(fromUserId, toUserId)}`;
const eventName = 'chat-message';
// 触发消息推送,推送给订阅该频道的前端
await pusher.trigger(channelName, eventName, {
fromUserId,
toUserId,
content,
timestamp: new Date().toISOString(),
});
return NextResponse.json({ success: true, message: '消息已发送' });
} catch (error) {
console.error('Pusher推送失败:', error);
return NextResponse.json({ error: '发送失败' }, { status: 500 });
}
}