写在最前面
- 如果没看前面的 node server篇 和 mongoDB database篇 ,可以先看看,这篇是结合上面两篇一起学习的文章
您可以按照顺序阅读
- 全栈 Todolist-server 篇 Node(server) React(client) MongoDB(database) Typescript
- Todolist-database 篇(Cloud MongoDB)
- Todolist-client 篇(React Typescript)
1、创建一个 react app(源码代码参考)
- 接着上篇的项目(项目之间相互不影响,也可以单独部署)
- 在 server 文件夹平行目录下,直接使用
create-react-app
的typescript
模板来创建。
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> => {
try {
const todos: AxiosResponse = 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> => {
try {
const todo: Omit"_id"> = {
name: formData.name,
description: formData.description,
status: false,
}
const saveTodo: AxiosResponse = 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> => {
try {
const todoUpdate: Pick"status"> = {
status: true,
}
const updatedTodo: AxiosResponse = 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> => {
try {
const deletedTodo: AxiosResponse = 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 = ({ saveTodo }) => {
const [formData, setFormData] = useState()
const handleForm = (e: React.FormEvent): void => {
setFormData({
...formData,
[e.currentTarget.id]: e.currentTarget.value,
})
}
return (
<form className='Form' onSubmit={(e) => saveTodo(e, formData)}>
<div>
<div>
<label htmlFor='name'>Namelabel>
<input onChange={handleForm} type='text' id='name' />
div>
<div>
<label htmlFor='description'>Descriptionlabel>
<input onChange={handleForm} type='text' id='description' />
div>
div>
<button disabled={formData === undefined ? true: false} >Add Todobutton>
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 = ({ 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([])
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 Todosh1>
<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 汇集了最终的完善的代码。
7、bugfix
mongoDB bug(MongoError: Authentication failed)
- 检查密码,用户名,数据库名是否有误
- 观察 clound mongoDB 的集群(Clusters) 观察是否正常 connected
8、启动成功后,咋们再优化一下样式
- 最后呈现,观察接口数据
- 也可以观察集群的具体数据,点击 METRICS 还有更详细的图表,connect 大于 1 表示连接成功。
@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;
}
参考
- create-react-app
- freeCodeCamp