创作背景
先和大家坦白:本项目是纯零基础、全程依托 AI 辅助完成的完整全栈实战项目。我本身没有任何代码经验,对 Astro 框架、Supabase 数据库、PGroonga 全文检索、服务端部署等技术栈完全零基础,也没有专业的产品设计和开发团队。
整个项目所有架构设计、数据库适配、链接检测、性能优化方案,都是在 AI 辅助下逐步打磨落地。零基础也能独立完成完整可上线的开源项目,这也是我想分享这篇实战文章的核心原因——普通人靠 AI 赋能,也能落地真实可用的全栈产品,快速积累实战开发经验。
目前项目已完整上线,欢迎大家体验、学习、支持!
[在线体验 →] (https://www.panws.net)
本文会完整分享技术实现,包括:
- 为什么最终选了 Astro 而不是 Next.js
- Supabase + PGroonga 实现中文全文搜索(比 LIKE 快两个数量级)
- 多数据库适配层设计(Supabase / Neon / Turso 一键切换)
- 静态站如何实现动态搜索
- 链接存活检测的工程化方案和踩坑
先看一下整体数据流,后面的实现都围绕这个架构展开:
- 数据采集层:从各渠道收集网盘资源链接,写入 PostgreSQL
- 存储检索层:Supabase PostgreSQL + PGroonga 扩展,支持中文全文索引
- 适配层:抽象数据库接口,支持 Supabase / Neon / Turso 三种后端
- 应用层:Astro 框架,静态页面 SSG + 搜索页 SSR 混合模式
- 检测层:异步批量检测链接存活状态,缓存结果
- 部署层:Vercel / Netlify 双平台适配,静态资源强缓存
框架:Astro
最初考虑过 Next.js,最终选择 Astro,不是因为 Next.js 不好,而是这个项目的场景更匹配 Astro 的特性:
- SSG 优先:首页、分类页、关于页全部静态生成,只有搜索结果页用 SSR。首屏加载极快,Lighthouse 性能分 95+
- 零 JS 框架开销:默认输出纯 HTML,不需要为了一个搜索框加载整个 React runtime
- 多框架岛屿支持:需要交互的地方可以混入 React / Vue / Svelte 组件,不强制全站统一
- 部署灵活:Vercel / Netlify 一键切换,adapter 抽象得很干净
实际踩坑:Astro 的 SSR 模式在 Vercel 上默认用 Edge Runtime,而 PostgreSQL 连接在 Edge 环境下有兼容问题,需要显式配置为 Node.js runtime。这个后面会详细说。
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
site: 'https://www.panws.net',
output: 'hybrid', // 混合模式:默认静态,指定页面 SSR
adapter: vercel(),
});
注意这里用的是 output: 'hybrid' 而不是 'static'。混合模式下,页面默认静态生成,只有加了 export const prerender = false 的页面才走 SSR,比纯 SSR 更省资源。
数据库:Supabase (PostgreSQL)
选择 Supabase 的核心理由:
- 免费额度对个人项目足够(500MB 数据库 + 带宽)
- 内置 PGroonga 扩展,中文全文搜索体验远超 PostgreSQL 自带的 tsvector
- PostgreSQL 生态,随时可以迁移到 Neon、自托管等其他方案
- 自带 Auth、Storage、Realtime,后续扩展功能不用额外搭服务
1. 数据库设计
资源表是整个系统的核心,字段设计兼顾检索效率和展示需求:
CREATE TABLE resources (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
url TEXT UNIQUE NOT NULL, -- 网盘链接
disk_type VARCHAR(20), -- baidu/quark/aliyun 等
password VARCHAR(20), -- 提取码
size VARCHAR(50), -- 文件大小
source VARCHAR(100), -- 来源频道
is_alive BOOLEAN DEFAULT true, -- 链接存活状态
last_checked TIMESTAMP, -- 上次检测时间
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 常用查询索引
CREATE INDEX idx_resources_disk_type ON resources(disk_type);
CREATE INDEX idx_resources_created_at ON resources(created_at DESC);
CREATE INDEX idx_resources_is_alive ON resources(is_alive);
相比原文,我增加了 is_alive 和 last_checked 两个字段。存活状态不应该每次查询时实时检测(太慢),而是异步检测后写回数据库,查询时直接过滤。
2. PGroonga 中文全文搜索
这是整个项目最关键的技术选型。PostgreSQL 自带的全文搜索对中文支持很差(需要装 zhparser 或用第三方分词),而 PGroonga 对中文原生支持,且查询性能优异。
-- 启用扩展(Supabase 已内置,直接启用即可)
CREATE EXTENSION IF NOT EXISTS pgroonga;
-- 创建全文搜索索引
CREATE INDEX idx_resources_search ON resources USING pgroonga(title);
-- 搜索查询(&@~ 是 PGroonga 的全文搜索操作符,支持中文分词)
SELECT * FROM resources
WHERE title &@~ '三体'
AND is_alive = true
ORDER BY created_at DESC
LIMIT 20;
在 Astro 页面中调用:
---
import { getDB } from '../lib/db';
const { q } = Astro.url.searchParams;
let results = [];
if (q) {
const db = getDB();
results = await db`
SELECT id, title, url, disk_type, password, size, created_at
FROM resources
WHERE title &@~ ${q}
AND is_alive = true
ORDER BY created_at DESC
LIMIT 20
`;
}
---
<html>
<body>
<h1>搜索结果:{q}</h1>
{results.map(item => (
<div class="card">
<h3>{item.title}</h3>
<p>{item.disk_type} · {item.size}</p>
<a href={item.url}>打开链接</a>
</div>
))}
</body>
</html>
性能对比:在 10 万条数据下,LIKE '%三体%' 查询耗时约 800ms-1.2s(全表扫描),而 PGroonga 索引查询稳定在 20-50ms,提升约 20-40 倍。数据量越大差距越明显。
3. 多数据库适配层
为了不被 Supabase 绑定,我设计了一层数据库适配。切换数据库只需要改环境变量,业务代码零改动。
import { createClient } from '@libsql/client'; // Turso (LibSQL)
import postgres from 'postgres'; // Supabase / Neon
const DB_SOURCE = process.env.DB_SOURCE || 'supabase';
export function getDB() {
switch (DB_SOURCE) {
case 'turso':
return createClient({
url: process.env.TURSO_URL,
authToken: process.env.TURSO_TOKEN,
});
case 'neon':
return postgres(process.env.DATABASE_URL);
case 'supabase':
default:
return postgres(process.env.SUPABASE_URL);
}
}
这里有个设计取舍:Turso 用的是 LibSQL 协议,API 和 postgres 库不完全一样。实际项目中我在适配层上面又包了一层统一的查询接口,避免业务代码直接操作底层 client。上面的代码是简化版,展示核心思路。
4. 链接存活检测
用户最讨厌的就是点开失效链接。但实时检测每个链接太慢(一个请求至少几百毫秒),所以采用异步批量检测 + 结果缓存的方案。
async function checkLink(url, diskType) {
const timeout = 5000;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
method: 'HEAD',
signal: controller.signal,
redirect: 'follow',
});
clearTimeout(timer);
return response.status === 200;
} catch {
return false;
}
}
// 批量检测(控制并发,避免被封 IP)
async function batchCheck(urls, concurrency = 5) {
const results = [];
for (let i = 0; i < urls.length; i += concurrency) {
const batch = urls.slice(i, i + concurrency);
const batchResults = await Promise.allSettled(
batch.map(url => checkLink(url))
);
results.push(...batchResults.map((r, j) => ({
url: batch[j],
alive: r.status === 'fulfilled' && r.value,
})));
}
return results;
}
5. 响应式设计
移动端流量占比超过 60%,响应式是刚需。用 CSS Grid 的 auto-fill 实现自适应卡片布局:
.results {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
@media (max-width: 640px) {
.results {
grid-template-columns: 1fr;
}
}
这部分是原文没有的,但也是最有价值的。分享几个实际开发中遇到的坑:
坑 1:Vercel Edge Runtime 不支持 PostgreSQL 连接
Astro + Vercel adapter 默认用 Edge Runtime 跑 SSR 页面,但 postgres 库依赖 Node.js 的 net 模块,在 Edge 环境下直接报错。
解决方案:在 astro.config.mjs 中配置 adapter 为 serverless(Node.js runtime),或者在页面级别指定:
// src/pages/search.astro 顶部
export const prerender = false;
// Vercel 配置文件中指定该路由使用 Node.js runtime
坑 2:HEAD 请求被网盘服务器拦截
部分网盘(尤其是百度网盘)对 HEAD 请求返回 403,但 GET 请求是正常的。导致存活检测误判。
解决方案:检测失败时降级为 GET 请求,但只读取 response headers 就 abort,不下载完整内容:
async function checkLink(url) {
// 先试 HEAD
try {
const res = await fetch(url, { method: 'HEAD' });
if (res.status !== 403) return res.status === 200;
} catch {}
// HEAD 被拦截,降级 GET(只读 header)
try {
const controller = new AbortController();
const res = await fetch(url, { signal: controller.signal });
controller.abort(); // 拿到 header 就取消,不下载 body
return res.status === 200;
} catch {
return false;
}
}
坑 3:PGroonga 在 Supabase 免费版的限制
Supabase 免费版数据库会在 7 天无活动后暂停,而 PGroonga 索引在数据库恢复后需要重建。另外免费版的内存只有 500MB,数据量超过 50 万条时索引构建会比较慢。
建议:个人项目 10 万条以内完全够用;数据量更大时考虑升级到 Pro 版,或者迁移到自托管 PostgreSQL。
坑 4:批量检测容易触发限流
并发太高会被网盘服务器封 IP,甚至触发验证码。实际测试中,并发数控制在 3-5 比较安全,同时每个请求之间加 200-500ms 的随机延迟。
Vercel 部署
{
"headers": [
{
"source": "/_astro/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
Netlify 部署
[build]
command = "npm run build"
publish = "dist"
[[headers]]
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Astro 构建产物的静态资源文件名自带 hash,所以可以放心设置一年的强缓存。
| 指标 | 数值 | 说明 |
|---|---|---|
| 首屏加载(静态页) | < 1s | SSG 页面,CDN 直出 |
| 搜索响应(SSR) | < 500ms | PGroonga 索引查询 20-50ms,其余为网络和渲染 |
| Lighthouse 性能分 | 95+ | 移动端测试 |
| 页面 JS 体积 | < 20KB | 零框架 runtime,只有少量交互脚本 |
做完这个项目,有几个比较深的体会:
- Astro 非常适合内容型 + 轻交互的网站。SSG + 少量 SSR 的混合模式,既保证了性能,又保留了动态能力,比纯 SPA 或纯 SSR 都更合适
- PGroonga 是中文全文搜索的性价比之选。不需要额外部署 Elasticsearch,一个 PostgreSQL 扩展就够了,个人项目完全够用
- 多数据库适配不难,但要尽早做。在项目初期抽象一层,后面迁移成本几乎为零;等业务代码写满了再重构就痛苦了
- 链接存活检测是体验刚需,但工程化要做好。异步检测 + 结果缓存 + 并发控制,三者缺一不可
GitHub 仓库:https://github.com/scenlinx/panws.net
欢迎在评论区交流:你们用 Astro 做混合渲染项目时踩过哪些坑?PGroonga 和其他中文搜索方案(Meilisearch、Elasticsearch、zhparser)你们怎么选?
