03-travel-list

本文最后更新于 2024年12月20日 下午

旅行清单(Travel-List)

功能和知识点蛮齐全的一个 app 页面

代码及功能解析

文件概览

代码文件概览

页面展示

页面展示

(1)App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useState } from "react";
import Logo from "./Logo";
import Form from "./Form";
import PackingList from "./PackingList";
import Stats from "./Stats";

export default function App() {
const [items, setItems] = useState([]);

function handleAddItems(item) {
setItems((items) => [...items, item]); //不可以items.push(item),会变异,要先散列一个完整的新数组,再把新元素添加进去
}

function handleDeleteItem(id) {
setItems((items) => items.filter((item) => item.id !== id));
}

function handleToggleItem(id) {
setItems((items) =>
items.map((item) =>
item.id === id ? { ...item, packed: !item.packed } : item
)
);
}

function handleClearList() {
const confirmed = window.confirm(
"Are you sure you want to delete all items?"
);

if (confirmed) setItems([]);
}

return (
<div className="app">
<Logo />
<Form onAddItems={handleAddItems} />
<PackingList
items={items}
onDeleteItem={handleDeleteItem}
onToggleItem={handleToggleItem}
onClearList={handleClearList}
/>
<Stats items={items} />
</div>
);
}

解释

(1)handleToggleItem 函数用 map 更新 items 数组中特定元素:

1、不可变性原则
在 React 中,状态是不可变的。我们不能直接修改原始状态对象,而是要基于原始状态创建一个新的状态。map 方法可以创建一个新的数组,其中每个元素都是基于原数组中的元素返回的,但不直接修改原数组。

2、条件更新某一项
map 方法允许你遍历数组中的每个元素,并返回一个新数组。通过在 map 中对每个元素进行条件判断,我们可以只修改需要修改的那一项,而不影响其他项。

(2)handleClearList 函数实现清空列表功能,并通过用户确认来避免误操作:

**window.confirm**:

  • 是浏览器提供的一个方法,用来显示一个带有 确认消息 的对话框。
  • 这个对话框有两个按钮:“确定”“取消”
  • 返回值:
    • 如果用户点击 “确定”window.confirm 返回 true
    • 如果用户点击 “取消”,返回 false

**const confirmed = window.confirm(...)**:

  • 通过 window.confirm 获取用户的操作结果。
  • 用户点击 “确定”confirmed 就是 true
  • 用户点击 “取消”confirmed 就是 false

(3) 为什么 setItems([]) 符合不可变性?

React 强调不可变性,是指我们不直接修改现有状态对象或数组,而是创建一个新对象或数组并将其作为新的状态替代旧状态

1、原始状态不会被修改

  • 假设当前 items 状态是一个非空数组 [item1, item2, item3]
  • 调用 setItems([]) 后,React 直接将新的状态 [](一个新的空数组)替换原来的 items
  • 原始数组 [item1, item2, item3] 仍然保留在内存中,并不会被直接修改。

2、创建了一个新数组

  • [] 是一个全新的空数组,与之前的 items 数组完全没有关联。

  • 这种状态更新方式满足了 React 对不可变性的要求:用新的状态替换旧的状态,而不是修改旧状态

(4)return 里几个子组件:

举个例子:

<Form onAddItems={handleAddItems} />

  • onAddItems 是一个 props,**handleAddItems** 是父组件定义的函数,负责将新项目添加到状态中。

    Form 组件会在用户提交表单时调用 onAddItems,将新数据传递回父组件。

!!!各组件和 props 的关系总结!!!

  1. 父组件(App 组件)负责管理状态items)和核心逻辑(如 handleAddItemshandleDeleteItem)。
  2. 子组件(如 Form、PackingList、Stats)负责 UI 和用户交互
  3. 通过 props 将父组件的函数和数据传递给子组件,子组件通过调用这些函数与父组件通信,实现状态的更新。

(2)Logo.js

1
2
3
export default function Logo() {
return <h1>⛰️ Far Away 💼</h1>;
}

Windows 系统 VSCode 输入 emjoy 表情的快捷键:Windows+.(点)

(3)Form.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useState } from "react";

export default function Form({ onAddItems }) {
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);

function handleSubmit(e) {
e.preventDefault();

if (!description) return;

const newItem = { description, quantity, packed: false, id: Date.now() };

onAddItems(newItem);

setDescription("");
setQuantity(1);
}
//e.target.value总是一个字符串
return (
<form className="add-form" onSubmit={handleSubmit}>
<h3>What do you need for your trip?😘</h3>
<select
value={quantity} //下拉框默认显示值为quantity值初始为1
onChange={(e) => setQuantity(Number(e.target.value))}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map((num) => (
<option value={num} key={num}>
{num} //勾选后此num为select当前值
</option>
))}
</select>
<input
type="text"
placeholder="Item..."
value={description}
onChange={(e) => setDescription(e.target.value)}
></input>
<button>Add</button>
</form>
);
}

解释

(1)e.preventDefault() :

用于阻止浏览器执行事件的默认行为。在表单提交时,默认行为是刷新页面或跳转到指定的 URL。通过调用 e.preventDefault(),可以阻止这种默认行为,使得表单提交时不会导致页面刷新闪烁,从而允许执行自定义的逻辑,例如处理数据或更新状态,而不丢失当前的页面状态。

(2)onAddItems(newItem)

调用父组件传递的onAddItems函数,将newItem传递过去。

(3)<select>:

  • 提供一个下拉列表,用户可以选择 1 到 20 之间的数量。
  • **value={quantity}**:下拉框的当前值绑定到quantity状态。
  • **onChange**:每当用户选择不同的值时,setQuantity会将值更新为数字类型。

(4){Array.from(…).map(…)}:

用花括号包裹是因为在HTML 标签内增加JavaScript语言(以下两个)时需要。

  1. Array.from的作用

Array.from用于创建一个新数组。语法如下:

1
Array.from({ length: n }, (element, index) => /* 返回值 */)
  • **{ length: 20 }**:表示创建一个长度为 20 的数组。数组的每个位置初始值为undefined
  • (_, i) => i + 1:生成器函数,用于返回新数组的每个元素。
    • _:占位符,表示当前元素的值(此处没用)。
    • i:当前元素的索引。
    • i + 1:生成从 1 到 20 的连续整数。
  1. .map的作用

.map方法用于对数组中的每个元素执行一个函数,并返回一个新数组。

  • 遍历数组[1, 2, 3, ..., 20]
  • 对于每个num(即 1 到 20 的数字),生成一个<option>元素。
1
2
3
<option value={num} key={num}>
{num}
</option>
  • **value={num}**:设置<option>value属性为num
  • **key={num}**:React 要求数组渲染时每个元素必须有唯一的key,方便 React 高效更新。
  • **{num}**:<option>的内容是num

(4)PackingList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { useState } from "react";
import Item from "./Item";

/*
状态state控制元素的方法:
1、用useState钩子定义一个状态
2、在想要改变的元素上强制元素取状态变量的值value={description}
3、用onChange更新状态变量,onChange={e=>setDescription(e.target.value)}
*/
//道具是传递父子组件数据的桥梁,不能用于兄弟组件。数据只能从组件树上传递下来,不能向上或横着。
export default function PackingList({
items,
onDeleteItem,
onToggleItem,
onClearList,
}) {
const [sortBy, setSortBy] = useState("input");

let sortedItems;
if (sortBy === "input") sortedItems = items;
if (sortBy === "description")
sortedItems = items
.slice()
.sort((a, b) => a.description.localeCompare(b.description));
if (sortBy === "packed")
sortedItems = items
.slice()
.sort((a, b) => Number(a.packed) - Number(b.packed));

return (
<div className="list">
<ul>
{sortedItems.map((item) => (
<Item
item={item}
onDeleteItem={onDeleteItem}
onToggleItem={onToggleItem}
key={item.id}
/> //Item:组件component名;item:道具props名;{item}:对象本身
))}
</ul>

<div className="actions">
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
<button onClick={onClearList}>Clear list</button>
</div>
</div>
);
}

解释

(1)物品列表排序:

slice():

  • 用来创建一个新的数组副本,避免直接修改原数组(items)。
  • React 等框架中建议避免直接改变状态,确保数据的不可变性。

localeCompare:

  • 用于字符串的字母顺序比较,支持多语言环境。

Number(a.packed):

  • 将布尔值转为数字进行排序:false0true1,未打包物品排在已打包物品前面。

(2)<ul>....</ul>:

将每个sortedItems数组中的元素渲染成一个<Item />组件,并将相关数据和回调函数作为props传递给<Item />组件。

(5)Item.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function Item({ item, onDeleteItem, onToggleItem }) {
return (
<li>
<input
type="checkbox" //定义复选框
value={item.packed} //复选框的值绑定到item.packed
onChange={() => {
//当复选框状态改变时(点击复选框)触发
onToggleItem(item.id); //调用onToggleItem,并传递当前item的id
}}
/>
<span style={item.packed ? { textDecoration: "line-through" } : {}}>
{item.quantity}
{item.description}
</span>
<button onClick={() => onDeleteItem(item.id)}>❌</button>
</li>
);
}

(6)Stats.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default function Stats({ items }) {
if (!items.length)
return (
<p className="stats">
<em>Start adding some items to your packing list🚀</em>
</p>
);

const numItems = items.length;
const numPacked = items.filter((item) => item.packed).length;
const percentage = Math.round((numPacked / numItems) * 100);
return (
<footer className="stats">
<em>
{percentage === 100
? "You got everything!Ready to go ✈️"
: `💼You have ${numItems} items on your list,and you already packed
${numPacked} (${percentage}%)`}
</em>
</footer>
);
}

解释

(1)反引号(``)用于模板字符串:

模板字符串允许在字符串中嵌入变量或表达式,而不需要手动拼接。它的语法是:

1
`固定文本 ${变量或表达式}`;

**${}**:插值语法,用于将变量或表达式的值嵌入到字符串中。

**反引号 ``**:可以包含多行字符串和变量表达式,且更直观、易读。

如果不用模板字符串,可以改成这样(不易读):

1
2
3
4
5
6
7
"💼 You have " +
numItems +
" items on your list, and you already packed " +
numPacked +
" (" +
percentage +
"%)";

写在最后

最初是把所有组件放在一个文件里,但过于冗杂,于是创建了一个components 文件夹存放所有组件。把一个文件里的每个组件进行剥离的快捷方式:

(1)收缩一段组件代码,右键选择重构

step1

(2)重构后显示如下,选择move to a new file

step2

在文件夹中立马有新的文件生成,且文件名和组件名相同,会在父组件里自动添加import {组件名} from “./文件名”,此组件函数前会加上export。但 jonas 老师更喜欢另一种方式:父组件里去掉组件名的花括号,此组件函数前加上export default

完结~撒花★,°:.☆( ̄ ▽  ̄)/$:.°★ 。😋


03-travel-list
http://sue-channing.github.io/2024/12/01/03-travel-list/
作者
Sue-Channing
发布于
2024年12月1日
许可协议