Vercel结合Turso与GitHub:我的现代全栈网站搭建踩坑实录
为啥我要折腾这套组合?
说实话,之前我一直在用传统的方式部署项目——买个云服务器,装个Nginx,手动配SSL证书,数据库用MySQL或者PostgreSQL。听起来挺稳的是吧?但实际用起来,痛点真的太多了。
首先,服务器要自己维护,系统补丁要打,Nginx配置要调,SSL证书过期了还得手动续。更头疼的是数据库连接池的问题——有一阵子我的博客突然流量上来了,结果MySQL连接池直接被打满,网站卡得跟幻灯片似的。我临时手忙脚乱地加了个Redis做缓存,又折腾了半天PgBouncer连接池中间件,才勉强扛住。
还有全球访问的问题。我服务器放在国内,海外用户反馈说打开页面要好几秒,这体验实在太差了。
后来我偶然了解到了 Vercel + Turso + GitHub 这套组合,试了一下,真的回不去了。这篇文章就是我从零开始搭建的完整记录,包括我踩过的各种坑,希望对你有帮助。
这三个东西分别是啥?
Vercel:部署神器
Vercel 简单来说就是一个专门给前端和全栈应用用的云平台。你把代码推到GitHub,它自动帮你构建、部署,还给你一个 .vercel.app 的域名直接访问。全球几十个边缘节点,国内外访问速度都不错。
最让我惊喜的是它对 Next.js 的支持简直是原生级别的——Serverless Functions、Edge Functions 都能无缝运行。而且2026年它全面拥抱了AI,推出了 v0、AI SDK 这些工具,在上面做AI驱动的网站变得特别方便。
Turso:边缘数据库的黑马
这是我踩坑最多也最惊喜的部分。传统的关系型数据库在 Serverless 环境下有个致命问题:无状态函数频繁触发,数据库连接池瞬间就耗尽了。
Turso 基于 libSQL(SQLite 的开源分支),它把数据库文件复制到全球边缘节点,查询走的是 HTTP 协议而不是传统的 TCP 长连接。这意味着什么?读操作延迟降到了毫秒级,高并发下依然稳如老狗。
我第一次用 Turso 的时候还犯了个低级错误——以为它跟普通数据库一样用TCP连接,结果怎么都连不上。后来才搞明白,它默认就是 HTTP 模式,要用 @libsql/client/http 这个包。
GitHub Actions:自动化的核心
这个不用多说了,代码推上去,自动触发 Vercel 构建部署,全自动化的闭环。push 一下代码,一分钟不到线上就更新了,比我自己手动 ssh + git pull + pm2 restart 的时代效率高了不知道多少倍。
技术选型对比:我为啥不继续用老方案?
老方案(虚拟机 + 集中式数据库)的痛点:
- 手动管理服务器、配置 Nginx 反向代理、处理 HTTPS 证书,光是这些运维工作就能消耗掉大量时间
- 数据库连接池容易被高并发打满,需要额外引入 Redis 或 PgBouncer
- 全球用户访问时,物理距离导致的网络延迟难以克服
新方案(Vercel + Turso + GitHub)的优势:
- 无需管理服务器,应用按需自动扩缩容,从 0 并发到 10 万并发不用改架构
- 通过 Turso 的嵌入式副本技术,数据库直接运行在离用户最近的边缘节点,读取延迟接近本地文件
- 只需一次
git push,Vercel 便自动完成构建、预览发布与正式部署
实战:手把手搭一个留言板系统
我选了一个 Next.js 的留言板作为示例,因为它足够简单又能展示全栈能力。
第一步:项目初始化
确保你装了 Node.js 18+,然后:
| 1 | # 创建 Next.js 项目
|
| 2 | npx create-next-app@latest edge-guestbook
|
| 3 |
|
| 4 | # 进入项目目录
|
| 5 | cd edge-guestbook
|
| 6 |
|
| 7 | # 安装 Turso 的 libSQL 客户端
|
| 8 | npm install @libsql/client
|
第二步:配置 Turso 数据库
先去 turso.tech 注册账号,然后安装 CLI:
| 1 | # 安装 Turso CLI
|
| 2 | curl -sSfL https://get.tur.so/install.sh | bash
|
| 3 |
|
| 4 | # 登录
|
| 5 | turso auth login
|
| 6 |
|
| 7 | # 创建数据库
|
| 8 | turso db create guestbook-db
|
| 9 |
|
| 10 | # 获取连接 URL(后面配环境变量要用)
|
| 11 | turso db show guestbook-db --url
|
| 12 |
|
| 13 | # 输出示例: libsql://guestbook-db-<your-name>.turso.io
|
| 14 |
|
| 15 | # 创建鉴权 Token
|
| 16 | turso db tokens create guestbook-db
|
然后连接数据库建表:
| 1 | turso db shell guestbook-db
|
在 shell 里执行:
| 1 | CREATE TABLE messages (
|
| 2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 3 | username TEXT NOT NULL,
|
| 4 | content TEXT NOT NULL,
|
| 5 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 6 | );
|
| 7 |
|
| 8 | .quit
|
踩坑提醒:我第一次建表的时候忘了 AUTOINCREMENT,后来插入数据时 id 字段老是报错,排查了半天才发现。
第三步:写业务逻辑
新建 app/api/messages/route.ts:
| 1 | import { createClient } from '@libsql/client/http';
|
| 2 | import { NextResponse } from 'next/server';
|
| 3 |
|
| 4 | const client = createClient({
|
| 5 | url: process.env.TURSO_DATABASE_URL!,
|
| 6 | authToken: process.env.TURSO_AUTH_TOKEN!,
|
| 7 | });
|
| 8 |
|
| 9 | export async function GET() {
|
| 10 | try {
|
| 11 | const result = await client.execute('SELECT * FROM messages ORDER BY created_at DESC');
|
| 12 | return NextResponse.json(result.rows);
|
| 13 | } catch (error) {
|
| 14 | console.error('Database query failed:', error);
|
| 15 | return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 });
|
| 16 | }
|
| 17 | }
|
| 18 |
|
| 19 | export async function POST(request: Request) {
|
| 20 | try {
|
| 21 | const { username, content } = await request.json();
|
| 22 | await client.execute({
|
| 23 | sql: 'INSERT INTO messages (username, content) VALUES (?, ?)',
|
| 24 | args: [username, content],
|
| 25 | });
|
| 26 | return NextResponse.json({ success: true }, { status: 201 });
|
| 27 | } catch (error) {
|
| 28 | console.error('Insert failed:', error);
|
| 29 | return NextResponse.json({ error: 'Failed to post message' }, { status: 500 });
|
| 30 | }
|
| 31 | }
|
前端页面 app/page.tsx:
| 1 | 'use client';
|
| 2 |
|
| 3 | import { useState, useEffect } from 'react';
|
| 4 |
|
| 5 | interface Message {
|
| 6 | id: number;
|
| 7 | username: string;
|
| 8 | content: string;
|
| 9 | created_at: string;
|
| 10 | }
|
| 11 |
|
| 12 | export default function Home() {
|
| 13 | const [messages, setMessages] = useState<Message[]>([]);
|
| 14 | const [username, setUsername] = useState('');
|
| 15 | const [content, setContent] = useState('');
|
| 16 |
|
| 17 | useEffect(() => {
|
| 18 | fetch('/api/messages')
|
| 19 | .then((res) => res.json())
|
| 20 | .then((data) => setMessages(data));
|
| 21 | }, []);
|
| 22 |
|
| 23 | const handleSubmit = async (e: React.FormEvent) => {
|
| 24 | e.preventDefault();
|
| 25 | if (!username || !content) return;
|
| 26 |
|
| 27 | const res = await fetch('/api/messages', {
|
| 28 | method: 'POST',
|
| 29 | headers: { 'Content-Type': 'application/json' },
|
| 30 | body: JSON.stringify({ username, content }),
|
| 31 | });
|
| 32 |
|
| 33 | if (res.ok) {
|
| 34 | setContent('');
|
| 35 | const updatedMsgs = await (await fetch('/api/messages')).json();
|
| 36 | setMessages(updatedMsgs);
|
| 37 | }
|
| 38 | };
|
| 39 |
|
| 40 | return (
|
| 41 | <main className="max-w-2xl mx-auto p-6">
|
| 42 | <h1 className="text-3xl font-bold mb-8">边缘留言板</h1>
|
| 43 | <form onSubmit={handleSubmit} className="mb-8 space-y-4">
|
| 44 | <input
|
| 45 | type="text"
|
| 46 | placeholder="你的名字"
|
| 47 | value={username}
|
| 48 | onChange={(e) => setUsername(e.target.value)}
|
| 49 | className="w-full p-2 border rounded"
|
| 50 | />
|
| 51 | <textarea
|
| 52 | placeholder="写下你的留言..."
|
| 53 | value={content}
|
| 54 | onChange={(e) => setContent(e.target.value)}
|
| 55 | className="w-full p-2 border rounded"
|
| 56 | />
|
| 57 | <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
| 58 | 提交留言
|
| 59 | </button>
|
| 60 | </form>
|
| 61 | <div className="space-y-4">
|
| 62 | {messages.map((msg) => (
|
| 63 | <div key={msg.id} className="border p-4 rounded shadow-sm">
|
| 64 | <p className="font-semibold">{msg.username}</p>
|
| 65 | <p>{msg.content}</p>
|
| 66 | <p className="text-sm text-gray-500 mt-2">{msg.created_at}</p>
|
| 67 | </div>
|
| 68 | ))}
|
| 69 | </div>
|
| 70 | </main>
|
| 71 | );
|
| 72 | }
|
本地开发时创建 .env.local:
| 1 | TURSO_DATABASE_URL="你的_TURSO_URL"
|
| 2 | TURSO_AUTH_TOKEN="你的_TURSO_TOKEN"
|
运行 npm run dev 就能在本地看到效果了。
第四步:推到 GitHub 并部署到 Vercel
| 1 | git init
|
| 2 | git add .
|
| 3 | git commit -m "Initial commit: Vercel + Turso stack"
|
| 4 | git remote add origin https://github.com/your-username/edge-guestbook.git
|
| 5 | git push -u origin main
|
然后去 Vercel Dashboard,点 "Add New..." -> "Project",选择你刚创建的仓库。Vercel 会自动识别 Next.js 项目。
关键步骤:配置环境变量
在 Vercel 的部署设置页面,找到 "Environment Variables":
- 添加
TURSO_DATABASE_URL
- 添加
TURSO_AUTH_TOKEN
这里有个血泪教训:2026年4月 Vercel 出过一次安全事件,第三方AI工具 Context.ai 被入侵,攻击者获取了部分 Vercel 员工权限,访问了一些没标记为 "Sensitive" 的环境变量。所以!凡涉及数据库凭证、API密钥,在 Vercel 后台配置时,必须勾选 Sensitive 选项。标记后这些值在面板、日志中都会被加密隐藏。
点击部署,不到一分钟你的网站就上线了。
最佳实践和注意事项
数据库迁移管理
开发初期我直接用 turso db shell 手动建表,但在正经项目中,建议引入版本化的迁移工具。可以结合 GitHub Actions,在代码合并到主分支触发部署前,自动运行 Turso 的迁移脚本,确保数据库结构和代码保持一致。
架构的适用边界
Vercel + Turso 是高度分布式的无状态架构,非常适合读多写少、需要极速响应的 Web 应用,比如博客、内容站、AI 聊天界面、SaaS 工具。
但如果你在做一个需要极其复杂的跨表事务处理的系统(比如大型金融结算),传统的集中式数据库加上长连接池可能仍是更稳妥的选择。技术选型没有银弹,关键是找到适合自己场景的方案。
总结
这套 Vercel + Turso + GitHub 的工作流确实改变了我对全栈开发的认知。以前我花大量时间在运维和服务器管理上,现在 git push 一下就搞定了部署,精力完全放在业务逻辑和用户体验上。
更重要的是,它让我看到了 Web 架构演进的方向——从单点集中式,走向无服务器化、边缘化和智能化。如果你也想试试现代全栈开发,这套组合绝对值得入手。