头图

前言

为了入门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;
}

样式代码具体解释如下:

  1. 全局样式

    • * { margin: 0; padding: 0; box-sizing: border-box; }:这是一个通配符选择器,它将所有元素的内外边距都设为0,并设置盒模型为border-box
    • button, input { outline: none; }:移除了按钮和输入框的默认轮廓线。
  2. 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添加了无限旋转的动画。
  3. 布局和容器样式

    • .flex-center:定义了一个垂直居中的flex容器。
    • .handle-container 和 .todo-list:分别定义了不同的flex容器样式,用于页面布局。
    • .ml-15:为元素添加了左边距。
  4. Todo列表样式

    • .todo-list-item:定义了待办事项列表项的基本样式。
    • .todo-list-item::before:使用伪元素在列表项前添加了一个序号。
    • .todo-list-item p:定义了列表项中文本的样式,包括文字溢出处理和鼠标悬停时的效果。
  5. 输入框和按钮样式

    • .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)。

组件的逻辑

  1. 组件接受上述两个 props。
  2. 渲染一个按钮,其类名为 "ew-btn ew-btn-primary ml-15"。
  3. 按钮的 onClick 事件与 onToDo 函数绑定。当按钮被点击时,如果提供了 onToDo 函数,那么它会被调用。
  4. 按钮的文本内容根据 editUUid 的值来决定:

    • 如果 editUUid 不等于 -1,那么按钮的文本是 "编辑"。
    • 如果 editUUid 等于 -1 或者没有提供 editUUid,那么按钮的文本是 "添加"。

使用示例

  1. 添加模式
<AddEditButton onToDo={() => console.log('添加新的条目')} />

这将渲染一个文本为 "添加" 的按钮,点击时会在控制台输出 "添加新的条目"。

  1. 编辑模式
<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_valuehandleChangehandleKeyDown。这些props分别对应于输入框的值、值改变时的处理函数以及键盘按键按下时的处理函数。

以下是对代码的详细解释:

  1. 导入依赖

    • react导入React库和SyntheticEvent类型。
  2. 定义TodoInputProps接口

    • todo_value?: 一个可选的字符串,表示输入框的当前值。
    • handleChange?: 一个可选的函数,当输入框的值改变时会被调用。它接收一个SyntheticEvent对象,该对象描述了事件本身(包括事件源、事件类型等)。
    • handleKeyDown?: 一个可选的键盘事件处理函数,当输入框中有键盘按键按下时会被调用。它的类型是React.KeyboardEventHandler<HTMLInputElement>,表示它是一个处理HTMLInputElement上键盘事件的函数。
  3. 定义TodoInput组件

    • 使用React.FC<TodoInputProps>来定义TodoInput组件为一个函数组件,该组件接受TodoInputProps类型的props。
    • 在组件的函数体中,通过解构赋值从props中提取todo_valuehandleChangehandleKeyDown
    • 渲染一个<input>元素,并设置其type为"text"(表示文本输入框),className为"ew-input"(用于样式应用),valuetodo_value(从props中传入的),placeholder为"请输入需要待办的事项"(当输入框为空时显示的提示文本)。
    • <input>元素添加onChangeonKeyDown事件处理器,分别绑定到handleChangehandleKeyDown(从props中传入的)。
  4. 导出TodoInput组件

    • 使用export defaultTodoInput组件导出,以便在其他文件中导入和使用。

列表组件

列表组件代码如下所示:

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 目录导入了 ListsAddEditButton 和 TodoInput 组件。
  • createUUID: 从 ./utils 导入的函数,用于生成唯一的 UUID。
  • ReactSvg: 一个 SVG 图标,可能是 React 的 Logo。

App 组件

  • App 是一个函数组件,它接受一个可选的 className prop。
  • 使用了三个 React 状态钩子(useState)来管理:

    1. 输入框的值(state)。
    2. 正在编辑的待办事项的 UUID(editUUid)。
    3. 待办事项列表(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组件的实现原理,包含事件,类名等属性的传递,还有相关类型定义。

想要查看完整源码,可以前往这里查看。

第一版的代码可以前往这里查看。

如果觉得本文有用,欢迎点赞收藏,没用可以忽略,感谢大家阅读。


夕水
5.3k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。