[译]全栈 Todolist-client 篇(React Typescript)

1,933 阅读4分钟

写在最前面

您可以按照顺序阅读

1、创建一个 react app(源码代码参考

  • 接着上篇的项目(项目之间相互不影响,也可以单独部署)
  • 在 server 文件夹平行目录下,直接使用 create-react-apptypescript 模板来创建。
npx create-react-app client --template typescript
// npm 也可以

/**
* 除了调用项目内部模块,npx 还能避免全局安装的模块。
* 比如,create-react-app这个模块是全局安装,npx 可以运行它,
* 而且不进行全局安装。
*/
  • 打开 client
cd client
  • 然后是安装 axios
yarn add axios
  • 等待安装好以后,我们来构建我们的目录,如下
├── 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

现在最重要的事儿,我们需要提前定义todolist 的 types,我们把他放在 type.d.ts

2、构建 types

  • src/type.d.ts
interface ITodo {
  _id: string
  name: string
  description: string
  status: boolean
  createdAt?: string
  updatedAt?: string
}

interface TodoProps {
  todo: ITodo
}

type ApiDataType = {
  message: string
  status: string
  todos: ITodo[]
  todo?: ITodo
}

代码所示,我们部署 todo 的整体内容的 interface,基本包括 id,name,description 等。为了方便 api 的获取,这边定义新的一条 todo 和旧数据 todos

3、构建请求接口的 API(源码参考

  • src/API.ts
import axios, { AxiosResponse } from "axios"

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

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

这里暂且写死 api 请求的地址和 server 端地址的保持一致。

  • src/API.ts
export const addTodo = async (
  formData: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
  try {
    const todo: Omit<ITodo, "_id"> = {
      name: formData.name,
      description: formData.description,
      status: false,
    }
    const saveTodo: AxiosResponse<ApiDataType> = await axios.post(
      baseUrl + "/add-todo",
      todo
    )
    return saveTodo
  } catch (error) {
    throw new Error(error)
  }
}

这是添加一条 todolist 的函数,根据 id 来定位。

  • src/API.ts
export const updateTodo = async (
  todo: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
  try {
    const todoUpdate: Pick<ITodo, "status"> = {
      status: true,
    }
    const updatedTodo: AxiosResponse<ApiDataType> = await axios.put(
      `${baseUrl}/edit-todo/${todo._id}`,
      todoUpdate
    )
    return updatedTodo
  } catch (error) {
    throw new Error(error)
  }
}

这是完成 todolist 的函数,我们把状态 status 置为 true

  • src/API.ts
export const deleteTodo = async (
  _id: string
): Promise<AxiosResponse<ApiDataType>> => {
  try {
    const deletedTodo: AxiosResponse<ApiDataType> = await axios.delete(
      `${baseUrl}/delete-todo/${_id}`
    )
    return deletedTodo
  } catch (error) {
    throw new Error(error)
  }
}

这是删除函数,传 id 来删除相关的 list

4、完成基础组件和展示页面(源码参考)

  • 添加一个有增加功能的基础组件
  • components/AddTodo.tsx
import React, { useState } from 'react'

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

const AddTodo: React.FC<Props> = ({ saveTodo }) => {
  const [formData, setFormData] = useState<ITodo | {}>()

  const handleForm = (e: React.FormEvent<HTMLInputElement>): void => {
    setFormData({
      ...formData,
      [e.currentTarget.id]: e.currentTarget.value,
    })
  }

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

export default AddTodo

我们把 todoItem 单独拆分出来

  • components/TodoItem.tsx
import React from "react"

type Props = TodoProps & {
  updateTodo: (todo: ITodo) => void
  deleteTodo: (_id: string) => void
}

const Todo: React.FC<Props> = ({ todo, updateTodo, deleteTodo }) => {
  const checkTodo: string = todo.status ? `line-through` : ""
  return (
    <div className="Card">
      <div className="Card--text">
        <h1 className={checkTodo}>{todo.name}</h1>
        <span className={checkTodo}>{todo.description}</span>
      </div>
      <div className="Card--button">
        <button
          onClick={() => updateTodo(todo)}
          className={todo.status ? `hide-button` : "Card--button__done"}
        >
          Complete
        </button>
        <button
          onClick={() => deleteTodo(todo._id)}
          className="Card--button__delete"
        >
          Delete
        </button>
      </div>
    </div>
  )
}

export default Todo

5、初始化数据和展示页面

  • App.tsx
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 = () => {
  const [todos, setTodos] = useState<ITodo[]>([])

  useEffect(() => {
    fetchTodos()
  }, [])

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

fetchTodos 获取数据库中初始的数据

  • App.tsx
const handleSaveTodo = (e: React.FormEvent, formData: ITodo): void => {
  e.preventDefault()
  addTodo(formData)
    .then(({ status, data }) => {
      if (status !== 201) {
        throw new Error("Error! Todo not saved")
      }
      setTodos(data.todos)
    })
    .catch(err => console.log(err))
}

handleSaveTodo 就是添加一条新的 list

  • App.tsx
const handleUpdateTodo = (todo: ITodo): void => {
  updateTodo(todo)
    .then(({ status, data }) => {
      if (status !== 200) {
        throw new Error("Error! Todo not updated")
      }
      setTodos(data.todos)
    })
    .catch(err => console.log(err))
}

const handleDeleteTodo = (_id: string): void => {
  deleteTodo(_id)
    .then(({ status, data }) => {
      if (status !== 200) {
        throw new Error("Error! Todo not deleted")
      }
      setTodos(data.todos)
    })
    .catch(err => console.log(err))
}

依次加上完成和删除的函数。

  • App.ts
 return (
    <main className='App'>
      <h1>My Todos</h1>
      <AddTodo saveTodo={handleSaveTodo} />
      {todos.map((todo: ITodo) => (
        <TodoItem
          key={todo._id}
          updateTodo={handleUpdateTodo}
          deleteTodo={handleDeleteTodo}
          todo={todo}
        />
      ))}
    </main>
  )
}
export default App

最后返回我们的 todolist,导出 App

6、启动(源码参考)

  • 启动 client 端
yarn start
  • 打开 server 端,启动 server 端
yarn start

尝试操作 todolist,增删改

  • 最终的代码可以按照这个顺序查看,1-5的顺序查看,master 汇集了最终的完善的代码。 Bsnz6K.png

7、bugfix

mongoDB bug(MongoError: Authentication failed)

  • 检查密码,用户名,数据库名是否有误
  • 观察 clound mongoDB 的集群(Clusters) 观察是否正常 connected

8、启动成功后,咋们再优化一下样式

  • 最后呈现,观察接口数据 Bsm0rF.md.jpg

  • 也可以观察集群的具体数据,点击 METRICS 还有更详细的图表,connect 大于 1 表示连接成功。 BsnFzV.md.jpg

@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap');

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Nunito', sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #fff;
    background: #333;
}

.App {
    max-width: 728px;
    margin: 4rem auto;
}

.App > h1 {
    text-align: center;
    margin: 1rem 0;
}

.Card {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: #444;
    padding: 0.5rem 1rem;
    border-bottom: 1px solid #333333;
}

.Card--text h1 {
    color: #ff9900;
}

.Card--button button {
    background: #f5f6f7;
    padding: 0.4rem 1rem;
    border-radius: 20px;
    cursor: pointer;
}

.Card--button__delete {
    border: 1px solid #ca0000;
    color: #ca0000;
}

.Card--button__done {
    border: 1px solid #00aa69;
    color: #00aa69;
    margin-right: 1rem;
}

.Form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    background: #444;
    margin-bottom: 1rem;
}

.Form > div {
    display: flex;
    justify-content: center;
    align-items: center;
}

.Form input {
    background: #f5f6f7;
    padding: 0.5rem 1rem;
    border: 1px solid #ff9900;
    border-radius: 10px;
    display: block;
    margin: 0.3rem 1rem 0 0;
}

.Form label {
}

.Form button {
    background: #ff9900;
    color: #fff;
    padding: 0.5rem 1rem;
    border-radius: 20px;
    cursor: pointer;
    border: none;
}

.line-through {
    text-decoration: line-through;
    color: #777 !important;
}

.hide-button {
    display: none;
}

参考