异步编程:Fetch API 与数据请求
你在浏览器打开一个网页,看到文章列表、最新评论、天气信息——这些数据不可能是 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() 返回的是一个 Promise。cached() 函数不管缓存命中与否,返回值始终是 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 引擎可以处理点击事件、渲染动画、执行其他脚本。等网络请求回来后,引擎再回到暂停的地方继续执行。
你每天在用异步
白物集首页的加载过程完整走了一遍异步链:
- 浏览器请求
baiwuji.top - Astro 服务端执行
await getArticles()→ 用fetch()请求后端 API - API 返回 JSON 数据
- Astro 用数据渲染 HTML 页面
- 页面发送到浏览器
- 浏览器解析 HTML 时,遇到
<img>标签,异步发起图片请求 - 图片加载完后触发
img.decoding = 'async'(第 222 行),进一步推迟解码
每一步都是异步的。你看到的「瞬间加载完的页面」,背后是一连串相互等待、协调的异步操作。
下一篇我们讲如何从零搭一个真实的开发环境:VS Code 装什么插件、Node.js 怎么配、Git 怎么初始化。这些不是理论,是每天写代码的第一步。