DrillLab
第 06 / 08 节LESSON 06 / 08约 15 分钟~15 min

历史与确认页:两个小而致命的细节The history and confirmation pages: two small details that decide pass or fail

slice(-3).reverse() 一个字符都不能错;bookedCabDetails?.name 少个问号就白屏。slice(-3).reverse() has to be exact, character for character; drop the question mark in bookedCabDetails?.name and the screen goes blank.

2 个练习2 exercisesCab Booking · 第 2 部分Cab Booking · Part 2
这一页有什么On this page6
学完这节你会After this lesson you can
  • 说清 slice(-3) 和 slice(0, 3) 的区别Explain the difference between slice(-3) and slice(0, 3)
  • 知道 reverse() 是原地修改,以及为什么这里安全Know that reverse() changes the array in place, and why it is safe here
  • 写出「最新三条、最新在最上」的取法Write the code for the three newest rides with the newest at the top
  • 说清为什么 bookedCabDetails 后面必须有可选链Explain why bookedCabDetails needs optional chaining after it
这在考试里考什么What the exam does with this

测试 4 是这道题唯一会「看起来做对了但实际全错」的地方:它同时查数量、顺序、和最旧那条真的消失。slice 方向写反、忘了 reverse、或者直接 reverse 到 state 上,三种错法都只在这一条测试里暴露。Test 4 is the one place in this task where your work can look correct and be completely wrong. It checks the count, the order, and that the oldest entry really disappeared, all at the same time. A slice in the wrong direction, a missing reverse, or a reverse applied to the state array: all three show up only in this single test.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
cab-booking-context/src/components/Home/RideHistory.jsx取最新三条并反转

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JSXRideHistory.jsx源项目From source
1import { useCabContext } from "../../context/CabContext";
2
3const RideHistory = () => {
4 const { rideHistory } = useCabContext();
5 const latestRides = rideHistory.slice(-3).reverse();
6
7 return (
8 <section className="history-container" aria-labelledby="ride-history-title">
9 <h3 id="ride-history-title">Ride History</h3>
10
11 {latestRides.length > 0 ? (
12 <ul className="history-list">
13 {latestRides.map((ride, index) => (
14 <li key={`${ride.id}-${index}`} data-testid="history-cabs">
15 <span>{ride.name}</span>
16 <strong>${ride.price}</strong>
17 </li>
18 ))}
19 </ul>
20 ) : (
21 <p data-testid="no-ride-title" className="empty-state">
22 No ride history yet.
23 </p>
24 )}
25 </section>
26 );
27};
28
29export default RideHistory;
Source: cab-booking-context/src/components/Home/RideHistory.jsx
cab-booking-context/src/components/CabConfirmation/CabConfirmation.jsx可选链在这里

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JSXCabConfirmation.jsx源项目From source
1import { useCabContext } from "../../context/CabContext";
2
3const CabConfirmation = ({ onConfirm }) => {
4 const { bookedCabDetails } = useCabContext();
5
6 return (
7 <main className="confirm-container">
8 <div className="success-icon" aria-hidden="true">
9
10 </div>
11 <h2>Cab Booked Successfully!</h2>
12 <p data-testid="confirm-message">
13 {bookedCabDetails?.name} is on the way and will arrive shortly.
14 </p>
15 <button
16 type="button"
17 data-testid="confirm-button"
18 className="primary-button"
19 onClick={onConfirm}
20 >
21 Okay
22 </button>
23 </main>
24 );
25};
26
27export default CabConfirmation;
Source: cab-booking-context/src/components/CabConfirmation/CabConfirmation.jsx
§01

slice(-3) 是「最后三个」slice(-3) means the last three

负数从尾巴数起。方向写反,测试 4 直接红A negative number counts from the end. Get the direction wrong and test 4 fails

一句话:arr.slice(-3)最后三个,arr.slice(0, 3)最前三个。

先看数据是怎么排的:updateBookedCabDetails 用的是[...rideHistory, details] ——新记录追加在尾部。 所以数组是「最旧 → 最新」的顺序。

写法拿到什么测试 4 结果
slice(-3)Honda Accord / Toyota Highlander / Ford Explorer (第 2、3、4 次)✓ 正确
slice(0, 3)Ford Fusion / Honda Accord / Toyota Highlander (第 1、2、3 次)✕ 最后一行断言直接炸 ——Ford Fusion 还在 DOM 里
slice(3)从第 4 个开始到结尾(只剩 1 条)toHaveLength(3) 收到 1

注意 slice(0, 3) 有多阴:toHaveLength(3) 会过, 因为它确实是 3 条。只有最后那句queryByText(/Ford Fusion/) 抓得住它。
这就是测试 4 为什么要写那一句 ——光查数量不够,得查「该消失的真的消失了」。

会追问:「数组不足三条时 slice(-3) 会怎样?」——安全。只有 1 条时返回那 1 条, 空数组时返回空数组,不会报错也不会补 undefined。 所以测试 1(空历史)和测试 3(1 条)都不用特殊处理。
这一点值得记住:slice 越界不抛错, 这也是它比手写下标循环安全的地方。

In one line: arr.slice(-3) takes the last three; arr.slice(0, 3) takes the first three.

Start from how the data is ordered: updateBookedCabDetails uses [...rideHistory, details], so new records are appended at the tail. The array runs oldest to newest.

Written asWhat you getTest 4
slice(-3)Honda Accord / Toyota Highlander / Ford Explorer (bookings 2, 3, 4)✓ correct
slice(0, 3)Ford Fusion / Honda Accord / Toyota Highlander (bookings 1, 2, 3)✕ the last assertion fails — Ford Fusion is still in the DOM
slice(3)From index 3 to the end (only 1 entry)toHaveLength(3) receives 1

Notice how sneaky slice(0, 3) is: toHaveLength(3) passes, because there really are three rows. Only that final queryByText(/Ford Fusion/) line catches it.
Which is precisely why test 4 has that line — counting is not enough; you have to check that what should be gone is gone.

Follow-up: “What does slice(-3) do with fewer than three entries?” — it is safe. With one entry it returns that one; with an empty array it returns an empty array; no error and no undefined padding. So test 1 (empty) and test 3 (one entry) need no special handling.
Worth remembering: slice never throws on out-of-range, which is one reason it beats a hand-written index loop.

JavaScript四条记录走一遍(示意)Four records, step by step (illustration)示意Illustrative
1const history = ["Ford Fusion", "Honda Accord", "Toyota Highlander", "Ford Explorer"];
2// ↑ 最旧(第 1 次) 最新(第 4 次)↑
3
4history.slice(-3);
5// ["Honda Accord", "Toyota Highlander", "Ford Explorer"] ← 最新三条 ✓
6
7history.slice(0, 3);
8// ["Ford Fusion", "Honda Accord", "Toyota Highlander"] ← 最旧三条 ✕
9
10history.slice(-3).reverse();
11// ["Ford Explorer", "Toyota Highlander", "Honda Accord"] ← 最新在最上 ✓
12// 这正是测试 4 断言的顺序
13
14// slice 越界是安全的:
15["A"].slice(-3); // ["A"]
16[].slice(-3); // []
1const history = ["Ford Fusion", "Honda Accord", "Toyota Highlander", "Ford Explorer"];
2// ↑ oldest (booking 1) newest (booking 4) ↑
3
4history.slice(-3);
5// ["Honda Accord", "Toyota Highlander", "Ford Explorer"] ← the three newest ✓
6
7history.slice(0, 3);
8// ["Ford Fusion", "Honda Accord", "Toyota Highlander"] ← the three oldest ✕
9
10history.slice(-3).reverse();
11// ["Ford Explorer", "Toyota Highlander", "Honda Accord"] ← newest at the top ✓
12// this is exactly the order test 4 asserts
13
14// slice is safe out of range:
15["A"].slice(-3); // ["A"]
16[].slice(-3); // []
§02

reverse() 原地修改 —— 这里为什么安全reverse() changes the array in place, and why that is safe here

因为 slice 已经给了你一个新数组Because slice has already given you a new array

一句话:reverse()改掉它作用的那个数组本身, 但 slice() 返回的是新数组, 所以 slice(-3).reverse() 改的是那个副本,碰不到 state

直接写 rideHistory.reverse() 会怎样:

  • state 数组被就地翻转了 —— 现在它是「最新 → 最旧」;
  • React 不知道(引用没变,连重渲染都不会触发);
  • 下次 [...rideHistory, details]把新记录追加在已经翻转过的数组尾部 —— 顺序彻底乱了;
  • 而且每次渲染都翻一次, 顺序在两种排列之间来回跳。

这个 bug 的表现极不稳定, 因为它取决于渲染了几次 ——StrictMode 开发模式下渲染两次,翻两次等于没翻,开发时看着正常,生产环境反而是错的

JS 里哪些数组方法原地改:

原地修改(改原数组)返回新数组(安全)
push / pop / shift / unshiftslice / concat / map / filter
splice / sort / reverse / fill[...arr] / toSorted / toReversed

三个最容易忘的是sort / reverse / splice —— 它们看起来像「计算」,实际是「修改」。
会追问:「有没有不改原数组的版本?」—— 有,toSorted() / toReversed() / toSpliced(),ES2023 加的, Node 20+ 和现代浏览器都支持。但这道题的 slice(-3).reverse() 已经安全了, 没必要换 —— 知道有这么个东西就行。

In one line: reverse() mutates the array it is called on, but slice() hands back a new array — so slice(-3).reverse() reverses the copy and never touches state.

What a bare rideHistory.reverse() would do:

  • The state array is flipped in place — it now runs newest to oldest;
  • React has no idea (the reference did not change, so not even a re-render is triggered);
  • The next [...rideHistory, details] appends to an already-reversed array, and the ordering is now meaningless;
  • And it flips again on every render, so the order oscillates between two arrangements.

The bug is wildly unstable because it depends on the render count — StrictMode renders twice in development, two flips cancel out, so it looks right while you develop and is wrong in production.

Which array methods mutate:

Mutating (changes the original)Returns a new array (safe)
push / pop / shift / unshiftslice / concat / map / filter
splice / sort / reverse / fill[...arr] / toSorted / toReversed

The three people forget are sort / reverse / splice — they read like computations and are really mutations.
Follow-up: “Are there non-mutating versions?” — yes: toSorted() / toReversed() / toSpliced(), added in ES2023 and available in Node 20+ and current browsers. But slice(-3).reverse() here is already safe, so there is no need to switch — just know they exist.

JSXsrc/components/Home/RideHistory.jsx源项目From source
1import { useCabContext } from "../../context/CabContext";
2
3const RideHistory = () => {
4 const { rideHistory } = useCabContext();
5 const latestRides = rideHistory.slice(-3).reverse();
6
7 return (
8 <section className="history-container" aria-labelledby="ride-history-title">
9 <h3 id="ride-history-title">Ride History</h3>
10
11 {latestRides.length > 0 ? (
12 <ul className="history-list">
13 {latestRides.map((ride, index) => (
14 <li key={`${ride.id}-${index}`} data-testid="history-cabs">
15 <span>{ride.name}</span>
16 <strong>${ride.price}</strong>
17 </li>
18 ))}
19 </ul>
20 ) : (
21 <p data-testid="no-ride-title" className="empty-state">
22 No ride history yet.
23 </p>
24 )}
25 </section>
26 );
27};
28
29export default RideHistory;
Source: cab-booking-context/src/components/Home/RideHistory.jsx
JavaScript四种写法对比(示意)Four versions side by side (illustration)示意Illustrative
1// ✓ 安全:slice 先造了新数组,reverse 改的是那个副本
2const latestRides = rideHistory.slice(-3).reverse();
3
4// ✕ 危险:直接翻转 state 本身
5const latestRides = rideHistory.reverse();
6// ↑ 改的是 state 数组
7// 引用没变 → React 不重渲染
8// 下次 append 接在翻转后的尾部 → 顺序全乱
9// 每次渲染翻一次 → StrictMode 下翻两次,开发时看不出来
10
11// ✓ 另一种安全写法(ES2023)
12const latestRides = rideHistory.slice(-3).toReversed();
13
14// ✓ 或者先复制再翻
15const latestRides = [...rideHistory].reverse().slice(0, 3);
16// 注意这个顺序也对,但多复制了整个数组
1// ✓ safe: slice built a new array first, so reverse changes that copy
2const latestRides = rideHistory.slice(-3).reverse();
3
4// ✕ dangerous: reversing the state itself
5const latestRides = rideHistory.reverse();
6// ↑ this changes the state array
7// the reference did not change → React does not re-render
8// the next append lands after the reversed items → the order falls apart
9// it reverses once per render → twice under StrictMode, invisible in development
10
11// ✓ another safe form (ES2023)
12const latestRides = rideHistory.slice(-3).toReversed();
13
14// ✓ or copy first, then reverse
15const latestRides = [...rideHistory].reverse().slice(0, 3);
16// this order is correct too, but it copies the whole array
§03

bookedCabDetails?.name —— 那个问号不能省bookedCabDetails?.name: you cannot drop that question mark

初始值是 null,而 null 上取属性会抛错The initial value is null, and reading a property of null throws an error

一句话:bookedCabDetails 初始是 nullnull.name 会抛TypeError,所以要写 ?.name

什么时候真的是 null:

  • App 刚启动、用户还没选过任何车 —— 这时 CabConfirmation 没被渲染,所以撞不到;
  • 但只要有人把 CabConfirmation单独 render 出来测试(比如写一个组件级单测), 就会立刻炸;
  • 或者以后加一个「查看上次行程」的入口, 在没有行程时点进去 ——白屏 + 控制台一行Cannot read properties of null (reading 'name')

可选链 ?. 做什么:左边是 nullundefined整个表达式短路成 undefined, 不抛错。 而 React 遇到 {undefined}什么也不渲染(不是渲染字符串 “undefined”), 所以页面只是少一段文字,不会崩。

注意它只挡 null / undefined ——bookedCabDetails{}?.name 照样是 undefined, 但那是属性不存在,不是崩溃。?. 防的是「取属性的动作本身炸掉」。

会追问:「那是不是所有地方都该加 ?.?」——不是。到处加 ?. 会把「这里可能没有值」这个信息抹平成噪音, 真正可能为空的地方反而看不出来。
规则是:只在「这个值确实可能不存在」的地方加。这道题里 bookedCabDetails 初始 null, 该加;ride.name 是遍历出来的记录,能进数组就一定有 name,不该加

In one line: bookedCabDetails starts as null, and null.name throws a TypeError, so you write ?.name.

When it really is null:

  • Right after the app starts, before the user has picked anything — but CabConfirmation is not rendered then, so nothing breaks;
  • The moment somebody renders CabConfirmation on its own — say in a component-level unit test — it throws immediately;
  • Or add a “view your last ride” entry point later and open it with no rides: blank screen and one console line reading Cannot read properties of null (reading 'name').

What optional chaining does: when the left side is null or undefined, the whole expression short-circuits to undefined instead of throwing. And React renders nothing at all for {undefined} (not the literal string “undefined”), so the page is just missing a phrase rather than crashing.

It only guards null and undefined — with bookedCabDetails as {}, ?.name is still undefined, but that is a missing property, not a crash. ?. protects against the property access itself blowing up.

Follow-up: “Should I add ?. everywhere then?” — no. Sprinkling it everywhere flattens the signal “this might be absent” into noise, and the places that genuinely can be empty stop standing out.
The rule: add it only where the value really can be missing. Here bookedCabDetails starts as null, so it earns one; ride.name comes from iterating records that always have a name to be in the array at all, so it does not.

JSXsrc/components/CabConfirmation/CabConfirmation.jsx源项目From source
1import { useCabContext } from "../../context/CabContext";
2
3const CabConfirmation = ({ onConfirm }) => {
4 const { bookedCabDetails } = useCabContext();
5
6 return (
7 <main className="confirm-container">
8 <div className="success-icon" aria-hidden="true">
9
10 </div>
11 <h2>Cab Booked Successfully!</h2>
12 <p data-testid="confirm-message">
13 {bookedCabDetails?.name} is on the way and will arrive shortly.
14 </p>
15 <button
16 type="button"
17 data-testid="confirm-button"
18 className="primary-button"
19 onClick={onConfirm}
20 >
21 Okay
22 </button>
23 </main>
24 );
25};
26
27export default CabConfirmation;
Source: cab-booking-context/src/components/CabConfirmation/CabConfirmation.jsx
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L1认出来Spot it哪些写法能让测试 4 全绿?(多选)Which versions make test 4 pass? (more than one)
历史是 [Fusion, Accord, Highlander, Explorer](最旧 → 最新)。 断言要求:3 条、顺序Explorer / Highlander / AccordFusion 不在 DOM 里。The history is [Fusion, Accord, Highlander, Explorer] (oldest → newest). The assertions want: 3 entries, in the order Explorer / Highlander / Accord, and Fusion not in the DOM.

这题是多选。More than one answer is correct.

先选一个选项Pick an option first
L3写整块Write a block从零写出 RideHistoryWrite RideHistory from an empty file
从 Context 读历史,取最新三条、最新在最上, 空的时候显示空状态。检查器会挡住原地修改。Read the history out of the Context, take the three newest with the newest at the top, and show the empty state when there is nothing. The checker blocks changes made in place.
要求Requirements
  • 从 useCabContext() 里读 rideHistoryRead rideHistory out of useCabContext()
  • 取最新三条并让最新的排在最上面(slice(-3).reverse() 或等价写法)Take the three newest and put the newest at the top (slice(-3).reverse() or an equivalent)
  • 不许在 rideHistory 上直接调 reverse / sort —— 那会原地改 stateNever call reverse or sort on rideHistory directly — that changes the state in place
  • 每条记录一个 <li data-testid="history-cabs">,里面有车名和 $价格One <li data-testid="history-cabs"> per record, holding the cab name and the $price
  • 空历史显示 <p data-testid="no-ride-title">No ride history yet.</p>,且此时不渲染列表An empty history shows <p data-testid="no-ride-title">No ride history yet.</p> and renders no list
  • key 不能只用 ride.id —— 同一辆车可以被订两次The key cannot be ride.id alone — the same cab can be booked twice
JSXsrc/components/Home/RideHistory.jsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

错例Wrong

初学者常见的几种写法错误Mistakes beginners actually make

下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.

JSX原地翻转 stateReversing the state in place示意Illustrative
1// ✕ 直接在 state 上 reverse
2const RideHistory = () => {
3 const { rideHistory } = useCabContext();
4 const latestRides = rideHistory.reverse().slice(0, 3);
5
6};
1// ✕ calling reverse on the state itself
2const RideHistory = () => {
3 const { rideHistory } = useCabContext();
4 const latestRides = rideHistory.reverse().slice(0, 3);
5
6};
这次的结果是对的,下次就不对了。reverse() 把 state 数组就地翻成「最新 → 最旧」, React 不知道(引用没变)。
然后下一次 [...rideHistory, details]把新记录接在已经翻转过的数组尾部 —— 也就是接在「最旧的那一头」,顺序从此彻底乱。
更麻烦的是每次渲染都翻一次StrictMode 下开发模式渲染两次, 翻两次抵消,你在开发时看不出任何异常把「读」和「改」分清楚 —— 渲染函数里只许读。
The result is right this time and wrong the next time. reverse() turns the state array itself into newest to oldest, and React does not notice, because the reference did not change.
The next [...rideHistory, details] then adds the new entry to the end of an array that is already reversed, which is the oldest end, and from that point the order is broken.
Worse, it reverses once on every render. Under StrictMode development renders twice, the two reversals cancel each other out, and you see nothing wrong while developing. Keep reading and changing apart: a render function may only read.
JSX少一个问号One question mark missing示意Illustrative
1// ✕ 忘了可选链
2<p data-testid="confirm-message">
3 {bookedCabDetails.name} is on the way and will arrive shortly.
4</p>
5
6// 单独 render CabConfirmation 时(或任何 bookedCabDetails 还是 null 的时刻):
7// TypeError: Cannot read properties of null (reading 'name')
8// → 整个组件树白屏,因为没有 error boundary
1// ✕ the optional chain was left out
2<p data-testid="confirm-message">
3 {bookedCabDetails.name} is on the way and will arrive shortly.
4</p>
5
6// rendering CabConfirmation on its own (or any moment when bookedCabDetails is null):
7// TypeError: Cannot read properties of null (reading 'name')
8// → the whole component tree goes blank, because there is no error boundary
useState(null) 的初始值就是 null在完整流程里撞不到(走到确认页时一定已经选过车), 所以四个测试都过 ——又一次「测试通过 ≠ 做对了」
但只要有人给 CabConfirmation 写一个组件级单测, 或者以后加一个「查看上次行程」的入口,它就是白屏。 React 没有默认的 error boundary, 渲染时抛错会让整棵树卸载
初始值是 null 的 state,读它的属性就该配 ?.
The initial value of useState(null) is null. The complete flow never reaches it, because by the time you are on the confirmation page a cab has been chosen, so all four tests pass. This is another case where passing tests does not mean correct code.
But as soon as somebody writes a component test for CabConfirmation, or a “view last ride” entry point is added later, the screen is blank. React has no default error boundary, and an error thrown during render removes the whole tree.
When a state starts as null, reading its properties needs ?..
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

要「最新 N 条」You need the newest N entries
slice(-N);越界安全,不足 N 条也不报错slice(-N); it is safe past the end and does not fail with fewer than N items
要倒序显示You need them shown in reverse order
先 slice 出副本再 reverse,或用 toReversed()slice a copy first and then reverse, or use toReversed()
看到 sort / reverse / splice 作用在 state 上You see sort, reverse or splice used on state
立刻停 —— 它们原地改,先复制Stop there: they change the array in place, so copy it first
某个 state 初始值是 nullA piece of state starts out as null
读它的属性配 ?.;只在真会为空的地方加Read its properties with ?.; add it only where the value really can be empty
测试只断言了「有几个」The test only checks how many items there are
补一条「该消失的真的消失了」—— 数量对内容错抓不住Add a check that what should be gone is gone; a right count with wrong content slips through
这节的要点What to take away
  1. slice(-3) 是最后三条,slice(0, 3) 是最前三条 —— 方向写反测试 4 才抓得住。slice(-3) is the last three, slice(0, 3) is the first three. Only test 4 catches the wrong direction.
  2. reverse() 原地修改;slice(-3).reverse() 安全是因为 slice 先给了新数组。reverse() changes the array in place; slice(-3).reverse() is safe because slice returns a new array first.
  3. 直接 rideHistory.reverse() 会翻掉 state,且 StrictMode 下开发时看不出来。Calling rideHistory.reverse() reverses the state itself, and under StrictMode you cannot see it while developing.
  4. sort / reverse / splice / push 都是原地改;slice / map / filter / concat 返回新数组。sort, reverse, splice and push all change the array in place; slice, map, filter and concat return a new array.
  5. bookedCabDetails 初始 null,所以 ?.name 那个问号不能省。bookedCabDetails starts as null, so the question mark in ?.name cannot be dropped.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson完整答案跑不起来 —— 一个扩展名的事The complete answer does not run — the cause is one file extension
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Loading:一秒之后自己跳走Loading: it moves to the next page by itself after one second