<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ cyan.wu - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ cyan.wu - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 03 Jun 2026 17:04:22 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/cyan/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何用 React、TypeScript、NodeJS 和 MongoDB 搭建待办事项 App ]]>
                </title>
                <description>
                    <![CDATA[ 在本教程中，我们将在服务器和客户端使用 TypeScript、React、NodeJS、Express 和 MongoDB 从头开始构建一个 Todo 应用程序。 我们从设计 API 开始。  * 用 NodeJS, Express, MongoDB 和 TypeScript 设计 API  * 启动  * 创建 Todo 类型  * 创建 Todo 模块  * 创建 API 控制器  * 获取、新增、更新和删除 Todo  * 创建 API 路由  * 创建服务器 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-a-todo-app-with-react-typescript-nodejs-and-mongodb/</link>
                <guid isPermaLink="false">5f3a0d77c8da7105cbc14b54</guid>
                
                <dc:creator>
                    <![CDATA[ cyan.wu ]]>
                </dc:creator>
                <pubDate>Wed, 14 Oct 2020 10:59:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/08/cover-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在本教程中，我们将在服务器和客户端使用 TypeScript、React、NodeJS、Express 和 MongoDB 从头开始构建一个 Todo 应用程序。</p>
<p>我们从设计 API 开始。</p>
<ul>
<li>用 NodeJS, Express, MongoDB 和 TypeScript 设计 API</li>
<li>启动</li>
<li>创建 Todo 类型</li>
<li>创建 Todo 模块</li>
<li>创建 API 控制器</li>
<li>获取、新增、更新和删除 Todo</li>
<li>创建 API 路由</li>
<li>创建服务器</li>
<li>用 React 和 TypeScript 创建客户端</li>
<li>启动</li>
<li>创建 Todo 类型</li>
<li>从 API 获取数据</li>
<li>创建组件</li>
<li>添加 Todo 表单</li>
<li>展示 Todo</li>
<li>获取和展示数据</li>
<li>资源</li>
</ul>
<h2 id="nodejsexpressmongodbtypescriptapi">用NodeJS, Express, MongoDB 和 TypeScript 设计 API</h2>
<h3 id="">启动</h3>
<p>如果你是新手，可以看看 <a href="https://www.ibrahima-ndaw.com/blog/a-practical-guide-to-typescript/">TypeScript 实用指南</a>，或者从 <a href="https://www.ibrahima-ndaw.com/blog/graphql-api-express-mongodb/">如何用 Node JS、Express 和 MongoDB 从头创建 API</a>。如果你有一定经验了，可以直接开始。</p>
<p>在终端上运行这个命令，创建一个新的 NodeJS 应用程序：</p>
<pre><code class="language-shell">  yarn init

</code></pre>
<p>它会询问几个问题，然后初始化应用程序。你可以通过向命令中添加 <code>-y</code> 标志来跳过。</p>
<p>然后，按照以下目录构建项目:</p>
<pre><code>├── dist
├── node_modules
├── src
   ├── app.ts
   ├── controllers
   |  └── todos
   |     └── index.ts
   ├── models
   |  └── todo.ts
   ├── routes
   |  └── index.ts
   └── types
      └── todo.ts
├── nodemon.json
├── package.json
├── tsconfig.json

</code></pre>
<p>如你所见，这个文件结构相对简单。代码编译成纯 JavaScript 后，dist 目录将用作输出文件夹。</p>
<p>我们还有一个 <code>app.ts</code>，它是服务器的入口。控制器、类型和路由也在它们各自以它们命名的的文件夹中。</p>
<p>现在，我们需要配置 <code>tsconfig.json</code>，使编译器运行我们的首选项。</p>
<ul>
<li>tsconfig.json</li>
</ul>
<pre><code class="language-js">{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist/js",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/types/*.ts", "node_modules", ".vscode"]
}

</code></pre>
<p>这里强调四个主要属性:</p>
<p><code>outDir</code>: 告诉编译器把编译好的代码放进 <code>dist/js</code> 文件夹</p>
<p><code>rootDir</code>: 告诉 TypeScript 编译 src 文件夹中的每个 .ts 文件</p>
<p><code>include</code>: 告诉编译器包含 src 目录和子目录中的文件</p>
<p><code>exclude</code>: 在编译时会排除数组中的文件或文件夹</p>
<p>现在我们安装依赖项，使项目可以使用 TypeScript。因为默认情况下，这个应用程序会使用 JavaScript。</p>
<p>在 NodeJS 应用程序中有两种使用 TypeScript 的方法，要么在项目中本地安装使用，要么在电脑中全局安装使用。基于个人喜好，我会选择后者。但如果你想，你也可以坚持使用本地安装使用的方式。</p>
<p>现在，让我们在终端上执行以下命令来安装 TypeScript。</p>
<pre><code class="language-shell">  yarn add typescript -g

</code></pre>
<p>这个 <code>g</code> 标志允许全局安装 TypeScript，这样它就能在计算机任何地方使用。</p>
<p>接下来，为了使用 Express 和 MongoDB，我们安装一些依赖项。</p>
<pre><code class="language-shell">  yarn add express cors mongoose

</code></pre>
<p>我们还需要安装它们的类型作为开发依赖项，帮助 TypeScript 编译器理解这些包。</p>
<pre><code class="language-shell">  yarn add -D @types/node @types/express @types/mongoose @types/cors

</code></pre>
<p>现在，TypeScript 不会再对你提示错误——它将使用这些类型来定义我们刚刚安装的库。</p>
<p>我们还需要安装其他依赖项，以便能够编译 TypeScript 代码并同时启动服务器。</p>
<pre><code class="language-shell">  yarn add -D concurrently nodemon

</code></pre>
<p>有了这些，我们现在就可以更新 <code>package.json</code> 的 scripts 来启动服务器。</p>
<ul>
<li>package.json</li>
</ul>
<pre><code class="language-js">  "scripts": {
    "build": "tsc",
    "start": "concurrently \"tsc -w\" \"nodemon dist/js/app.js\""
  }

</code></pre>
<p><code>concurrently</code>  帮助编译 TypeScript 代码，持续观察变化，同时启动服务器。也就是说，我们现在可以启动服务器了——但是，我们还没有创建一些有意义的东西。所以，让我们在下一节中解决这个问题。</p>
<h3 id="todo">创建 Todo 类型</h3>
<ul>
<li>types/todo.ts</li>
</ul>
<pre><code class="language-ts">import { Document } from "mongoose"
export interface ITodo extends Document {
  name: string
  description: string
  status: boolean
}

</code></pre>
<p>这里，我们有了继承 <code>mongoose</code> 提供的 <code>Document</code> 类型的 Todo 接口。稍后我们将使用它与 MongoDB 交互。也就是说，我们现在可以定义 Todo 模块。</p>
<h3 id="todo">创建 Todo 模块</h3>
<ul>
<li>models/todo.ts</li>
</ul>
<pre><code class="language-ts">import { ITodo } from "./../types/todo"
import { model, Schema } from "mongoose"
const todoSchema: Schema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
 description: {
      type: String,
      required: true,
    },

    status: {
      type: Boolean,
      required: true,
    },
  },
  { timestamps: true }
)
export default model&lt;ITodo&gt;("Todo", todoSchema)
</code></pre>
<p>首先导入 <code>ITodo </code> 接口和 一些 <code>mongoose</code> 导出的模块，后者是帮助定义 Todo schema 和在导出前把 ITodo 作为类型参数传入 <code>model</code> 。</p>
<p>这样，我们现在就可以在其他文件中使用 Todo 模块来与数据库交互。</p>
<h3 id="api">创建 API 控制器</h3>
<h4 id="todos">获取、新增、更新和删除 Todos</h4>
<ul>
<li>controllers/todos/index.ts</li>
</ul>
<pre><code class="language-ts">import { Response, Request } from "express"
import { ITodo } from "./../../types/todo"
import Todo from "../../models/todo"

const getTodos = async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {
  try {
    const todos: ITodo[] = await Todo.find()
    res.status(200).json({ todos })
  } catch (error) {
    throw error
  }
}
</code></pre>
<p>这里，我们首先需要从 <code>express</code> 导入一些类型，因为我想显式指明类型。如果你想，你可以让 TypeScript 帮你推断。</p>
<p>接下来，我们使用 getTodos() 函数来获取数据，它接收 <code>req</code> 和 <code>res</code> 参数并返回 promise。</p>
<p>在前面创建的 Todo 模块的帮助下，我们现在可以从 MongoDB 获取数据并返回 Todo 数组。</p>
<ul>
<li>controllers/todos/index.ts</li>
</ul>
<pre><code class="language-ts">const addTodo = async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {
   try {
    const body = req.body as Pick&lt;ITodo, "name" | "description" | "status"&gt;

    const todo: ITodo = new Todo({
      name: body.name,
      description: body.description,
      status: body.status,
    })

    const newTodo: ITodo = await todo.save()
    const allTodos: ITodo[] = await Todo.find()

    res
      .status(201)
      .json({ message: "Todo added", todo: newTodo, todos: allTodos })
  } catch (error) {
    throw error
  }
}
</code></pre>
<p>如你所见，<code>addTodo()</code> 函数接收包含用户输入数据的 body 对象。</p>
<p>接下来，我使用类型转换来避免拼写错误，并限制 <code>body</code> 变量与 <code>ITodo</code> 类型匹配，然后基于该模块创建一个新的 Todo。</p>
<p>有了这些，我们现在可以在 DB 中保存 Todo 并返回新增的 Todo 和更新后的 todos 数组。</p>
<ul>
<li>controllers/todos/index.ts</li>
</ul>
<pre><code class="language-ts">const updateTodo = async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {
  try {
    const {
      params: { id },
      body,
    } = req
    const updateTodo: ITodo | null = await Todo.findByIdAndUpdate(
      { _id: id },
      body
    )
    const allTodos: ITodo[] = await Todo.find()
    res.status(200).json({
      message: "Todo updated",
      todo: updateTodo,
      todos: allTodos,
    })
  } catch (error) {
    throw error
  }
}

</code></pre>
<p>为了实现更新 todo, 我们需要拿到 id 和从 <code>req</code> 对象中获取 body，然后把他们传入 <code>findByIdAndUpdate()</code>，这个函数将会在数据库中找到 Todo 并且更新它。</p>
<ul>
<li>controllers/todos/index.ts</li>
</ul>
<pre><code class="language-ts">const deleteTodo = async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {
  try {
    const deletedTodo: ITodo | null = await Todo.findByIdAndRemove(
      req.params.id
    )
    const allTodos: ITodo[] = await Todo.find()
    res.status(200).json({
      message: "Todo deleted",
      todo: deletedTodo,
      todos: allTodos,
    })
  } catch (error) {
    throw error
  }
}

export { getTodos, addTodo, updateTodo, deleteTodo }
</code></pre>
<p><code>deleteTodo()</code> 函数允许你从数据库中删除 Todo。在这里，我们从 req 中拿到 id，并把它作为参数传递给 <code>findByIdAndRemove()</code>，来获取到对应的 Todo 并从 DB 中删除它。</p>
<p>接下来，导出这些函数以便我们在其他文件中使用它们。也就是说，我们现在可以为 API 创建一些路由，并使用这些方法来处理请求。</p>
<h3 id="api">创建 API 路由</h3>
<ul>
<li>routes/index.ts</li>
</ul>
<pre><code class="language-ts">import { Router } from "express"
import { getTodos, addTodo, updateTodo, deleteTodo } from "../controllers/todos"
const router: Router = Router()
router.get("/todos", getTodos)
router.post("/add-todo", addTodo)
router.put("/edit-todo/:id", updateTodo)
router.delete("/delete-todo/:id", deleteTodo)

</code></pre>
<p>如你所见，我们创建四个路由对应从数据库中获取、新增、更新和删除 todo。因为我们已经创建了函数，所以唯一要做的就是导入这些方法并将它们作为参数传递。</p>
<p>到目前为止，我们已经谈了很多，但是仍然没有启动服务器。所以，我们在下一节中解决这个问题。</p>
<h3 id="">创建服务器</h3>
<p>在创建服务器之前，我们需要在 <code>nodemon.json</code> 加一些环境变量来保存 MongoDB 的凭据。</p>
<ul>
<li>nodemon.json</li>
</ul>
<pre><code class="language-js">{
    "env": {
        "MONGO_USER": "your-username",
        "MONGO_PASSWORD": "your-password",
        "MONGO_DB": "your-db-name"
    }
}

</code></pre>
<p>你可以在 <a href="https://www.mongodb.com/cloud/atlas">MongoDB Atlas</a>，通过创一个新集群来得到凭据。</p>
<ul>
<li>app.ts</li>
</ul>
<pre><code class="language-ts">import express, { Express } from "express"
import mongoose from "mongoose"
import cors from "cors"
import todoRoutes from "./routes"

const app: Express = express()

const PORT: string | number = process.env.PORT || 4000

app.use(cors())
app.use(todoRoutes)

const uri: string = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@clustertodo.raz9g.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&amp;w=majority`
const options = { useNewUrlParser: true, useUnifiedTopology: true }
mongoose.set("useFindAndModify", false)

mongoose
  .connect(uri, options)
  .then(() =&gt;
    app.listen(PORT, () =&gt;
      console.log(`Server running on http://localhost:${PORT}`)
    )
  )
  .catch(error =&gt; {
    throw error
  })

</code></pre>
<p>这里，我们首先从导入 <code>express</code> 库开始，这使用我们能调用 <code>use()</code> 方法，这个方法将帮助处理 Todo 路由。</p>
<p>然后，我们用 <code>mongoose</code> 包，通过读取 <code>nodemon.json</code> 带凭证的 url 去连接 MongoDB。</p>
<p>就是说，现在如果我们能成功连接 MongoDB，服务器就会启动，否则，会抛出错误。</p>
<p>我们现在已经通过 Node、Express、TypeScript 和 MongoDB 完成 api 的构建。现在我们开始用 React 和 TypeScript 构建客户端。</p>
<h2 id="reacttypescript">用 React 和 TypeScript 创建客户端</h2>
<h3 id="">构建</h3>
<p>为了创建一个新的 React 应用，我将会使用 create-react-app ——你可以用其他你想用的方法。</p>
<p>所以，在终端运行以下代码：</p>
<pre><code class="language-shell">  npx create-react-app my-app --template typescript

</code></pre>
<p>然后，为了能获取远程数据安装 Axios 库。</p>
<pre><code class="language-shell">  yarn add axios

</code></pre>
<p>安装完成后，按照以下目录构建项目：</p>
<pre><code>├── node_modules
├── public
├── src
|  ├── API.ts
|  ├── App.test.tsx
|  ├── App.tsx
|  ├── components
|  |  ├── AddTodo.tsx
|  |  └── TodoItem.tsx
|  ├── index.css
|  ├── index.tsx
|  ├── react-app-env.d.ts
|  ├── setupTests.ts
|  └── type.d.ts
├── tsconfig.json
├── package.json
└── yarn.lock

</code></pre>
<p>这样，我们有一个相对简单的文件结构。最值得注意的是  <code>src/type.d.ts</code> 被用来存放类型。我几乎在每个文件中都使用了它们，所以我添加了扩展 <code>.d.ts</code> ，使类型全局可用。现在我们不再需要导入它们。</p>
<h3 id="todo">创建 Todo 类型</h3>
<ul>
<li>src/type.d.ts</li>
</ul>
<pre><code class="language-ts">interface ITodo {
  _id: string
  name: string
  description: string
  status: boolean
  createdAt?: string
  updatedAt?: string
}
interface TodoProps {
  todo: ITodo
}

</code></pre>
<p>这里， <code>ITodo</code>  接口需要跟 API 返回的数据类型一样。这里没有  <code>mongoose</code> , 所以需要加一些额外的属性来匹配 API 定义的数据类型。</p>
<p>然后，我们用相同的的接口定义 <code>TodoProps</code> ，组件会接受它并渲染数据。</p>
<p>现在我们已经定义了类型——现在让我们开始从 API 获取数据。</p>
<h3 id="api">从API获取数据</h3>
<ul>
<li>src/API.ts</li>
</ul>
<pre><code class="language-ts">import axios, { AxiosResponse } from "axios"

const baseUrl: string = "http://localhost:4000"

export const getTodos = async (): Promise&lt;AxiosResponse&lt;ApiDataType&gt;&gt; =&gt; {
  try {
    const todos: AxiosResponse&lt;ApiDataType&gt; = await axios.get(
      baseUrl + "/todos"
    )
    return todos
  } catch (error) {
    throw new Error(error)
  }
}

</code></pre>
<p>我们需要导入 <code>axios</code>，通过 api 来请求数据，然后，用  <code>getTodos()</code>  函数从服务端获取数据。 它将返回 <code>AxiosResponse</code> 为类型的 promise， 保存获取到的 <code>ApiDataType</code> 类型的 Todos。</p>
<ul>
<li>src/API.ts</li>
</ul>
<pre><code class="language-ts">export const addTodo = async (
  formData: ITodo
): Promise&lt;AxiosResponse&lt;ApiDataType&gt;&gt; =&gt; {
  try {
    const todo: Omit&lt;ITodo, "_id"&gt; = {
      name: formData.name,
      description: formData.description,
      status: false,
    }
    const saveTodo: AxiosResponse&lt;ApiDataType&gt; = await axios.post(
      baseUrl + "/add-todo",
      todo
    )
    return saveTodo
  } catch (error) {
    throw new Error(error)
  }
}

</code></pre>
<p>这个函数接受用户输入的数据作为参数并返回 promise。这里，我们需要去掉 <code>_id</code> 属性因为 MongoDB 会自动生成。</p>
<ul>
<li>src/API.ts</li>
</ul>
<pre><code class="language-ts">export const updateTodo = async (
  todo: ITodo
): Promise&lt;AxiosResponse&lt;ApiDataType&gt;&gt; =&gt; {
  try {
    const todoUpdate: Pick&lt;ITodo, "status"&gt; = {
      status: true,
    }
    const updatedTodo: AxiosResponse&lt;ApiDataType&gt; = await axios.put(
      `${baseUrl}/edit-todo/${todo._id}`,
      todoUpdate
    )
    return updatedTodo
  } catch (error) {
    throw new Error(error)
  }
}

</code></pre>
<p>为了实现更新 Todo，我们必须传入更新后的数据和对象 id。这里，我们需要更改 Todo 的 <code>状态</code> ，那么在发送到服务器之前我们只需要选择所需的属性即可。</p>
<ul>
<li>src/API.ts</li>
</ul>
<pre><code class="language-ts">export const deleteTodo = async (
  _id: string
): Promise&lt;AxiosResponse&lt;ApiDataType&gt;&gt; =&gt; {
  try {
    const deletedTodo: AxiosResponse&lt;ApiDataType&gt; = await axios.delete(
      `${baseUrl}/delete-todo/${_id}`
    )
    return deletedTodo
  } catch (error) {
    throw new Error(error)
  }
}

</code></pre>
<p>这里，我们也有一个函数接受 <code>_id</code> 属性作为参数并返回 promise。</p>
<p>有了这些，我们现在可以转到 components 文件夹并向其文件中添加一些有意义的代码。</p>
<h3 id="">创建组件</h3>
<h4 id="todo">增加 Todo 表单</h4>
<ul>
<li>components/AddTodo.tsx</li>
</ul>
<pre><code class="language-jsx">import React, { useState } from 'react'

type Props = { 
  saveTodo: (e: React.FormEvent, formData: ITodo | any) =&gt; void 
}

const AddTodo: React.FC&lt;Props&gt; = ({ saveTodo }) =&gt; {
  const [formData, setFormData] = useState&lt;ITodo | {}&gt;()

  const handleForm = (e: React.FormEvent&lt;HTMLInputElement&gt;): void =&gt; {
    setFormData({
      ...formData,
      [e.currentTarget.id]: e.currentTarget.value,
    })
  }

  return (
    &lt;form className='Form' onSubmit={(e) =&gt; saveTodo(e, formData)}&gt;
      &lt;div&gt;
        &lt;div&gt;
          &lt;label htmlFor='name'&gt;Name&lt;/label&gt;
          &lt;input onChange={handleForm} type='text' id='name' /&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;label htmlFor='description'&gt;Description&lt;/label&gt;
          &lt;input onChange={handleForm} type='text' id='description' /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;button disabled={formData === undefined ? true: false} &gt;Add Todo&lt;/button&gt;
    &lt;/form&gt;
  )
}

export default AddTodo

</code></pre>
<p>如你所见，这里有一个 React 类型的函数组件。FC (FC 代表函数组件)，它接收 <code>saveTodo()</code> 方法为 props，该方法允许我们将数据保存到数据库。</p>
<p>然后，我们创建 <code>formData</code> state，它需要匹配 ITodo 类型来满足编译器的要求。这就是我们将它传递给 useState hook 的原因。我们还需要添加一个替代类型({})，因为初始状态是个空对象。</p>
<p>有了这些，我们现在可以继续下一步，展示获取的数据。</p>
<h3 id="todo"><strong>展示 Todo</strong></h3>
<ul>
<li>components/TodoItem.tsx</li>
</ul>
<pre><code class="language-jsx">import React from "react"
type Props = TodoProps &amp; {
  updateTodo: (todo: ITodo) =&gt; void
  deleteTodo: (_id: string) =&gt; void
}
const Todo: React.FC&lt;Props&gt; = ({ todo, updateTodo, deleteTodo }) =&gt; {
  const checkTodo: string = todo.status ? line-through : ""
  return (
    &lt;div className="Card"&gt;
      &lt;div className="Card--text"&gt;
        &lt;h1 className={checkTodo}&gt;{todo.name}&lt;/h1&gt;
        &lt;span className={checkTodo}&gt;{todo.description}&lt;/span&gt;
      &lt;/div&gt;
      &lt;div className="Card--button"&gt;
        &lt;button
          onClick={() =&gt; updateTodo(todo)}
          className={todo.status ? hide-button : "Card--button__done"}
        &gt;
          Complete
        &lt;/button&gt;
        &lt;button
          onClick={() =&gt; deleteTodo(todo._id)}
          className="Card--button__delete"
        &gt;
          Delete
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

</code></pre>
<p>这里，我们需要继承 <code>TodoProps</code>  类型并加入 <code>updateTodo</code>  和  <code>deleteTodo</code>  函数，作为 props 传递给组件。</p>
<p>现在，当传入 Todo 对象，我们将能够显示它并更新或删除 Todo。</p>
<p>太棒了！现在我们可以到 <code>App.tsx</code> 文件并把最后一块拼图放进去。</p>
<h3 id="">获取和展示数据</h3>
<ul>
<li>App.tsx</li>
</ul>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react'
import TodoItem from './components/TodoItem'
import AddTodo from './components/AddTodo'
import { getTodos, addTodo, updateTodo, deleteTodo } from './API'

const App: React.FC = () =&gt; {
  const [todos, setTodos] = useState&lt;ITodo[]&gt;([])

  useEffect(() =&gt; {
    fetchTodos()
  }, [])

  const fetchTodos = (): void =&gt; {
    getTodos()
    .then(({ data: { todos } }: ITodo[] | any) =&gt; setTodos(todos))
    .catch((err: Error) =&gt; console.log(err))
  }

</code></pre>
<p>这里，我们首先导入组件和 <code>API.ts</code> 导出的函数。然后，我们传递 <code>ITodo</code>  类型的数组给 <code>useState</code> 并且把它初始化为空数组。</p>
<p><code>getTodos()</code> 方法会返回 promise —— 因此，我们可以调用 then 函数并用获取到的数据更新 state，或者在发生任何错误时抛出一个错误。</p>
<p>有了这些，我们现在可以在组件组件成功挂载之后，调用 <code>fetchTodos()</code> 函数。</p>
<ul>
<li>App.tsx</li>
</ul>
<pre><code class="language-jsx">const handleSaveTodo = (e: React.FormEvent, formData: ITodo): void =&gt; {
  e.preventDefault()
  addTodo(formData)
    .then(({ status, data }) =&gt; {
      if (status !== 201) {
        throw new Error("Error! Todo not saved")
      }
      setTodos(data.todos)
    })
    .catch(err =&gt; console.log(err))
}

</code></pre>
<p>当发送表单时，我们用 <code>addTodo()</code> 向服务端发送请求。如果 Todo 被成功保存，我们将更新数据，否则将会抛出错误。</p>
<ul>
<li>App.tsx</li>
</ul>
<pre><code class="language-jsx">const handleUpdateTodo = (todo: ITodo): void =&gt; {
  updateTodo(todo)
    .then(({ status, data }) =&gt; {
      if (status !== 200) {
        throw new Error("Error! Todo not updated")
      }
      setTodos(data.todos)
    })
    .catch(err =&gt; console.log(err))
}

</code></pre>
<p>更新和删除 Todo 函数是很类似的。它们都接受参数，发送请求并得到响应，然后它们会检查请求是否成功并作相应处理。</p>
<ul>
<li>App.tsx</li>
</ul>
<pre><code class="language-jsx">  return (
    &lt;main className='App'&gt;
      &lt;h1&gt;My Todos&lt;/h1&gt;
      &lt;AddTodo saveTodo={handleSaveTodo} /&gt;
      {todos.map((todo: ITodo) =&gt; (
        &lt;TodoItem
          key={todo._id}
          updateTodo={handleUpdateTodo}
          deleteTodo={handleDeleteTodo}
          todo={todo}
        /&gt;
      ))}
    &lt;/main&gt;
  )
}

</code></pre>
<p>这里我们遍历  <code>todos</code>  数组并将所需的数据传递给 <code>TodoItem</code>。</p>
<p>现在，如果你打开服务器端应用程序的文件夹（并在终端中执行以下命令）：</p>
<pre><code class="language-shell">yarn start

</code></pre>
<p>在客户端也如此:</p>
<pre><code class="language-shell">yarn start

</code></pre>
<p>你应该能看到我们的 Todo 应用程序会按预期工作。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2021/10/todo-image.png" alt="todo app image" width="868" height="669" loading="lazy"></p>
<p>太棒了！最后，我们使用 TypeScript、React、NodeJs、Express 和 MongoDB 完成了一个 Todo 应用程序的构建。</p>
<p>附上 <a href="https://github.com/ibrahima92/fullstack-typescript-mern-todo">源代码</a>。</p>
<p>你可以在我的 <a href="https://www.ibrahima-ndaw.com/">博客</a> 上找到类似的内容，或者在 <a href="https://twitter.com/ibrahima92_">Twitter</a> 上关注我以获得相关的信息。</p>
<p>谢谢阅读。</p>
<h2 id="resources">Resources</h2>
<p><a href="https://github.com/typescript-cheatsheets/react-typescript-cheatsheet">React TypeScript Cheatsheet</a></p>
<p><a href="https://www.ibrahima-ndaw.com/blog/advanced-typescript-cheat-sheet/">Advanced TypeScript Types cheatsheet (with examples)</a></p>
<p><a href="https://github.com/typescript-cheatsheets">TypeScript Cheatsheets</a></p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/how-to-build-a-todo-app-with-react-typescript-nodejs-and-mongodb/">How to Build a Todo App with React, TypeScript, NodeJS, and MongoDB</a>，作者：<a href="https://www.freecodecamp.org/news/author/ibrahima92/">Ibrahima Ndaw</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 GitHub Pages，10 分钟搭建静态网页 ]]>
                </title>
                <description>
                    <![CDATA[ 静态网站的应用十分广泛，因为它具有明显的优势——页面响应速度快，而且随着越来越多的托管服务支持，搭建起来很容易。 我不打算在这篇文章里讨论“谁在什么时候，在哪里，选用静态网站做什么”或者“为什么选用它”。我假定你对上述问题至少有一个模糊的概念，或者只是想要搭建你自己的网站，并不在意其他细节——那么，这篇文章正是写给你这样的读者的。 首先，我想说的是我写这篇文章是为了给尽可能多的读者看的，所以你不需要任何编程知识，但是熟悉一些命令行和Git操作会帮到你很多。 所以怎么在10分钟内用 GitHub 创建一个静态网站？ 我们将用到两个特定工具：GitHub Pages，专门提供静态内容服务，另一个 Jekyll，用来生成静态内容。 Jekyll 是一个用来简单创建静态站点的 Ruby gem，所以如果你想用 Jekyll, 需要在你的电脑上安装 Ruby。如果你是 OSX 的操作系统，你很可能已经有某个版本Ruby（尽管你可能需要更新它）。如果你没有安装，或者你使用的是 windows 的电脑，你可以在这里了解更多关于安装它的信息:Installing Ruby [https://w ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-a-free-static-site-with-github-pages-in-10-minutes/</link>
                <guid isPermaLink="false">5edf4c47db4be8080eb70d7f</guid>
                
                <dc:creator>
                    <![CDATA[ cyan.wu ]]>
                </dc:creator>
                <pubDate>Tue, 09 Jun 2020 08:54:43 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/06/123.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>静态网站的应用十分广泛，因为它具有明显的优势——页面响应速度快，而且随着越来越多的托管服务支持，搭建起来很容易。</p><p>我不打算在这篇文章里讨论“谁在什么时候，在哪里，选用静态网站做什么”或者“为什么选用它”。我假定你对上述问题至少有一个模糊的概念，或者只是想要搭建你自己的网站，并不在意其他细节——那么，这篇文章正是写给你这样的读者的。</p><p>首先，我想说的是我写这篇文章是为了给尽可能多的读者看的，所以你不需要任何编程知识，但是熟悉一些命令行和Git操作会帮到你很多。</p><h2 id="-10-github-">所以怎么在10分钟内用 GitHub 创建一个静态网站？</h2><p>我们将用到两个特定工具：GitHub Pages，专门提供静态内容服务，另一个 Jekyll，用来生成静态内容。</p><p>Jekyll 是一个用来简单创建静态站点的 Ruby gem，所以如果你想用 Jekyll, 需要在你的电脑上安装 Ruby。如果你是 OSX 的操作系统，你很可能已经有某个版本Ruby（尽管你可能需要更新它）。如果你没有安装，或者你使用的是 windows 的电脑，你可以在这里了解更多关于安装它的信息: &nbsp;<a href="https://www.ruby-lang.org/en/documentation/installation/">Installing Ruby</a>。</p><p>做完上述操作后，打开新的终端窗口，输入<code>gem install bundler jekyll</code>。安装 Bundler（Ruby 的包管理工具）和 Jekyll。</p><p>当安装完这些 gems（Ruby 包）后，输入<code>Jekyll new my-static-site</code>(随意给它命名),这个命令会运行 Jekyll 的生成器，在一个新目录里创建你的项目，在网站创建站点后，通过输入 &nbsp;<code>cd my-static-site</code>（或者 &nbsp;<code>cd</code> &nbsp;你项目的名字）进入到新创建网站的目录。</p><p>在文本编辑器打开项目，你会看到Jekyll为你创建的几个文件和文件夹。现在你只需要关心 Gemfile (不是 Gemfile.lock)，Gemfile 是一个 Ruby 文件，它管理运行一个项目所需的所有相关Ruby包。</p><p>这个文件有一行写着 Jekyll 的版本，把这行注释掉：</p><pre><code class="language-ruby">#gem "jekyll", "~&gt; 4.0.0"
</code></pre><p>然后加入这一行：</p><pre><code class="language-ruby">gem "github-pages", group: :jekyll_plugins
</code></pre><p>当你安装 GitHub Pages gem 时，可能会出现有很多问题，有时于它所依赖的 gem 已经过时了，或者你本地安装的 gem 版本太新。</p><p>我发现这使得在本地构建和测试Jekyll站点变得很困难。一个简单的方法是，只在本地测试站点并保存其构建直到你准备部署之前。然而，通过以下代码，你可以在Gemfile中指定这些依赖项版本，Jekyll 就可以同时在本地和 GitHub Pages上运行。</p><pre><code class="language-ruby">gem "jekyll", "~&gt; 3.8.5"
gem "github-pages","~&gt; 202" , group: :jekyll_plugins
group :jekyll_plugins do
  gem "jekyll-feed", "~&gt; 0.11.0"
end
</code></pre><p>感谢 &nbsp;<a href="https://stackoverflow.com/users/6885157/alex-waibel">Alex Waibel</a> &nbsp;在 &nbsp;<a href="https://stackoverflow.com/questions/58598084/how-does-one-downgrade-jekyll-to-work-with-github-pages">StackOverflow</a> &nbsp;提供的最新配置。</p><p>为了看到你的网站，在命令行输入 &nbsp;<code>bundle exec Jekyll serve</code> &nbsp;这会开启一个服务器，你可以用通过在浏览器的 url 栏上输入 "localhost:4000" 看到你的网站。</p><p>瞧！你已经在项目目录里通过 Jekyll 创建一个网站。你已经完成了50%。</p><h2 id="-">让我们把它放到网上</h2><p>打开 GitHub.com 并注册一个账号，或者你已经有一个账号，选择 new 按钮并创建一个新的仓库，这个仓库的命名很重要，之后你的 GitHub Pages 链接就是你的用户名 .github.io 。例如我的 github 用户名是 tfantina，我的博客就是 &nbsp;<a href="https://tfantina.github.io/">tfantina.github.io</a>，所以我的 GitHub 仓库名是: "tfantina.github.io".</p><p>回到你的终端窗口，输入下面的命令，把你电脑的 Jekyll 网站上传到 Github 。</p><pre><code class="language-shell">git init
git remote add origin git@github.com:&lt;your_github_username&gt;/&lt;your_github_repo_name&gt;.git
git commit -am “Setting up Jekyll!”
git push -u origin master

</code></pre><p>(当替换你的用户名和项目名时，你不需要左右尖括号&lt;&gt;).</p><p>一旦你的修改提交到你的Github仓库，你就有一个正在运行的静态页面，这是因为你正在用GitHub Pages gem，并用GitHub理解的命名方式命名仓库。</p><p>你可以通过访问你的站点或者在GitHub的仓库设置页并滑到页面部分来确认这件事，你应该会看到一个绿色的方框，显示你的站点已经被发布了。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/06/image-11.png" class="kg-image" alt="image-11" width="600" height="400" loading="lazy"></figure><p>你也将会注意到很容易在这里就改变页面的主题，GitHub为Jekyll提供一些默认主题，当然你可以自己设计。如果你的站点已经发布但是看起来是空白页，你可能需要强制更新或者尝试在一个私隐窗口打开你的网站。这个可能看上去像是废话，但几乎每次我设置一个新的Jekyll实例都这样。</p><p>如果一切按计划进行，你的网站应该是这样的：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/06/image-10.png" class="kg-image" alt="image-10" width="600" height="400" loading="lazy"></figure><p>就这样，在这几分钟里你已经在 GitHub 创建并部署一个静态网站。但是，你可能想在页面放一些内容。</p><p>我承诺过只会花费10分钟，因此，我不会深入到页面、front matter或 Liquid 模板语言的所有细节，我会在之后再发一篇文章。但是我会分享如何创建你的第一篇文章。</p><p>回到文本编辑器，打开 “_posts” 文件夹，这里已经有一篇欢迎你到新博客的文章。创建一个新的 markdown 文件，并且按这种格式 YEAR-MONTH-DAY-TITLE.markdown 命名并保存它（见下文）：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/06/image-12.png" class="kg-image" alt="image-12" width="600" height="400" loading="lazy"></figure><p>Jekyll 文章包含两个部分 front matter 和 body。</p><p>front matter 给了 Jekyll 一些具体的指示，比如文章的标题是什么，使用什么布局，以及文章是什么时候写的。</p><p>Front matter 是高度可配置的。比如，我想我的帖子有英雄的图片，所以我创建 &nbsp;<code>lead_image</code> &nbsp;标签，并在我的布局放一些语法，它会根据每个帖子的 front matter 来显示对应的 lead images。Liquid 模板语言能轻松地根据 front matter ，把内容放到你的主题。</p><p>你还可以在 front matter 配置更多，但是让我们从一个通用的示例开始。</p><p>一个默认的 front matter 像这样：</p><pre><code class="language-gfm">—
layout: post 
title:  "Welcome to Jekyll!"
date:   2019-11-09 18:07:11 -0600
categories: jekyll update
—

</code></pre><ul><li>布局告诉Jekyll用那种布局来显示你的内容。你可以为不同的页面或帖子类型设置多个布局。</li><li>文章标题</li><li>文章日期</li><li>分类，本质上是标签，可以添加任意数量的标签(以空格分隔)</li></ul><p>在配置好 front matter 之后，用 &nbsp;<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">Markdown</a> &nbsp;写帖子会获得很大的灵活性。</p><p>当你写完并保存之后，打开终端窗口。</p><pre><code class="language-shell">git commit -am “Publishes first post
git push
</code></pre><p>在一分钟之后(也许刷新一下)，你就可以看到你的帖子了。</p><p>希望你现在已经在使用 Jekyll 创建的 GitHub Pages 上有了一个可以工作的静态站点，如果你有任何疑问，请发推特 &nbsp;<a href="https://twitter.com/tfantina">@tfantina</a>，或者你可以给我发邮件<a>contact@travisfantina.com</a>。</p><p>原文：<a href="https://www.freecodecamp.org/news/create-a-free-static-site-with-github-pages-in-10-minutes/">How to create a free static site with GitHub Pages in 10 minutes</a>，作者：Travis Fantina</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
