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

异步编程:Fetch API 与数据请求

📅 2026-07-07

你在浏览器打开一个网页,看到文章列表、最新评论、天气信息——这些数据不可能是 HTML 文件里写死的,而是页面从后端「要」来的。这个「要」的动作,在前端世界里就叫 Fetch API。

浏览器里的服务员

Fetch API 是浏览器内置的一个函数(fetch()),它的工作很简单:去某个地址拿数据,然后告诉你结果。白物集首页加载文章列表时,背后的代码长这样:

async function get(path) {
  const res = await fetch('https://api.example.com' + path);
  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`API ${res.status}: ${body.slice(0, 100)}`);
  }
  return res.json();
}

这段代码来自白物集网站 src/lib/api.js 第 42-49 行,是整个站点数据加载的基础。两件事值得拆开说。

await 是什么

await 的意思是「等结果回来再说」。没有 await 的时候,fetch() 返回的是一个「承诺(Promise)」,承诺会在将来某个时候给你数据,但你现在拿不到。加了 await,JavaScript 引擎会暂停执行当前函数,等网络请求完成后才继续往下走。

这就是异步编程最核心的区别:不等待 vs 等待结果

白物集的文章详情页在服务端渲染时,先等文章数据回来,再等全量文章列表回来做交叉推荐:

const result = await getArticle(id);
article = result.article;

const { articles: all } = await getArticles({ limit: 100 });

这两行来自 src/pages/articles/[id].astro 第 13-20 行。服务端渲染阶段,Astro 会等待两个 API 请求都完成之后,再生成最终的 HTML 发送给浏览器。用户打开文章页时,看到的已经是完整的页面,不需要再额外加载。

不做重复的傻请求

网络请求是页面加载中最慢的环节。白物集在 SSR 层做了一层内存缓存:

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

逻辑很简单:如果在 TTL(缓存有效期)内有过相同请求,直接返回缓存结果,不再调用 fetch()。列表页的缓存时间是 60 秒,详情页是 120 秒。这意味着一个用户在 60 秒内刷新 10 次首页,只有第一次会真正发起网络请求。

这背后有一个容易被新手忽视的细节:fetcher() 返回的是一个 Promisecached() 函数不管缓存命中与否,返回值始终是 Promise(因为缓存命中时返回的就是 entry.data——注意这个 .data 可能是之前 fetcher().then(data => { ... return data }) 的结果,也就是原始数据本身,而不是 Promise。等等,这里需要注意:

看仔细了——这个 cached() 函数有一个 bug 隐患:缓存命中时直接返回 entry.data(普通值),缓存未命中时返回 fetcher().then(...)(Promise)。虽然调用方统一用了 await cached(...)await 普通值也会直接通过,所以实践中没问题。但这个「有时返回 Promise 有时返回值」的模式,如果调用方不用 await 而是直接 .then(),就会出问题。这是异步编程中常见的「统一返回值类型」陷阱。

异步不是只有 fetch

白物集页面还有一个更小的异步操作——复制到剪贴板:

navigator.clipboard.writeText(this.dataset.url).then(function() {
  showToast('✅ 链接已复制');
}).catch(function() {
  showToast('❌ 复制失败,请手动复制');
});

点一下分享按钮,浏览器异步写入剪贴板,成功后弹一个提示。不阻塞用户继续操作,这就是异步编程的实际体验收益。

为什么不是同步等

一个常被问的问题:为什么不把网络请求做成同步的——调用 fetch() 然后卡住直到结果回来?

因为 JavaScript 是单线程的。如果 fetch() 是同步的,发一个请求出去,整个页面就冻住了——按钮点不了、滚动没反应、动画卡死。网络请求的耗时在几十毫秒到几秒之间,让用户等这么长时间什么都不做,体验极其糟糕。

await 的妙处在于:它只暂停当前函数,不阻塞整个线程。暂停期间,JavaScript 引擎可以处理点击事件、渲染动画、执行其他脚本。等网络请求回来后,引擎再回到暂停的地方继续执行。

你每天在用异步

白物集首页的加载过程完整走了一遍异步链:

  1. 浏览器请求 baiwuji.top
  2. Astro 服务端执行 await getArticles() → 用 fetch() 请求后端 API
  3. API 返回 JSON 数据
  4. Astro 用数据渲染 HTML 页面
  5. 页面发送到浏览器
  6. 浏览器解析 HTML 时,遇到 <img> 标签,异步发起图片请求
  7. 图片加载完后触发 img.decoding = 'async'(第 222 行),进一步推迟解码

每一步都是异步的。你看到的「瞬间加载完的页面」,背后是一连串相互等待、协调的异步操作。

下一篇我们讲如何从零搭一个真实的开发环境:VS Code 装什么插件、Node.js 怎么配、Git 怎么初始化。这些不是理论,是每天写代码的第一步。

← DOM 操作:JavaScript 如何控制页面 → Apple 频道 从零搭建开发环境:VS Code、Node.js、Git →
🍎 Apple 深度分析
本文基于 Apple 公开资料及行业分析撰写。观点仅供参考与学习交流。