前言
为了入门react,我特意花了10分钟写了一个todo-list,它长这样:
我将之拆分成如下模块:
- 图标
- 输入框与添加按钮
列表展示待办事项
- 事项索引
- 事项详情
- 编辑与删除
这其中还包含了一些隐藏的操作,例如输入待办事项,按下enter键也可以实现编辑或者确定的效果,点击待办事项还可以弹出一个消息提示框,展示待办事项详情,点击删除出现二次确认弹框。
给我10分钟,实现如上功能足够了,接下来我们一起来看看吧。
插件部分
插件用到了弹框插件和消息提示框插件,这两个插件是我以前手写实现的,这里暂不概述,感兴趣可分别点击如下链接查看。
工具函数
这里就用到了一个工具函数,那就是生成伪uuid格式的随机字符串,它不是一个真正符合标准格式的uuid。代码如下:
export const createUUID = () =>
substr((Math.random() * 10000000).toString(16), 0, 4) +
'-' +
new Date().getTime() +
'-' +
substr(Math.random().toString(), 2, 5);
可以看到,我们通过随机函数以及日期函数来拼接出这个字符串,用来当作待办事项的id,方便我们做删除和编辑操作。
注意: 这里由于substr方法是一个已废弃的方法,因此这里我们模拟实现了这个方法,这个方法的实现代码还不少。如下所示:
export const trunc = <T extends number>(x: T) =>
Math.trunc(x) ||
(function (x) {
let n = +x;
return (n > 0 ? Math.floor : Math.ceil)(x);
})(x);
export const isString = <T>(v: T) => typeof v === 'string';
export const toIntOrInf = <T>(x: T) => {
let n = +x;
return n !== n || n === 0 ? 0 : trunc(n);
};
export const substr = (str: string, s: number, l: number) => {
if (!isString(str)) {
str = String(str);
}
const size = str.length;
let intStart = toIntOrInf(s);
let intLength, intEnd;
if (intStart === Infinity) intStart = 0;
if (intStart < 0) intStart = Math.max(size + intStart, 0);
intLength = l === undefined ? size : toIntOrInf(l);
if (intLength <= 0 || intLength === Infinity) return '';
intEnd = Math.min(intStart + intLength, size);
return intStart >= intEnd
? ''
: String.prototype.slice.call(str, intStart, intEnd);
};
以下是以上工具函数的详细解释:
trunc
函数
- 目的:对给定的数字进行截断,即移除其小数部分。
- 参数:
x
(一个泛型类型,扩展自number
)。 实现:
- 首先,它尝试使用
Math.trunc(x)
来截断数字。但是,如果x
是一个特殊的NaN
或无穷大/无穷小值,Math.trunc
会返回NaN
。 - 如果
Math.trunc(x)
返回NaN
或false
(在 JavaScript 中,NaN
是唯一一个与任何值(包括它自身)都不相等的值,并且当转换为布尔值时,NaN
会被当作false
),则执行一个内部函数。 - 内部函数试图将
x
转换为数字n
(通过一元加号+
操作符)。然后,它检查n
是否大于 0,如果是,则使用Math.floor
向下取整;否则,使用Math.ceil
向上取整。但这里的逻辑似乎有些冗余,因为对于非负数,Math.trunc
已经等同于Math.floor
。
- 首先,它尝试使用
isString
函数
- 目的:检查给定的值是否是字符串。
- 参数:
v
(泛型类型)。 - 实现:使用
typeof
操作符来检查v
是否为字符串,并返回相应的布尔值。
toIntOrInf
函数
- 目的:将给定的值转换为整数或无穷大(如果转换失败)。但如果转换结果为 0 或 NaN,则返回 0。
- 参数:
x
(泛型类型)。 实现:
- 首先,将
x
转换为数字n
(通过一元加号+
操作符)。 - 然后,检查
n
是否不等于自身(即NaN
)或是否等于 0。如果是,则返回 0。 - 否则,使用之前定义的
trunc
函数来截断n
的小数部分,并返回结果。
- 首先,将
substr
函数
- 目的:从字符串中提取一个子串。这个函数类似于 JavaScript 的
substring
或slice
方法,但具有额外的类型检查和边界处理。 参数:
str
:要从中提取子串的字符串。s
:子串开始的索引(可以是任何类型,但会被转换为整数或无穷大)。l
:可选参数,表示子串的长度(同样可以是任何类型,但会被转换为整数或无穷大)。
实现:
- 首先,检查
str
是否是字符串,如果不是,则将其转换为字符串。 - 接下来,进行一系列的类型转换和边界检查,以确保
s
和l
是有效的索引和长度。 - 使用
String.prototype.slice.call(str, intStart, intEnd)
来从str
中提取子串。这里使用call
方法来确保slice
方法在正确的上下文中被调用(即str
)。 - 如果提取的子串为空(即开始索引大于或等于结束索引),则返回空字符串。
- 首先,检查
样式部分
样式代码是对按钮输入框以及logo等的一个美化,完整代码如下所示:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
button,
input {
outline: none;
}
.logo {
height: 10em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
cursor: pointer;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
.logo {
animation: logo-spin infinite 20s linear;
}
}
.flex-center {
height: 100vh;
justify-content: center;
align-items: center;
display: flex;
flex-direction: column;
}
.handle-container {
width: 100%;
max-width: 500px;
display: flex;
justify-content: space-between;
}
.ml-15 {
margin-left: 15px;
}
.todo-list {
padding: 10px 0;
display: flex;
flex-direction: column;
width: 100%;
max-width: 500px;
}
.todo-list-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0;
}
.ew-btn-group {
display: inline-flex;
justify-content: space-around;
align-items: center;
}
.todo-list-item::before {
content: attr(data-order);
color: #f3a42d;
font: 18px '宋体', '楷体';
}
.todo-list-item p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
color: #535455;
letter-spacing: 2px;
font: 16px '黑体', '楷体';
}
.todo-list-item p:hover {
border-bottom: 1px solid #2396ef;
color: #2396ef;
}
.ew-input {
display: inline-block;
padding: 12px 20px;
border: 1px solid #535455;
background-color: transparent;
color: #535455;
font: 14px '幼圆', '楷体';
letter-spacing: 2px;
border-radius: 4px;
width: 100%;
}
.ew-input::-webkit-input-placeholder {
color: #535455;
}
.ew-btn {
display: inline-block;
padding: 12px 20px;
border: 1px solid #dcdfe5;
background-color: #ffffff;
font-size: 14px;
line-height: 1;
white-space: nowrap;
color: #606265;
text-align: center;
transition: all 0.1s;
cursor: pointer;
border-radius: 4px;
letter-spacing: 2px;
}
.ew-btn:hover {
color: #57a5f3;
border-color: #d1e6fc;
background-color: #e6ecf3;
}
.ew-btn.ew-btn-primary {
color: #fff;
background-color: #57a5f3;
border-color: #57a5f3;
}
.ew-btn.ew-btn-primary:hover {
background: #3f94f0;
border-color: #3f94f0;
color: #fff;
}
.ew-btn.ew-btn-danger {
color: #fff;
background-color: #fc7c7c;
border-color: #fc7c7c;
}
.ew-btn.ew-btn-danger:hover {
background: #f78585;
border-color: #f78585;
color: #fff;
}
样式代码具体解释如下:
全局样式:
* { margin: 0; padding: 0; box-sizing: border-box; }
:这是一个通配符选择器,它将所有元素的内外边距都设为0,并设置盒模型为border-box
。button, input { outline: none; }
:移除了按钮和输入框的默认轮廓线。
Logo样式:
.logo
:定义了Logo的基本样式,包括高度、内边距、鼠标悬停时的阴影效果等。.logo:hover
和.logo.react:hover
:分别定义了Logo在悬停时和具有.react
类的Logo在悬停时的阴影效果。@keyframes logo-spin
:定义了一个名为logo-spin
的关键帧动画,用于旋转Logo。@media (prefers-reduced-motion: no-preference) { .logo { ... } }
:当用户没有偏好减少页面动画时,为Logo添加了无限旋转的动画。
布局和容器样式:
.flex-center
:定义了一个垂直居中的flex容器。.handle-container
和.todo-list
:分别定义了不同的flex容器样式,用于页面布局。.ml-15
:为元素添加了左边距。
Todo列表样式:
.todo-list-item
:定义了待办事项列表项的基本样式。.todo-list-item::before
:使用伪元素在列表项前添加了一个序号。.todo-list-item p
:定义了列表项中文本的样式,包括文字溢出处理和鼠标悬停时的效果。
输入框和按钮样式:
.ew-input
:定义了输入框的基本样式。.ew-input::-webkit-input-placeholder
:定义了输入框占位符的样式。.ew-btn
:定义了按钮的基本样式。
组件部分
组件我们分成了三个组件,即添加/编辑按钮组件,输入框组件以及列表组件,最后是根组件App。
添加/编辑按钮组件
组件代码如下所示:
import React from 'react';
interface AddEditButtonProps {
onToDo?: () => void;
editUUid?: string | number;
}
const AddEditButton: React.FC<AddEditButtonProps> = ({ onToDo, editUUid }) => (
<button
className="ew-btn ew-btn-primary ml-15"
type="button"
onClick={onToDo}
>
确认{editUUid !== -1 ? '编辑' : '添加'}
</button>
);
export default AddEditButton;
这个 AddEditButton
是一个 React 函数组件,它根据传入的 props 来决定按钮的文本内容和点击事件的行为。下面是该组件的详细解释:
组件的 props
onToDo?
: 一个可选的函数,当按钮被点击时会被调用。它的类型是一个没有参数且没有返回值的函数。editUUid?
: 一个可选的字符串或数字,代表一个编辑对象的唯一标识符(这里是判断是否是-1)。
组件的逻辑
- 组件接受上述两个 props。
- 渲染一个按钮,其类名为 "ew-btn ew-btn-primary ml-15"。
- 按钮的
onClick
事件与onToDo
函数绑定。当按钮被点击时,如果提供了onToDo
函数,那么它会被调用。 按钮的文本内容根据
editUUid
的值来决定:- 如果
editUUid
不等于-1
,那么按钮的文本是 "编辑"。 - 如果
editUUid
等于-1
或者没有提供editUUid
,那么按钮的文本是 "添加"。
- 如果
使用示例
- 添加模式:
<AddEditButton onToDo={() => console.log('添加新的条目')} />
这将渲染一个文本为 "添加" 的按钮,点击时会在控制台输出 "添加新的条目"。
- 编辑模式:
<AddEditButton onToDo={() => console.log('编辑条目', 123)} editUUid={123} />
这将渲染一个文本为 "编辑" 的按钮,点击时会在控制台输出 "编辑条目 123"。
输入框组件
输入框组件代码如下所示:
import React, { SyntheticEvent } from 'react';
interface TodoInputProps {
todo_value?: string;
handleChange?: (e: SyntheticEvent<HTMLInputElement, Event>) => void;
handleKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
}
const TodoInput: React.FC<TodoInputProps> = ({
todo_value,
handleChange,
handleKeyDown
}) => (
<input
type="text"
className="ew-input"
value={todo_value}
placeholder="请输入需要待办的事项"
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
export default TodoInput;
这段代码定义了一个React输入框组件TodoInput
,它接收三个props(属性):todo_value
、handleChange
和handleKeyDown
。这些props分别对应于输入框的值、值改变时的处理函数以及键盘按键按下时的处理函数。
以下是对代码的详细解释:
导入依赖:
- 从
react
导入React库和SyntheticEvent
类型。
- 从
定义TodoInputProps接口:
todo_value?
: 一个可选的字符串,表示输入框的当前值。handleChange?
: 一个可选的函数,当输入框的值改变时会被调用。它接收一个SyntheticEvent
对象,该对象描述了事件本身(包括事件源、事件类型等)。handleKeyDown?
: 一个可选的键盘事件处理函数,当输入框中有键盘按键按下时会被调用。它的类型是React.KeyboardEventHandler<HTMLInputElement>
,表示它是一个处理HTMLInputElement上键盘事件的函数。
定义TodoInput组件:
- 使用
React.FC<TodoInputProps>
来定义TodoInput
组件为一个函数组件,该组件接受TodoInputProps
类型的props。 - 在组件的函数体中,通过解构赋值从props中提取
todo_value
、handleChange
和handleKeyDown
。 - 渲染一个
<input>
元素,并设置其type
为"text"(表示文本输入框),className
为"ew-input"(用于样式应用),value
为todo_value
(从props中传入的),placeholder
为"请输入需要待办的事项"(当输入框为空时显示的提示文本)。 - 为
<input>
元素添加onChange
和onKeyDown
事件处理器,分别绑定到handleChange
和handleKeyDown
(从props中传入的)。
- 使用
导出TodoInput组件:
- 使用
export default
将TodoInput
组件导出,以便在其他文件中导入和使用。
- 使用
列表组件
列表组件代码如下所示:
import React from 'react';
export interface TodoListItem {
uuid: string;
text: string;
}
export interface ListsProps {
editUUid?: string | number;
todoList?: Partial<TodoListItem>[];
seeDetail?: (t: Partial<TodoListItem>) => void;
handleEdit?: (t: Partial<TodoListItem>, i: number) => void;
handleDelete?: (t: Partial<TodoListItem>, i: number) => void;
}
const Lists: React.FC<ListsProps> = ({
todoList,
seeDetail,
handleEdit,
editUUid,
handleDelete,
}) => (
<div className="todo-list">
{todoList?.map((todo, index) => (
<div
className="todo-list-item"
key={todo.uuid}
data-order={index + 1 + '.'}
>
<p onClick={() => seeDetail?.(todo)}>
{todo?.uuid}:{todo.text}
</p>
<div className="ml-15 ew-btn-group">
<button
className="ew-btn ew-btn-primary"
type="button"
onClick={() => handleEdit?.(todo, index)}
>
{editUUid !== -1 && editUUid === todo.uuid ? '取消' : ''}
编辑
</button>
<button
className="ew-btn ew-btn-danger ml-15"
type="button"
onClick={() => handleDelete?.(todo, index)}
>
删除
</button>
</div>
</div>
))}
</div>
);
export default Lists;
这段 React 代码定义了一个名为 Lists
的函数组件,它接收一个名为 ListsProps
的 props 对象,该对象包含了渲染待办事项列表所需的各种数据和回调函数。以下是对这段代码的详细解释:
1. 接口定义
TodoListItem
: 定义了一个待办事项的数据结构,包括一个唯一的 UUID 和一个文本内容。ListsProps
: 定义了Lists
组件的 props。包括:editUUid
: 当前正在编辑的待办事项的 UUID(或没有编辑的标识)。todoList
: 一个包含待办事项的数组,每个待办事项都是TodoListItem
的一个部分对象(Partial<TodoListItem>
)。seeDetail
: 一个回调函数,用于查看某个待办事项的详细信息。handleEdit
: 一个回调函数,用于编辑某个待办事项。handleDelete
: 一个回调函数,用于删除某个待办事项。
2. 组件实现
Lists
组件接收ListsProps
类型的 props,并使用 map 函数遍历todoList
数组,为每个待办事项渲染一个列表项。每个列表项 (
todo-list-item
) 包含:- 一个
<p>
元素,显示待办事项的 UUID 和文本。当点击这个<p>
元素时,会调用seeDetail
回调函数。 一个按钮组 (
ew-btn-group
),包含两个按钮:- 一个编辑按钮 (
ew-btn ew-btn-primary
),用于编辑当前待办事项。如果当前待办事项的 UUID 与editUUid
相同,则按钮文本变为“取消”。点击此按钮时,会调用handleEdit
回调函数。 - 一个删除按钮 (
ew-btn ew-btn-danger
),用于删除当前待办事项。点击此按钮时,会调用handleDelete
回调函数。
- 一个编辑按钮 (
- 一个
3. 注意点
- 使用可选链操作符 (
?.
) 来安全地访问todoList
和todo
对象上的属性,以防它们为undefined
或null
。 - 使用可选函数调用 (
?.()
) 来安全地调用回调函数,以防它们未定义。 - 使用
key
属性为每个列表项提供唯一的标识符,这里是待办事项的 UUID。 - 使用
data-order
属性为列表项提供排序信息(尽管在这段代码中并未直接使用这个属性)。 - 使用内联样式或 CSS 类(如
ml-15
)来设置按钮之间的间距。
根组件
根组件代码如下所示:
import ewMessage from 'ew-message';
import React from 'react';
import Lists, { TodoListItem } from './components/Lists';
import AddEditButton from './components/AddEditButton';
import TodoInput from './components/TodoInput';
import { createUUID } from './utils';
import ReactSvg from './assets/react.svg';
export interface AppProps {
className?: string;
}
const App: React.FC<AppProps> = ({ className }) => {
const [state, setState] = React.useState(''); //输入的值
const [editUUid, setEditUUid] = React.useState<string | number>(-1); //编辑事项的uuid
const [listData, setListData] = React.useState<TodoListItem[]>([]); //todoList
// 请求数据
const onRequestTodoList = () => {
let todoList: TodoListItem[] = [];
todoList.push({
uuid: createUUID(),
text: 'Now you can learn react.js by writing the todo-list application!',
});
setListData(todoList);
};
// 值改变时
const handleChange = (e: React.SyntheticEvent<HTMLInputElement, Event>) => {
setState((e?.target as HTMLInputElement).value);
};
// 点击确认添加或确认编辑
const handleToDo = (editUUid: string | number) => {
if (!state) return ewMessage.error('请输入需要待办的事项!');
// 等于-1表示是添加否则是编辑
if (editUUid === -1) {
let newTodoList: TodoListItem[] = [];
newTodoList.push({
text: state,
uuid: createUUID(),
});
setListData(listData.concat(newTodoList));
setState('');
} else {
let idx = listData.findIndex((_) => _.uuid === editUUid);
if (idx > -1) {
listData[idx].text = state;
}
setListData(listData);
setState('');
setEditUUid(-1);
}
};
// 当输入值并按下enter键时触发
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.keyCode === 13) {
handleToDo(editUUid);
} else {
return null;
}
};
// 点击编辑
const handleEdit = (
item: Partial<TodoListItem>,
editUUid: number | string
) => {
if (editUUid === -1 || editUUid !== item.uuid) {
setEditUUid(item.uuid!);
setState(item.text!);
} else {
setEditUUid(-1);
setState('');
}
};
// 点击删除
const handleDelete = (item: Partial<TodoListItem>) => {
window.ewConfirm({
title: '温馨提示',
content: '确定要删除该待办事项吗?',
showCancel: true,
sure: (context: { close: () => void }) => {
context.close();
// copy the listData and splice the item,set the copyable data to listData,the dom can update the listData
// so why?
const data = listData.slice();
const idx = data.findIndex((_) => _.uuid === item.uuid);
if (idx > -1) data.splice(idx, 1);
setListData(data);
},
});
};
// 点击查看待办事项详情
const seeDetail = (item: Partial<TodoListItem>) => {
return ewMessage.info({
showClose: false,
content: `<h1 style="margin-bottom:10px;">待办事项详情:</h1><p style="margin-bottom:10px;">uuid:${item.uuid}</p><p style="margin-bottom:10px;">事项:${item.text}</p>`,
duration: 3000,
showTypeIcon: false,
});
};
React.useEffect(() => {
onRequestTodoList();
}, []);
return (
<section className={className}>
<img src={ReactSvg} className="logo react" alt="React logo" />
<div className="handle-container">
<TodoInput
handleChange={(e) => handleChange(e)}
todo_value={state}
handleKeyDown={(e) => handleKeyDown(e)}
/>
<AddEditButton
onToDo={() => handleToDo(editUUid)}
editUUid={editUUid}
/>
</div>
<Lists
handleEdit={(item) => handleEdit(item, editUUid)}
todoList={listData}
handleDelete={(item) => handleDelete(item)}
seeDetail={(item) => seeDetail(item)}
editUUid={editUUid}
/>
</section>
);
};
export default App;
这段代码是一个使用 React 编写的待办事项列表(Todo List)应用程序的一部分。它包含了主组件 App
的实现,以及一些与待办事项列表相关的状态和事件处理函数。以下是对这段代码的详细解释:
导入模块
ewMessage
: 可能是用于显示消息(如错误、成功等)的库或组件。React
: React 库本身。- 组件导入:从
./components
目录导入了Lists
、AddEditButton
和TodoInput
组件。 createUUID
: 从./utils
导入的函数,用于生成唯一的 UUID。ReactSvg
: 一个 SVG 图标,可能是 React 的 Logo。
App
组件
App
是一个函数组件,它接受一个可选的className
prop。使用了三个 React 状态钩子(
useState
)来管理:- 输入框的值(
state
)。 - 正在编辑的待办事项的 UUID(
editUUid
)。 - 待办事项列表(
listData
)。
- 输入框的值(
事件处理函数
onRequestTodoList
: 初始化待办事项列表的函数。在这里,它只是创建了一个包含单个待办事项的列表。handleChange
: 当输入框的值改变时触发,更新state
。handleToDo
: 当点击“确认添加”或“确认编辑”时触发。如果editUUid
为-1
,则添加新的待办事项;否则,编辑现有的待办事项。handleKeyDown
: 当在输入框中按下键盘键时触发。如果按下的是 Enter 键(键码为 13),则调用handleToDo
函数。handleEdit
: 当点击编辑按钮时触发。但由于代码被截断了,我们只能看到函数的一部分。这个函数应该更新editUUid
状态以表示正在编辑的待办事项的 UUID。
特别说明
handleChange
和handleKeyDown
中的类型断言(如(e?.target as HTMLInputElement)
)是安全的,因为这两个事件处理函数都是绑定到<input>
元素的。handleToDo
函数在添加新待办事项时使用了concat
方法,这可能会导致性能问题,特别是当待办事项列表变得很长时。更常见的做法是使用展开运算符(...
)来创建新的数组,如setListData([...listData, newTodo]);
。handleEdit
根据函数名和参数,是更新editUUid
状态,并可能触发TodoInput
组件的重新渲染以显示正在编辑的待办事项的文本,也就是往输入框中注入编辑的值,也将uuid记录一下,用来确定当前编辑的是哪个待办事项,从而达到更新的目的。ReactSvg
组件在代码片段中没有使用,但可能在完整的组件树中的其他地方使用。
最后
最后我们给App组件加上一个类名即可,如下所示:
<App className="flex-center" />
总结
通过本文,我们熟悉了react的常用语法,例如使用useState来管理状态,使用props来向子组件传递数据,同样我们也可以使用回调函数将子组件的数据回调给父组件,并且我们还知道了react组件的实现原理,包含事件,类名等属性的传递,还有相关类型定义。
想要查看完整源码,可以前往这里查看。
第一版的代码可以前往这里查看。
如果觉得本文有用,欢迎点赞收藏,没用可以忽略,感谢大家阅读。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。