三个实战项目 — 天气应用、串并行对比、重试器¶
这一章是全书模式的装配车间: 三个项目,分别把「取数+错误处理」「串并行编排」「重试机制」装成能跑的完整应用。 读完你拿到三个项目的完整结构与关键实现,更重要的是每个项目里"哪部分该独立成函数"的判断过程。
1. 这一章讲什么¶
前九章是零件与模式,本章是整机。三个项目难度递进:
- 天气应用(第 3 节):单请求、单界面——练习取数与显示分离;
- 电影库(第 4 节):同一批请求跑两遍,串行一遍、并行一遍——练习编排,还练习测量;
- 重试器(第 5 节):会失败的接口+退避重试——练习失败路径的完整设计。
每个项目都按同一条流水线走:搭 HTML/CSS 骨架 → 写 JS 实现 → 说明怎么测、会看到什么。
2. 顶层全景:三个项目各练什么¶
项目一(天气) 项目二(电影库) 项目三(重试器)
───────── ───────────── ─────────────
输入→取数→显示 输入→[串行|并行]→显示 点按钮→可能失败→退避重试→结果
练:错误分支 练:编排+计时 练:失败路径设计
新结构:无 新结构:console.time 新结构:递归
3. 项目一:天气应用¶
3.1 骨架¶
界面三件套:输入框(城市名)、按钮、结果显示区;一个 app.js 文件承担全部逻辑。取数指向 OpenWeatherMap 的真实接口(需要免费申请一个 API key 填进代码)1。
3.2 核心实现:三件事分成三个函数¶
原书的实现把界面程序拆成职责分明的三块2:
async function getWeather(city) {
const response = await fetch(`${BASE_URL}?q=${city}&appid=${API_KEY}&units=metric`);
if (!response.ok) { throw new Error('City not found'); }
return await response.json();
}
function displayWeather(data) {
const { name, main, weather } = data;
weatherInfo.innerHTML = `
<h2>${name}</h2>
<p>Temperature: ${main.temp}°C</p>
<p>Feels like: ${main.feels_like}°C</p>
<p>Humidity: ${main.humidity}%</p>
<p>Weather: ${weather[0].description}</p>`;
}
getWeatherButton.addEventListener('click', async () => {
const city = cityInput.value.trim();
if (!city) { weatherInfo.textContent = 'Please enter a city name'; return; }
try {
weatherInfo.textContent = 'Loading...';
const weatherData = await getWeather(city);
displayWeather(weatherData);
} catch (error) {
weatherInfo.textContent = `Error: ${error.message}`;
}
});
三块各自的职责,一条线说清:
事件处理(按钮) getWeather(取数) displayWeather(显示)
───────────── ────────────── ─────────────────
读输入、空值检查 发请求、判 ok、抛错 把数据拼成 HTML
显示"Loading..." 不关心数据哪来的
接住错误,写错误文案 不关心往哪显示
这个三分法就是第 11 章"分离关注点"的实战出处:界面层管时序与状态,取数层管网络与错误,显示层管渲染。 错误分支也齐全:空输入、请求失败(404 → 'City not found')、加载中,三种状态都有对应界面。
3.3 测试路径¶
填入 API key → 打开页面 → 输城市名点按钮 → 看四种情形:正常显示气温/体感/湿度/天气描述;空输入给提醒;不存在的城市显示 Error;请求期间显示 Loading 动画3。
3.3a 骨架里的教学细节¶
三个项目的 HTML 骨架都小得可怜(一个容器、一两个控件、一个结果区),但有两个细节是刻意设计的15:
其一,结果区一开始是空 div,内容由 JavaScript 填充。 天气应用的 weatherInfo、电影库的 movieList、重试器的 result,全是"占位的空盒子"。这对应一个界面原则:HTML 管结构,JavaScript 管状态——刷新页面看到的一切动态内容,都应该能在 app.js 里找到对应的赋值语句。如果 HTML 里预先写死了部分内容,状态就漏了一格,测试清单(3.4)上就永远有一格查不到。
其二,脚本用普通 <script src> 而不是模块。 这是入门书的最小阻力选择——不用配构建工具,双击 HTML 就能跑。代价是没有模块化隔离,app.js 里的一切都是全局可见的。三个项目各自只有一个脚本文件,所以这个代价为零;项目一大,就要换模块或构建工具,那是另一本书的话题。
CSS 的定位也一样:三个项目的样式都是"能看就行"的最小集(居中、卡片阴影、按钮变色),没有任何动画与响应式。这不是疏忽——本书的力气全花在 app.js 上,界面刻意保持"朴素到不会分散精力"。
3.3b 一次点击的完整旅程¶
把天气应用从点击到渲染的每一步串起来,标出各层的位置(耗时为演示编)16:
t=0.0 用户点按钮 → 事件层被唤醒:trim 读输入
├─ 空?→ 显示提醒,旅程结束(不碰网络)
└─ 不空 → 显示 "Loading...",旅程继续
t=0.1 事件层调 getWeather(city) → 进入取数层
t=0.1 fetch 发出;response.ok 检查;数据对象成形
t=1.1 数据回到事件层 → 调 displayWeather(data) → 进入显示层
t=1.1 innerHTML 拼接,四项数据显示,旅程结束
── 失败分支:t=0.1~ 请求挂掉 → 错误抛回事件层 catch → 显示 Error
这张旅程图里藏着一条层的纪律:取数层抛出的错误,穿过调用它的 await,落在事件层的 catch 里——取数层自己不会 try/catch(它只负责"把失败变成错误递出去"),事件层负责"失败时界面怎么办"。两层各司其职,各自简单;混在一起,就是一个又长又难测的函数。三个项目的取数函数(getWeather/fetchMovie/simulateAPICall 的消费端)全部遵守这条纪律。
3.4 天气应用的四种界面状态¶
3.2 的三分法之外,这个项目还有一个值得单独说的设计:界面在任意时刻必居四种状态之一,代码里各有一条路径对应10:
| 状态 | 触发 | 界面显示 | 代码位置 |
|---|---|---|---|
| 空闲 | 刚打开 | 输入框+按钮,结果区空白 | (初始) |
| 加载中 | 点了按钮,请求在途 | "Loading..." | try 块第一行 |
| 成功 | 数据到了 | 城市名+四项数据 | displayWeather |
| 失败 | 请求挂了 | "Error: City not found" | catch 块 |
| 输入为空 | 点了按钮但输入框空 | "Please enter a city name" | 事件处理开头的 if |
第五种状态(空输入)常被漏掉——它在任何网络交互之前,是纯界面逻辑,所以它归事件处理层管而不是 catch 管。这个"状态清单"是任何取数界面的通用骨架,写自己的应用时先列这五格,再往里填代码,比写完全局再补错误分支要稳得多。
3.5 从天气应用到任意取数界面¶
把天气应用抽象一层,它其实是"一个输入、一次取数、一个展示"的最小模板,换个 API 就是新应用:
输入控件(文本框/下拉/按钮) ← HTML
事件处理(读输入 → 校验空值 → Loading → try 取数 → display/Error) ← app.js 的事件层
取数函数(一个 URL 一个函数,内部只管 fetch+ok+json+throw) ← app.js 的取数层
展示函数(收数据对象,拼 HTML) ← app.js 的显示层
第 12 章会看到这个模板在 React/Vue 里的变形(取数层装进自定义 hook,状态不再散落在 DOM 里而是收进状态);在纯 JavaScript 里,这个四格模板就是"能维护"的下限。项目一的全部教学价值,就在于它足够小,小到你能在一次阅读里看清四格的边界。
4. 项目二:电影库——同一组请求,两种编排,实测量差¶
4.1 设计¶
界面两个按钮("串行取" / "并行取")+ 电影列表区。取数对象是 TMDB 的真实电影接口,一次取 5 部指定 ID 的电影4。
4.2 两条取数路径¶
原书用同一批 5 个 ID,写了两个函数,差异只在编排5:
async function fetchMoviesSequentially() {
console.time('Sequential');
for (const id of movieIds) { // 一个回来,才发下一个
try {
const movie = await fetchMovie(id);
displayMovie(movie);
} catch (error) { console.error(`Error fetching movie ${id}:`, error); }
}
console.timeEnd('Sequential');
}
async function fetchMoviesInParallel() {
console.time('Parallel');
const moviePromises = movieIds.map(id => fetchMovie(id)); // 五个同时上路
try {
const movies = await Promise.all(moviePromises);
movies.forEach(displayMovie);
} catch (error) { console.error('Error fetching movies:', error); }
console.timeEnd('Parallel');
}
4.3 用 console.time 量出差距¶
console.time('标签') 与 console.timeEnd('标签') 是一对计时记号:同标签的头尾之间,浏览器计好耗时打到控制台。两个按钮各点一次,控制台两行输出(数字是为演示编的):
约 5 倍差距——正是并行度(5)。这个数字比任何说教都管用:它把"独立请求该并行"从一条规则变成一次亲眼所见。 顺带注意两个函数的错误处理差异:串行版每部单独 try/catch,一部失败不影响下一部;并行版一个 catch 全包(第 04 章 all 的短路语义——一部失败,Promise.all 整体拒)6。
判断(我们的,不是书里的): 这个项目最值钱的不是"并行快"(那是第 06 章的已知结论),而是它示范了用一对计时函数把架构决策变成可测量的数字。优化之争(该不该并行、要不要缓存)在无数团队里停在口水战,而这个项目给了止战工具:各写一版,各量一次。把这套做法推广到任何"我觉得更快"的争论上,比记住 5 倍这个数字有用得多。 如果错,会错在: console.time 测的是全程墙钟时间(从发出到收齐),受网络抖动影响大;严谨对比要跑多次取中位数(书里跑一次就下结论)。这是演示,不是规范的性能对比——别拿单次结果去做架构决策。
4.4 串行版的逐圈走查¶
4.2 的两版并排之外,把串行版单独走一遍,看清它每圈在干什么(各请求 1 秒,为演示编)11:
第 1 圈:for 取 id=550 → await fetchMovie(550) → 1 秒后显示 → catch 兜这一部的错
第 2 圈:for 取 id=551 → await ……
…5 圈共约 5 秒,电影一张一张出现(先来先显示)
"一张一张出现"是串行的外观特点,也是它唯一的体验优点;并行的外观是"转 1 秒圈,五张同时出现"。两种体验对应两种产品预期:一页一页加载、瀑布流适合串行(用户想先看先来的);报表、对照页适合并行(用户要一次看全)。项目二把两个按钮并排放在页面上,等于把这个产品决策点显式交给了点按钮的人——教学上聪明,产品上要自己定。
4.5 计时输出的读法¶
console.time 的输出形如 Sequential: 5137.000ms12。读这两个数字时的三个提醒:
- 看数量级,不看个位数——网络抖动能让单次结果差出几百毫秒,5137 与 4900 没有区别,5137 与 1100 才是结构性的差距(约等于并行度);
- 顺序标签必须成对——
console.time('Sequential')与console.timeEnd('Sequential')的标签字符串要一字不差,打错字母的话 timeEnd 找不到起点,什么都不打; - 嵌套计时各记各的——并行版内部还可以给单个请求再嵌套计时(标签不同即可),这把"总耗时"与"最慢一环"分开,是定位并行收益被谁吃掉的第一手数据。
4.6 模拟接口的"诚实"设计¶
项目三的第一步是造一个会失败的模拟接口,这个设计本身有教学讲究14:
async function simulateAPICall() {
const randomDelay = Math.floor(Math.random() * 1000) + 500; // 500-1500ms
await new Promise(resolve => setTimeout(resolve, randomDelay));
if (Math.random() < 0.5) throw new Error('API call failed');
return { message: 'API call succeeded', timestamp: new Date().toISOString() };
}
三个设计决策。其一,延迟也随机:真实网络不是固定 1 秒,把延迟做成 500-1500ms 的随机,重试的时间线才不会呈现出"假规律"(如果每次恰好 1 秒,指数退避的等待表格会整齐得像作弊)。其二,失败率定在一半:太高(90%)会让"成功"像奇迹,测试时几乎看不到成功路径;太低(10%)点十次都碰不到一次重试。50% 是让"成功、重试后成功、全败"三条路径在一次手动测试里都能撞见的甜蜜点。其三,成功也带时间戳:结果里带 new Date().toISOString(),每次成功都不一样,界面上一眼能区分"这是新结果"还是"上一次的残留"。
这个"诚实的模拟"原则适用于一切异步测试:模拟值(模拟的 1 秒)不等于真实值(网络的随机长尾),但模拟的分布要按真实的样子造(有快有慢、有败有成),否则测试通过的那一刻就是错觉开始的时候(第 00 章 §5 的判断块说的就是这个)。
5. 项目三:重试器——失败路径的完整设计¶
5.1 设计¶
一个会故意失败的模拟接口:随机等 0.5-1.5 秒,然后抛硬币(一半机会)决定成功或失败。按钮一点,重试机制上场,界面显示每次重试的进展7。
5.2 核心实现:递归版指数退避¶
async function simulateAPICall() {
const randomDelay = Math.floor(Math.random() * 1000) + 500; // 500-1500ms
await new Promise(resolve => setTimeout(resolve, randomDelay));
if (Math.random() < 0.5) throw new Error('API call failed');
return { message: 'API call succeeded', timestamp: new Date().toISOString() };
}
async function fetchWithRetry(retries = 3, delay = 1000) {
try {
return await simulateAPICall();
} catch (error) {
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(retries - 1, delay * 2); // 递归:次数减一,延迟翻倍
} else {
throw error;
}
}
}
和第 06 章 4.3 的循环版退避相比,这里是递归版:函数失败后不循环,而是带新参数调自己——retries - 1(剩余次数减一)与 delay * 2(等待翻倍)从一次调用传进下一次。走一遍(各次成败是为演示编的):
第 1 次:失败 → 等 1000ms → 调自己(retries=2, delay=2000)
第 2 次:失败 → 等 2000ms → 调自己(retries=1, delay=4000)
第 3 次:失败 → 等 4000ms → 调自己(retries=0, delay=8000)
第 4 次:失败 → retries=0,不再递归,把错误抛出去
全程 1+2+4=7 秒等待 + 4 次请求,全败才认输
递归版的结构把"还有几次机会、现在等多久"全部压进参数,循环体里没有任何状态变量——这是递归这一写法的标准示范(函数自己调用自己,每次用更小的参数,直到一个"停止条件"(这里是 retries > 0 为假)让递归停住)。
5.3 界面与观察点¶
界面只显示"Fetching..."→ 成功文案(带发生时刻)/ 错误文案;真正的观察点在浏览器控制台:能看到每次 Retrying... (N attempts left) 的日志与逐渐翻倍的实际间隔8。这个项目教的不只是重试,还有失败路径要可观测——日志打在控制台,用户看到的是平静的一句话。
5.4 递归版的边界:次数与延迟两个参数的收敛¶
5.2 的递归有两个参数在收敛:retries 每次 -1(向下到 0 停),delay 每次 ×2(向上翻)。两个方向一个收敛一个发散,这正是递归能安全使用的条件——至少有一个参数在朝停止条件走。如果把 retries - 1 误写成 retries,停止条件永远到不了,递归会变成无限循环(每次翻倍等待,很快就把事件循环堵死——第 08 章错误五的递归版)。写递归时的自检口诀:找到"那个每次变小的东西",找不到就别提交。
顺带一个风格注记:递归版也可以用循环改写(第 13 章 4.3 场景三给了 while 版),两版行为等价。递归版的优点是"每次尝试的全部上下文(还剩几次、现在等多久)都在参数里",一眼能看全;循环版的优点是"没有函数一层层叠上去的增长"(第 09 章边界提过,深递归会吃栈——3 次重试无所谓,300 次就得换循环)。这个项目用递归,是教学选择,不是技术必须。
5.5 三个项目的共同骨架:搭—实现—测¶
回头看,三个项目走的是同一条流水线,这条流水线本身就是可复用的方法13:
① 搭骨架:HTML 摆控件,CSS 摆样式 —— 先让页面"长得像"
② 实现:app.js 按"事件层/取数层/显示层"三格填逻辑
③ 测试:列出状态清单(空闲/加载/成功/失败/空输入),逐格手动验证
三个项目各自把这流水线走了一遍:天气应用练②(三格分工最干净),电影库练③的"测量"变体(测试不止"对不对",还"快不快"),重试器练②的失败路径(成功路径三行就完,90% 的代码在 catch 与递归参数上)。如果只能带走一样,带走这条流水线——它适用于任何"界面 + 异步"的小项目,与框架无关(第 12 章只是给每层换了容器)。
5.6 三个项目的延伸练习¶
书在结尾鼓励"扩展这些项目",这里给四个具体方向,每个都对应一章的知识:
- 天气应用加缓存(第 07 章 4.1):同一城市十分钟内不再请求,直接显示上次结果——练"缓存回退链";
- 电影库加并发上限(第 09 章边界):并行版限制"同时在途最多 2 个"——练信号量式的排队;
- 重试器加分诊(第 06 章 4.5):4xx 失败不重试、直接显示——练"按错误类型决定流程";
- 任意项目加 AbortController(第 09 章 5.3):离开页面/重新点击时取消未完成的请求——练"取消"这个 Promise 的盲区。
做完任意两个,第 06-09 章的知识就从"读过"变成"用过"。
最后交代一句选型:三个项目全部用 async/await 而没出现一条 .then(书的第 10 章代码里 then 只在第 8 章的调试与排障语境中露过面)——这不是书的偏见,是第 05 章结论(默认 async/await,除非要链的精确位置语义)的自然结果;Promise 仍在那里,只是退到了 await 的底下。
5.7 项目三与第 06 章的对照账¶
项目三(递归重试)与第 06 章 4 节(循环重试)是同一个模式的两份实现,把它们的差异记成账,比单独读任何一份都记得牢:
| 第 06 章循环版 | 项目三递归版 | |
|---|---|---|
| 状态放在哪 | 函数体里的局部变量(i) | 参数(retries, delay) |
| 重复的方式 | for 循环转圈 | 函数自己调自己 |
| 翻倍写在哪 | delay * 2 顺手乘 |
调用时 delay * 2 传下去 |
| 栈的增长 | 无 | 每次递归多一帧(3 层无碍) |
| 读起来像 | 流程 | 数学归纳 |
书里两处都教,用意明显:同一个逻辑能被两种语法灵魂各写一遍,读代码时才不会被形式吓住——老库里的递归重试和新代码里的循环重试,从此是熟人。
5.8 从项目到产品:三个被省略的环节¶
从"能跑的项目"到"能上线的产品",中间隔着书里(故意)省略的三个环节:
- 输入校验与转义:天气应用把城市名直接拼进 URL、把接口返回直接 innerHTML——练习无妨,上线前 city 要编码(空格与特殊字符)、返回内容要防注入。书不教,因为教的是异步,不是安全;但心里要有这根弦。
- API key 的存放:
const API_KEY = 'your_api_key_here'写在 app.js 里,等于把钥匙贴在门上——浏览器里的 key 本来就防不住人看,真实产品要么走后端代理,要么用允许来源白名单的 key。 - 错误信息的用户友好度:
Error: City not found是开发者语言;产品上更常见的是"没有找到这个城市,检查一下拼写?"——第 07 章 4.2 默认值的精神延伸到文案:给用户的,要像人话。
这三环不是异步知识,却是把异步代码送出门前的最后一道工序。书把它们留白了,我们用这张清单补上。
6. 作者的判断与证据¶
本章的判断藏在结构里:三个项目一个共同点——每个都先把"取数"从"界面"里拽出来单独成函数。这是作者对"能维护的异步代码"的定义,第 11 章会把它写成明文规则9。
证据形态:三个项目都可运行、可测试,测试说明具体到"你会在控制台看到什么"——可验证性是本章证据的全部。
7. 边界与局限¶
- 天气与电影项目都依赖外部真实 API(要注册 key、受接口改版影响),不可离线复现;
- 电影项目只对比了"全串行 vs 全并行",没演示混合编排(第 06 章 3.2 的三段式);
- 重试器的递归版没有次数硬顶之外的熔断(连续失败的退避上限),也没有区分错误类型(第 06 章 4.5 的分诊没用上);
- HTML/CSS 骨架占了不小篇幅但都是常规写法,无教学增量。
8. 可带走的¶
- 界面程序三分:事件层(读输入/管状态展示/接错误)、取数层(发请求/判 ok/抛错)、显示层(拼 HTML)——各层互不越界;
- 异步代码的界面要覆盖四种状态:正常、加载中、空输入、错误;
- 串行 vs 并行用
console.time/timeEnd量出来再下结论——但这次是演示,严谨对比要多次取中位; - 递归三要素:自己调自己、参数变小(
retries-1, delay*2)、停止条件(retries>0为假就抛); - 失败路径要可观测:控制台留日志,用户界面保持平静;
- 并行 catch 与串行 catch 的语义不同:all 是一拒全拒,逐个好抓——编排方式决定错误粒度。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章三项目 | Chapter 10: Hands-On Mini Projects | text/13-ch10-chapter-10-hands-on-mini-projects.txt:7(搜「three exciting mini-projects」) |
| 天气应用结构 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:9(搜「Weather Application」) · text/13-ch10-chapter-10-hands-on-mini-projects.txt:75(搜「getWeather」) |
| 天气应用测试 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:125(搜「API key」) |
| 电影库:串行 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:235(搜「fetchMoviesSequentially」) |
| 电影库:并行与计时 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:251(搜「fetchMoviesInParallel」) · text/13-ch10-chapter-10-hands-on-mini-projects.txt:276(搜「console.time」) |
| 重试器:模拟接口 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:365(搜「simulateAPICall」) |
| 重试器:递归退避 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:376(搜「fetchWithRetry」) · text/13-ch10-chapter-10-hands-on-mini-projects.txt:384(搜「delay * 2」) |
| 重试器:测试观察点 | 同上 | text/13-ch10-chapter-10-hands-on-mini-projects.txt:421(搜「browser console」) |
-
出处:「Chapter 10: Hands-On Mini Projects」第 13-73 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:28,搜「cityInput」;:69,搜「openweathermap」)。HTML 骨架、BASE_URL 指向 OpenWeatherMap 照原书。 ↩ -
出处:同章第 75-115 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:75,搜「getWeather」;:90,搜「displayWeather」;:101,搜「addEventListener」)。三个函数的分工与错误分支照原书。 ↩ -
出处:同章第 123-129 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:125,搜「API key」)。测试需注册 OpenWeatherMap 免费 key,照原书。 ↩ -
出处:同章第 131-214 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:133,搜「TMDB」;:210,搜「movieIds」)。TMDB 接口与 5 个示例电影 ID 照原书。 ↩ -
出处:同章第 235-268 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:235,搜「fetchMoviesSequentially」;:251,搜「fetchMoviesInParallel」)。串行 for...of 与并行 map+all 两版照原书。 ↩ -
出处:同章第 270-288 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:272,搜「Sequential Fetching」;:274,搜「Parallel Fetching」)。两种编排的用途说明照原书。 ↩ -
出处:同章第 290-374 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:364,搜「50%」)。模拟接口 50% 失败率、随机 500-1500ms 延迟照原书。 ↩ -
出处:同章第 419-429 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:421,搜「browser console」)。测试时控制台可见重试日志与剩余次数照原书。 ↩ -
出处:同章第 117-121 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:121,搜「separation of concerns」)。原文:"This separation of concerns (fetching data vs. displaying data) makes our code more modular and easier to maintain." ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 101-115 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:104,搜「Please enter」;:109,搜「Loading」;:113,搜「Error」)。空输入、加载中、错误三条界面路径照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 235-249 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:235,搜「fetchMoviesSequentially」)。for...of 逐圈取数照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 237-265 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:237,搜「console.time」;:253,搜「console.time」)。两处的计时标记照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 13-129 段与 431-443 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:13,搜「Setting Up」;:433,搜「Conclusion」)。"Setting Up → Implementing → Testing"的三段流程与"Feel free to extend these projects"照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 363-374 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:364,搜「50%」;:366,搜「randomDelay」)。模拟接口的随机延迟、失败率与成功返回值照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 15-73 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:30,搜「weatherInfo」;:32,搜「app.js」)。HTML 骨架的容器-控件-结果区结构与 script 引入方式照原书。 ↩ -
出处:「Chapter 10: Hands-On Mini Projects」第 75-115 段(
text/13-ch10-chapter-10-hands-on-mini-projects.txt:75,搜「getWeather」;:101,搜「addEventListener」)。点击到渲染的调用链与错误分支照原书。 (本章完:三个项目=三个模式,搭—实现—测=一条流水线;把 5.6 的延伸练习做掉两个,本书才算真正读完。) 把延伸练习做掉两个,本书才算真正读完;这也是作者最后的嘱咐。 本章是全书唯一需要动手的一章——书中的代码一字不改地敲一遍,胜过把前十章再读一遍。 动手时的报错与书里不一致,多半是 API key 或接口改版——这正是 5.8 第三环(诚实模拟)存在的理由。 (本章完:搭—实现—测,流水线通吃一切小项目。) ↩