前端所有代码已经同步到 GitHub Repo: EthanLuu/soomooc: React + TypeScript 实践,在线教学直播平台。 (github.com)

Mock 说明

在当前这个前后端开发分离的时代,作为前端开发人员,自然是需要调用后端开发人员的写的接口来获取数据。例如为了获取一个课程的信息,在前端的项目中就需要如下操作:

1
2
3
4
5
const fetchData = async () => {
const response = await fetch('https://api.soomooc.com/course/1') // 虚构的api
const courseData = await response.json()
return courseData
}

但是问题就出现在这个接口上,我们并不能保证在需要获取课程数据的时候,后端的兄弟就已经写好了对应的 API。

因此,伟大的 Mock 便诞生了。Mock 字面意思是 「虚假」,因此 Mock 数据可以解释为「虚假的数据」。

其本质就是在前端开发的时候,我们通过一些手段来模拟数据的获取。既保证了开发的效率,又方便了产品的演示。

Mock 方式

代码侵入

把所有的数据写死在代码中,或者把所有数据存放在本地的 JSON 文件中,需要的时候调用。

1
2
3
4
5
6
7
8
const fetchData = () => {
return new Promise((resolve) => {
resolve({
id: '1',
courseName: '软件工程'
})
})
}

显然,这种最初级的方式效果很不好,上线的时候需要手动修改很多地方。

请求拦截

通过如 Mock.js 等插件,来根据模板和规则生成假数据。

以获取访客的接口为例,我们在服务器端通过 mock 方法可以返回对应伪造的访客数据。

1
2
3
4
5
6
7
8
9
10
11
Mock.mock(/\\/api\\/visitor\\/list/, 'get', {
code: 2000,
msg: 'ok',
'data|10': [
{
'id|+1': 6,
'name': '@csentence(5)',
'age': '@age(1, 100),
}
]
})

这个方法的缺点也很明显,那就是只能生成随机的假数据,不能真正地增删改查(是不是听起来像杠精发言)。另一个缺点就没这么明显,那就是这个数据接口不能通过 fetch 调用。

接口管理工具

利用 rap, swagger, moco, yapi 等工具直接管理接口。

更像是后端在直接帮我们先造临时接口,在前端开发缺少后端接口开发相关知识或者团队规模较小的时候就显得有些繁琐了。

本地 node 服务器

利用 json-server,在使用 JSON 存储数据的基础上,实现真实的增删改查。

并且 json-server 无需设计接口的地址,所有接口均可以 REST API 格式进行直接调用。

虽然也有缺点,就是这个 API 也会在前端写死,无法动态调整。

接下来我们就用这种方法来配置 SooMooc 吧。

json-server 配置

首先在本地安装包:

1
yarn add json-server -D

在项目根目录建立一个文件夹 __json_server_mock__,用于存放 mock 数据。

在文件夹中新建一个 db.json 文件,随便写点数据。

image-20210510220538952

1
2
3
{
"users": [{ "id": 1, "username": "ethan", "password": "123456" }]
}

为了便捷地打开 json-server 服务器,我们将启动命令写进 package.json 中。

code

在命令行输入:yarn mock,看到如下提示就说明新建好啦!

image-20210510221142934

这个时候,我们可以直接使用浏览器或者一些 API 测试工具试用一下。

我这边用的是 Postman

用 GET 方法,请求 localhost:3001/users,我们发现成功返回了刚刚手动输入的信息。

image-20210510221418268

这还只是 ,试试 呢?

用 POST 方法,请求 localhost:3001/users,同时把需要新增的数据以 json 的格式放到 body 里。

image-20210510221614450

返回的信息显示添加成功了!并且服务器还自动帮我们加上了 id。此时,我们惊喜地发现项目本地的 db.json 文件中也插入了这条信息。

image-20210510222347500

但是有没有发现,我们并没有自己写插入用户的方法,为啥 POST 一下就能新增数据呢?

这就是 json-server 的神奇之处了,正如我上文所说,他利用 REST API 的形式,提供了一套增删改查的接口,完全自动配置。

这意味着:

1
2
3
4
5
GET			/users				# 获取所有的用户信息
GET /users/?id=1 # 查询id=1的用户信息
POST /users # 通过在body里写需要新增的用户信息即可插入一条新数据
DELETE /users/1 # 删除id=1的用户信息
PUT /users/2 # 和POST类似在body里附加信息,即可实现用户信息更新

一套完整 API,超短时间配置,同时支持利用接口访问和本地文件访问,就问你香不香?

或许会有更优秀的 mock 数据方式出现,不过目前而言对于本项目而言,json-server 方式足矣。

项目结合

为了方便未来从开发到上线过程中 API 的修改,我们在项目根目录新建两个文件用于配置路由。

image-20210510223605342

.env 文件中,写入未来真实线上的 API 地址:

1
REACT_APP_API_URL=http://api.ethanloo.cn

.env.development 文件中,写入我们 json-server 的地址:

1
REACT_APP_API_URL=http://localhost:3001

添加一个便于 URL 查询的库

1
2
3
4
# queryString
yarn add qs
# 适配TS
yarn add @types/qs -D

编写一个用于 http 请求的函数,存放在 src/utils/http.ts 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import qs from 'qs'

interface Config extends RequestInit {
data?: object
}

const apiUrl = process.env.REACT_APP_API_URL

export const http = async (
endpoint: string,
{ data, headers, ...customConfig }: Config = {}
) => {
const config = {
method: 'GET',
headers: {
'Content-Type': data ? 'application/json' : '',
},
...customConfig,
}

if (config.method.toUpperCase() === 'GET') {
endpoint += `?${qs.stringify(data)}`
} else {
config.body = JSON.stringify(data || {})
}

return window
.fetch(`${apiUrl}/${endpoint}`, config)
.then(async (response) => {
const data = await response.json()
if (response.ok) {
return data
} else {
return Promise.reject(data)
}
})
}

再编写一个用户列表界面用来测试增删改查。

我这边标注成 jsx 是因为网页的代码高亮还不支持 tsx,实际的文件后缀名是 tsx

这个组件还有很多问题,比如:

  • 每次增或删之后会重新调取接口获取所有用户信息,然后渲染(正式项目中可以考虑使用乐观更新)
  • 因为不太会 TS,所以很不好地写了一个 users: any
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { Button, Card, Input, Form } from 'antd'
import { useEffect, useState } from 'react'
import styled from '@emotion/styled'
import { http } from 'utils/http'

interface UserProps {
id: number
username: 'string'
password: 'string'
}

export const UsersList = () => {
const [users, setUsers] = useState([])
const [form] = Form.useForm()

const renderUsers = () => {
http('users').then((users: any) => {
setUsers(users)
})
}

// 初次加载时渲染users
useEffect(() => {
renderUsers()
}, [])

const deleteUser = async (id: number) => {
const config = {
method: 'DELETE',
}
http(`users/${id}`, config).then(() => {
form.resetFields()
renderUsers()
})
}

const addUser = async (values: { username: string; password: string }) => {
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: values,
}
http('users', config).then(() => {
form.resetFields()
renderUsers()
})
}

return (
<Container>
<UsersContainer>
{users.map((user: UserProps) => (
<Card
title={`id:${user.id}`}
key={user.id}
style={{ width: 200, margin: 30 }}
extra={<Button onClick={() => deleteUser(user.id)}>x</Button>}
>
<p>username: {user.username}</p>
<p>password: {user.password}</p>
</Card>
))}
</UsersContainer>

<Form
onFinish={addUser}
form={form}
style={{
textAlign: 'center',
padding: '10px',
border: '1px solid #ccc',
boxShadow: '3px 3px 3px #ccc',
borderRadius: '5px',
}}
>
<Form.Item name={'username'}>
<Input placeholder={'用户名'} />
</Form.Item>
<Form.Item name={'password'}>
<Input placeholder={'密码'} />
</Form.Item>
<Form.Item>
<Button htmlType={'submit'} type={'primary'}>
注册
</Button>
</Form.Item>
</Form>
</Container>
)
}

const UsersContainer = styled.div`
font-size: 2rem;
height: 30vh;
width: 80%;
display: flex;
border: 1px solid #ccc;
box-shadow: 3px 3px 3px #ccc;
border-radius: 5px;
margin: 30px;
`

const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
`

效果如下:

动画 (1)

Thumbnail (1)-tuya