在 Visual Studio 中创建 React 应用

在本教程中,你将使用 JavaScript 和 Visual Studio 2022 为 to-do 列表 Web 应用创建 React 前端。 可在 ToDoJSWebApp找到此应用的代码。

先决条件

请确保安装以下内容:

创建 React ToDo 列表应用

  1. 在 Visual Studio 中,选择“文件 > 新建 > 项目 打开”创建新项目“对话框,选择”React 应用 JavaScript 模板,然后选择“下一步”

    显示选择模板的屏幕截图。

  2. ** 将项目命名为 TodoWebApp 并选择 创建

    这将使用 vite 命令行工具创建 JavaScript 项目。

  3. 在解决方案资源管理器中,右键单击 src 文件夹,然后选择“添加”>“新建文件夹”。 并创建名为 components的新文件夹。

    这是将组件放置在组件文件夹中的常见约定,但这不是必需的。

  4. 右键单击新文件夹,选择 添加 > React JSX 组件文件,将其命名为 TodoList,然后单击 添加

    显示添加 JSX 组件的 屏幕截图。

    这会在组件文件夹中创建新的 JSX 文件。

  5. 打开 TodoList 组件,并将默认内容替换为以下内容:

    function TodoList() {
      return (
        <h2>TODO app contents</h2>
      );
    }
    

    此组件显示一个标头,稍后将替换该标头。

    接下来,在应用中连接此组件。 App.jsx 是加载的主组件,它表示 to-do 列表应用程序。 此组件用于 main.jsx 文件中。

  6. 在解决方案资源管理器中,打开 App.jsx,从顶部移除所有导入,并清除 return 语句的内容。 该文件应如下所示。

    function App() {
      return (
        <>
          <TodoList />
        </>
      );
    }
    export default App;
    
  7. 若要添加 TodoList 组件,请将光标置于片段中,然后键入 <TodoL RETURN。 这将添加组件和导入语句。

    显示向应用添加 JSX 组件的屏幕截图 屏幕截图。

    接下来,清除 CSS 文件。

  8. 打开 App.css 并删除所有内容。

  9. 打开 Index.css 并删除除 :root样式以外的所有内容:

    :root {
      font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
      line-height: 1.5;
      font-weight: 400;
      color-scheme: light dark;
      color: rgba(255, 255, 255, 0.87);
      background-color: #242424;
    }
    

运行应用

从工具栏中选择 “开始调试” 按钮或按 F5 键盘快捷方式。

应用将在浏览器窗口中打开。

显示浏览器中运行的应用的屏幕截图。

向应用添加 to-do 列表函数

可以让应用保持运行状态。 当你进行更改时,应用会使用 Vite 的热模块替换功能,自动刷新以加载最新内容。 某些操作(如添加文件夹或重命名文件)要求停止调试,然后重启应用,但一般情况下,你可以在开发应用时将其保留在后台运行。 打开 TodoList.jsx 组件,以便我们可以开始定义它。

  1. 在解决方案资源管理器中,打开 TodoList.jsx 并添加显示和管理 to-do 列表条目所需的 UI。 将内容替换为以下代码:

    function TodoList() {
      return (
        <div>
          <h1>TODO</h1>
          <div>
            <input type="text" placeholder="Enter a task" required aria-label="Task text" />
            <button className="add-button" aria-label="Add task">Add</button>
          </div>
          <ol id="todo-list">
            <p>existing tasks will be shown here</p>
          </ol>
        </div>
      );
    }
    export default TodoList;
    

    前面的代码为新的 to-do 任务添加输入框和用于提交输入的按钮。 接下来,关联“添加”按钮。 使用 useState React 挂钩添加两个状态变量,一个用于添加的任务,另一个用于存储现有任务。 对于本教程,任务将存储在内存中,而不是永久性存储。

  2. 将以下 import 语句添加到 TodoList.jsx 以导入 useState

    import { useState } from 'react'
    
  3. 接下来,使用该挂钩创建状态变量。 在 return 语句上方的 TodoList 函数中添加以下代码。

    const [tasks, setTasks] = useState(["Drink some coffee", "Create a TODO app", "Drink some more coffee"]);
    const [newTaskText, setNewTaskText] = useState("");
    

    这为数据和两个函数设置了两个变量,tasksnewTaskText,可以调用这些变量来更新这些变量,setTaskssetNewTasks。 更改状态变量的值时,React 会自动重新呈现组件。

    你已准备好更新 TodoList.jsx,以将 to-do 项显示为列表,但首先需要了解一个重要的 React 概念。

    在 React 中,当你显示项列表时,需要添加一个键来唯一标识列表中的每个项。 此功能在 呈现列表的 React 文档中进行了深入介绍,但在这里你将了解基础知识。 你有一个要显示的 to-do 项的列表,并且需要为每个项关联唯一键。 每个项的键不应更改,因此不能将数组中项的索引用作键。 你需要一个 ID,该 ID 在这些值的整个生命周期内都不会更改。 你将使用 randomUUID() 为每个 to-do 项创建唯一 ID。

  4. 使用 UUID 作为每个 to-do 项的键创建 TodoList.jsx。 使用以下代码更新 TodoList.jsx

    import React, { useState } from 'react';
    
    const initialTasks = [
        { id: self.crypto.randomUUID(), text: 'Drink some coffee' },
        { id: self.crypto.randomUUID(), text: 'Create a TODO app' },
        { id: self.crypto.randomUUID(), text: 'Drink some more coffee' }
    ];
    
    function TodoList() {
        const [tasks, setTasks] = useState(initialTasks);
        const [newTaskText, setNewTaskText] = useState("");
    
        return (
            <article
                className="todo-list"
                aria-label="task list manager">
                <header>
                    <h1>TODO</h1>
                    <form className="todo-input" aria-controls="todo-list">
                        <input
                            type="text"
                            placeholder="Enter a task"
                            value={newTaskText} />
                        <button
                            className="add-button">
                            Add
                        </button>
                    </form>
                </header>
                <ol id="todo-list" aria-live="polite" aria-label="task list">
                    {tasks.map((task, index) =>
                        <li key={task.id}>
                            <span className="text">{task.text}</span>
                        </li>
                    )}
                </ol>
            </article>
        );
    }
    export default TodoList;
    

    由于 ID 值是在 TodoList 函数外部分配的,因此,如果重新呈现页面,则可以确保这些值不会更改。 尝试处于此状态的应用时,你会注意到无法键入 todo 输入元素。 这是因为输入元素绑定到已初始化为空白字符串的 newTaskText。 若要允许用户添加新任务,需要处理该控件上的 onChange 事件。 还需要实现“添加”按钮支持。

  5. 在 TodoList 函数的 return 语句之前立即添加所需的函数。

    function handleInputChange(event) {
        setNewTaskText(event.target.value);
    }
    
    function addTask() {
        if (newTaskText.trim() !== "") {
            setTasks(t => [...t, { id: self.crypto.randomUUID(), text: newTaskText }]);
            setNewTaskText("");
        }
        event.preventDefault();
    }
    

    handleInputChanged 函数中,输入字段中的新值通过 event.target.value传入,该值用于使用 setNewTaskText更新 newTaskText 变量的值。 在 addTask 函数中,使用 setTasks 将新任务添加到现有任务列表中,并将项的 ID 设置为新的 UUID 值。 更新输入元素以包含 onChange={handleInputChange} 并更新“添加”按钮以包括 onClick={addTask}。 此代码将事件连接到处理该事件的函数。 之后,应能够向任务列表添加新任务。 新任务将添加到列表底部。 若要使此应用更有用,需要添加对删除任务的支持,以及向上或向下移动任务。

  6. 添加函数以支持删除、上移和下移,然后更新标记以显示每个操作的按钮。 在 return 语句上方的 TodoList 函数中添加以下代码。

    function deleteTask(id) {
        const updatedTasks = tasks.filter(task => task.id != id);
        setTasks(updatedTasks);
    }
    
    function moveTaskUp(index) {
        if (index > 0) {
            const updatedTasks = [...tasks];
            [updatedTasks[index], updatedTasks[index - 1]] = [updatedTasks[index - 1], updatedTasks[index]];
            setTasks(updatedTasks);
        }
    }
    
    function moveTaskDown(index) {
        if (index < tasks.length) {
            const updatedTasks = [...tasks];
            [updatedTasks[index], updatedTasks[index + 1]] = [updatedTasks[index + 1], updatedTasks[index]];
            setTasks(updatedTasks);
        }
    }
    

    delete 函数接收任务 ID,并从列表中删除对应的任务;然后使用 数组的 filter() 方法创建一个新数组,将被删除的项排除在外,接着调用 setTasks()。 另外两个函数接收项的索引,因为这项工作依赖于项的排序。 moveTaskUp()moveTaskDown() 都使用 数组析构分配 来交换所选任务与其邻居。

  7. 接下来,更新视图以包含这三个按钮。 更新 return 语句以包含以下内容。

    return (
        <article
            className="todo-list"
            aria-label="task list manager">
            <header>
                <h1>TODO</h1>
                <form className="todo-input" onSubmit={addTask} aria-controls="todo-list">
                    <input
                        type="text"
                        required
                        autoFocus
                        placeholder="Enter a task"
                        value={newTaskText}
                        aria-label="Task text"
                        onChange={handleInputChange} />
                    <button
                        className="add-button"
                        aria-label="Add task">
                        Add
                    </button>
                </form>
            </header>
            <ol id="todo-list" aria-live="polite">
                {tasks.map((task, index) =>
                    <li key={task.id}>
                        <span className="text">{task.text}</span>
                        <button className="delete-button" onClick={() => deleteTask(task.id)}>
                            🗑️
                        </button>
                        <button className="up-button" onClick={() => moveTaskUp(index)}>
                            ⇧
                        </button>
                        <button className="down-button" onClick={() => moveTaskDown(index)}>
                            ⇩
                        </button>
                    </li>
                )}
            </ol>
        </article>
    );
    

    你已添加执行我们之前讨论的任务所需的按钮。 使用 Unicode 字符作为按钮上的图标。 在标记中,添加了一些属性以支持稍后添加一些 CSS。 你可能还会注意到使用了 aria 属性来提高可访问性,这虽然是可选的,但还是非常推荐使用。 如果运行应用程序,它应如下图所示。

    显示应用正在运行并显示列表的屏幕截图

    现在,你应该能够在 TODO Web 应用中执行以下操作。

    • 添加任务
    • 删除任务
    • 上移任务
    • 向下移动任务

    这些函数可以正常工作,但可以重构以生成可重用组件以显示 to-do 项。 to-do 项的标记被包含在新的组件 TodoItem 中。 由于列表的管理保留在 Todo 组件中,因此可以将回调传递给“删除”和“移动”按钮。

  8. 若要开始,请在解决方案资源管理器中右键单击 组件 文件夹,然后选择“添加 > 新项

  9. 在打开的对话框中,选择 React JSX 组件文件,为其命名 TodoItem,然后选择 添加

  10. 将以下代码添加到 TodoItem。

    在此代码中,将任务和回调作为属性传递到此新组件。

    import PropTypes from 'prop-types';
    
    function TodoItem({ task, deleteTaskCallback, moveTaskUpCallback, moveTaskDownCallback }) {
        return (
            <li aria-label="task" >
                <span className="text">{task}</span>
                <button
                    type="button"
                    aria-label="Delete task"
                    className="delete-button"
                    onClick={() => deleteTaskCallback()}>
                    🗑️
                </button>
                <button
                    type="button"
                    aria-label="Move task up"
                    className="up-button"
                    onClick={() => moveTaskUpCallback()}>
                    ⇧
                </button>
                <button
                    type="button"
                    aria-label="Move task down"
                    className="down-button"
                    onClick={() => moveTaskDownCallback()}>
                    ⇩
                </button>
            </li>
        );
    }
    
    TodoItem.propTypes = {
        task: PropTypes.string.isRequired,
        deleteTaskCallback: PropTypes.func.isRequired,
        moveTaskUpCallback: PropTypes.func.isRequired,
        moveTaskDownCallback: PropTypes.func.isRequired,
    };
    
    export default TodoItem;
    

    前面的代码包含 Todo 组件中的标记,并在其末尾声明 PropTypes。 属性用于将数据从父组件传递到子组件。 关于属性(Props)的更多信息,请参阅 将属性传递给组件 - React。 由于删除和移动函数作为回调传入,因此需要更新 onClick 处理程序以调用这些回调。

  11. 添加所需的代码。 此处显示了使用 TodoItem 组件的 TodoList 的完整代码。

    import React, { useState } from 'react'
    import TodoItem from './TodoItem'
    
    const initialTasks = [
        { id: self.crypto.randomUUID(), text: 'Drink some coffee' },
        { id: self.crypto.randomUUID(), text: 'Create a TODO app' },
        { id: self.crypto.randomUUID(), text: 'Drink some more coffee' }
    ];
    
    function TodoList() {
        const [tasks, setTasks] = useState(initialTasks);
        const [newTaskText, setNewTaskText] = useState("");
        function handleInputChange(event) {
            setNewTaskText(event.target.value);
        }
        function addTask() {
            if (newTaskText.trim() !== "") {
                setTasks(t => [...t, { id: self.crypto.randomUUID(), text: newTaskText }]);
                setNewTaskText("");
            }
            event.preventDefault();
        }
        function deleteTask(id) {
            const updatedTasks = tasks.filter(task => task.id !== id);
            setTasks(updatedTasks);
        }
        function moveTaskUp(index) {
            if (index > 0) {
                const updatedTasks = [...tasks];
                [updatedTasks[index], updatedTasks[index - 1]] = [updatedTasks[index - 1], updatedTasks[index]];
                setTasks(updatedTasks);
            }
        }
        function moveTaskDown(index) {
            if (index < tasks.length) {
                const updatedTasks = [...tasks];
                [updatedTasks[index], updatedTasks[index + 1]] = [updatedTasks[index + 1], updatedTasks[index]];
                setTasks(updatedTasks);
            }
        }
        return (
            <article
                className="todo-list"
                aria-label="task list manager">
                <header>
                    <h1>TODO</h1>
                    <form onSubmit={addTask} aria-controls="todo-list">
                        <input
                            type="text"
                            required
                            placeholder="Enter a task"
                            value={newTaskText}
                            aria-label="Task text"
                            onChange={handleInputChange} />
                        <button
                            className="add-button"
                            aria-label="Add task">
                            Add
                        </button>
                    </form>
                </header>
                <ol id="todo-list" aria-live="polite">
                    {tasks.map((task, index) =>
                        <TodoItem
                            key={task.id}
                            task={task.text}
                            deleteTaskCallback={() => deleteTask(task.id)}
                            moveTaskUpCallback={() => moveTaskUp(index)}
                            moveTaskDownCallback={() => moveTaskDown(index)}
                        />
                    )}
                </ol>
            </article>
        );
    }
    
    export default TodoList;
    

    现在,TodoItem 组件用于呈现每个 to-do 项。 请注意,该键设置为 task.id,其中包含该任务的 UUID 值。 运行应用时,不应看到对应用的外观或行为所做的任何更改,因为你重构了它以使用 TodoItem。

    现在,你已实现了所有基本功能,接下来开始添加一些样式,使其看起来更加美观。 首先,在 Index.html 中添加一个字体系列 Inter 的链接,你将在此应用中使用该字体系列。 在 Index.html 中,有一些其他的项需要清理。 具体而言,标题应更新,并且你想要替换当前用作图标的 vite.svg 文件。

  12. 使用以下内容更新 Index.html

    <!doctype html>
    <html lang="en">
        <head>
            <meta charset="UTF-8" />
            <link rel="icon" type="image/svg+xml" href="/checkmark-square.svg" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>TODO app</title>
            <link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'>
            <script type="module" defer src="/src/main.jsx"></script>
        </head>
        <body>
        </body>
    </html>
    
  13. 编辑 main.jsx 文件,以在调用 createRoot时将 root 替换为 main

    此处显示了 main.jsx 的完整代码。

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import App from './App.jsx'
    import './index.css'
    
    createRoot(document.querySelector('main')).render(
        <StrictMode>
            <App />
        </StrictMode>,
    )
    

    除了这些更改,文件 checkmark-square.svg 已添加到公用文件夹中。 这是 FluentUI 复选标记方块图像中的 SVG,可以直接下载。 (有一个包可用于更集成的体验,但这超出了本文的范围。

    接下来,更新 TodoList 组件的样式。

  14. 在组件文件夹中,添加名为 TodoList.css的新 CSS 文件。 可以右键单击项目并选择 “添加 > 新项”,然后选择 样式表。 为文件提供名称 TodoList.css

  15. 将以下代码添加到 TodoList.css

    .todo-list {
        background-color: #1e1e1e;
        padding: 1.25rem;
        border-radius: 0.5rem;
        box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.3);
        width: 100%;
        max-width: 25rem;
    }
    
    .todo-list h1 {
        text-align: center;
        color: #e0e0e0;
    }
    
    .todo-input {
        display: flex;
        justify-content: space-between;
        margin-bottom: 1.25rem;
    }
    
    .todo-input input {
        flex: 1;
        padding: 0.625rem;
        border: 0.0625rem solid #333;
        border-radius: 0.25rem;
        margin-right: 0.625rem;
        background-color: #2c2c2c;
        color: #e0e0e0;
    }
    
    .todo-input .add-button {
        padding: 0.625rem 1.25rem;
        background-color: #007bff;
        color: #fff;
        border: none;
        border-radius: 0.25rem;
        cursor: pointer;
    }
    
    .todo-input .add-button:hover {
        background-color: #0056b3;
    }
    
    .todo-list ol {
        list-style-type: none;
        padding: 0;
    }
    
    .todo-list li {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0.625rem;
        border-bottom: 0.0625rem solid #333;
    }
    
    .todo-list li:last-child {
        border-bottom: none;
    }
    
    .todo-list .text {
        flex: 1;
    }
    
    .todo-list li button {
        background: none;
        border: none;
        cursor: pointer;
        font-size: 1rem;
        margin-left: 0.625rem;
        color: #e0e0e0;
    }
    
    .todo-list li button:hover {
        color: #007bff;
    }
    
    .todo-list li button.delete-button {
        color: #ff4d4d;
    }
    
    .todo-list li button.up-button,
    .todo-list li button.down-button {
        color: #4caf50;
    }
    
  16. 接下来,编辑 TodoList.jsx,在文件顶部添加以下导入。

    import './TodoList.css';
    
  17. 保存更改后刷新浏览器。 这应改进应用的样式。 应用应如下所示。

    显示正在运行的应用的最终版本的屏幕截图。

    现在,你已生成一个工作 to-do 列表应用,用于将 to-do 项存储在内存中。 此时,可以更新应用,将 to-do 项存储在 localStorage/IndexedDb中,或者将其与服务器端数据库或其他后端集成,以便进行更永久的存储。

总结

在本教程中,你已使用 Visual Studio 创建新的 React 应用。 该应用包含一个 to-do 列表,其中包括添加任务、删除任务和重新排序的支持。 你创建了两个新的 React 组件,并在本教程中使用了这些组件。

资源