科技前沿 科普Web 基础教程建站系列

全栈联调:前端怎么调后端接口

📅 2026-07-18

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);
});

注意三个关键点:

  1. 输入校验优先articleIdParam.safeParse(req.params) 用 Zod 做类型校验,参数不合法直接返回 400,不碰数据库
  2. 缓存在外层getOrSet 把 API 响应用 TTL 缓存起来,同 ID 的请求在 120 秒内走缓存不走数据库
  3. 一次请求带多份数据 — 文章内容 + 推荐列表一次返回,前端不需要再发请求

五、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_mdcontent_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 小时跑在公网上。

← Apple 诉 OpenAI:商业秘密案如何重塑 AI 格局 → Apple 频道 uv:87k Stars 的 Python 包管理革命 →
🍎 Apple 深度分析
本文基于 Apple 公开资料及行业分析撰写。观点仅供参考与学习交流。