全栈联调:前端怎么调后端接口
API 写好了,数据库搭好了,前后端之间隔着一层 HTTP 协议。怎么让前端页面真正拿到后端的数据?这个问题在独立开发中每天都会遇到。
白物集项目前后端分离:Express API(Node.js)提供数据,Astro SSR 在服务端渲染时调用 API。这套模式兼顾了 SEO 和开发效率。本文用白物集的真实代码拆解全栈联调的全过程。
一、API 路由的组织方式
后端不是一个巨大的文件。白物集的 Express 路由按功能拆分到单独的文件,通过 index.js 统一挂载:
// api/src/index.js
import articlesRouter from './routes/articles.js';
import ingestRouter from './routes/ingest.js';
import cardsRouter from './routes/cards.js';
// ... 其他路由
const v1 = express.Router();
v1.use('/articles', apiLimiter, articlesRouter);
v1.use('/content/ingest', ingestLimiter, ingestRouter);
// ...
app.use('/api/v1', v1);
每个路由文件负责一个资源。/api/v1/articles 下再分 GET(列表/详情)、PATCH(更新)、DELETE(删除)。前端只通过 HTTP 调用这些端点,不需要知道后端用的是 Express 还是别的框架。
二、前端怎么发起请求
白物集的 API 客户端放在 website/src/lib/api.js,所有前端页面共用。关键代码只有几行:
const BASE = 'http://127.0.0.1:3001';
const API_PREFIX = '/api/v1';
async function get(path) {
const res = await fetch(BASE + API_PREFIX + path);
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`API ${res.status}: ${body.slice(0, 100)}`);
}
return res.json();
}
fetch 是浏览器和 Node.js 都有的原生 API。Astro 在服务端渲染时,这个 get() 在 Node.js 环境中执行,请求同机的 Express 进程。返回的 JSON 对象直接作为 Astro 组件的数据源。
理解这一点很重要:不是浏览器直接请求 API。浏览器请求的是 Astro 渲染好的完整 HTML,Astro 在编译/渲染阶段先通过内部的 fetch 拿到数据,拼进模板再输出给浏览器。用户看到的 HTML 里已经有完整内容了。
三、页面组件如何消费数据
以文章详情页 [id].astro 为例,它在页面顶部用服务端代码直接调用 API 客户端:
---
import { getArticle, getArticles } from '../../lib/api.js';
const id = Number(Astro.params.id);
let article;
try {
const result = await getArticle(id);
article = result.article;
} catch {
return new Response(null, { status: 404 });
}
const { articles: all } = await getArticles({ limit: 100 });
// 找上一篇/下一篇
// 交叉推荐 ...
---
getArticle(id) 背后调的是 GET /api/v1/articles/:id。后端从 Supabase 查到文章数据后返回 JSON。Astro 拿到 article 对象,在模板中直接渲染 {article.title}、{article.content_html}。
你看到的是同一份数据模型——Express API 返回什么字段,Astro 模板就能用什么字段。不需要中间转换层。
四、后端 API 的完整流程
看白物集的 articles.js 路由,一个 GET 详情接口的实际流程:
router.get('/:id', async (req, res) => {
const parsed = articleIdParam.safeParse(req.params);
if (!parsed.success) return badRequest(res, 'Invalid article ID');
const { id } = parsed.data;
const result = await getOrSet('articles', { id }, async () => {
const { data, error } = await supabase
.from('articles')
.select('*')
.eq('id', id)
.single();
if (error) {
if (error.code === 'PGRST116') return null;
throw error;
}
// 获取同类文章推荐
const { data: related } = await supabase
.from('articles')
.select('id, title, abstract, category, published_at')
.eq('category', data.category)
.eq('status', 'published')
.neq('id', id)
.limit(3);
return { article: data, related: related || [] };
}, TTL.ARTICLE_DETAIL);
if (!result) return notFound(res, `Article #${id} not found`);
res.json(result);
});
注意三个关键点:
- 输入校验优先 —
articleIdParam.safeParse(req.params)用 Zod 做类型校验,参数不合法直接返回 400,不碰数据库 - 缓存在外层 —
getOrSet把 API 响应用 TTL 缓存起来,同 ID 的请求在 120 秒内走缓存不走数据库 - 一次请求带多份数据 — 文章内容 + 推荐列表一次返回,前端不需要再发请求
五、SSR 级缓存——不要重复查数据库
白物集 API 客户端最大特点是 SSR 级缓存。前端在 Astro 渲染进程内维护一个 Map:
const cache = new Map();
const TTL = {
list: 60_000, // 列表缓存 60s
detail: 120_000, // 详情缓存 120s
categories: 300_000, // 分类缓存 300s
};
function cached(key, ttl, fetcher) {
const now = Date.now();
const entry = cache.get(key);
if (entry && (now - entry.time) < ttl) {
return entry.data;
}
return fetcher().then(data => {
cache.set(key, { data, time: now });
return data;
});
}
这个设计解决了一个常见问题:同一个列表页被频繁刷新时,每次都打到 Express API → Supabase。SSR 层的短 TTL 让多次请求在内存中直接命中,后端服务压力锐减。
六、避坑:content_html 必须同时写入
全栈联调中最容易踩的坑是:POST 内容时只传了 content_md,没传 content_html。Astro 详情页的渲染模板是:
<Fragment set:html={article.content_html || '<p>内容加载中...</p>'} />
content_html 为空时,页面显示「内容加载中」。API 不会自动把 markdown 转成 HTML——前端只管渲染传过来的字段。解决办法是 POST 前先用 markdown 库转换:
import markdown
html = markdown.markdown(md_clean, extensions=['fenced_code', 'tables'])
然后 content_md 和 content_html 同时入库。这也是白物集自动化管线中强制校验的步骤。
七、前后端数据契约
全栈联调的核心是数据契约——前端期待什么形状的数据,后端就返回什么。白物集的 RESTful 设计让前后端解耦:
- Astro 页面通过
getArticle(id)获取文章,返回{ article: {...}, related: [...] } - 列表页通过
getArticles({ category, limit })获取列表,返回{ articles: [...], total: N } - 首页需要统计分类时,调
getCategories()返回[{ category: 'AI', count: 10 }, ...]
每个接口的返回结构固定。前端按结构渲染,后端按结构返回。如果某个接口改了字段名,前端渲染会直接报错——比悄悄显示错误数据好得多。
八、从联调到部署
前端调后端的过程可以用一个 curl 命令模拟——这和白物集内容管线 POST 文章的操作完全一致:
curl -s https://ai.golfr20.cn/api/v1/articles?limit=3
返回的 JSON 就是前端页面在 SSR 时拿到的数据。能调到这个接口,说明 Express API 在跑、Supabase 有数据、网络层通。联调的第一步不是写代码,是先确认这条链路是通的。
下一篇预告: 从本地到线上——Nginx + 服务器部署入门。一个网站写好了,怎么让它 24 小时跑在公网上。