停止编写 API 函数
如果您正在开发使用带有 RESTFUL API 的后端的前端应用程序,那么您必须停止为每个端点编写函数!
RESTFUL API 通常会提供一组端点,用于对不同的实体执行 CRUD(创建、读取、更新、删除)操作。我们的项目中通常会为每个端点提供一个函数,这些函数的功能非常相似,只是针对不同的实体。例如,假设我们有以下函数:
// apis/users.js
// Create
export function createUser(userFormValues) {
return fetch("/users", { method: "POST", body: userFormValues });
}
// Read
export function getListOfUsers(keyword) {
return fetch(`/users?keyword=${keyword}`);
}
export function getUser(id) {
return fetch(`/users/${id}`);
}
// Update
export function updateUser(id, userFormValues) {
return fetch(`/users/${id}`, { method: "PUT", body: userFormValues });
}
// Destroy
export function removeUser(id) {
return fetch(`/users/${id}`, { method: "DELETE" });
}
对于其他实体,例如City
,,, ...,可能也存在类似的函数集。但是,我们可以用一个简单的函数调用来替换所有这些函数:Product
Category
// apis/users.js
export const users = crudBuilder("/users");
// apis/cities.js
export const cities = crudBuilder("/regions/cities");
然后像这样使用它:
users.create(values);
users.show(1);
users.list("john");
users.update(values);
users.remove(1);
“但是为什么呢?”你可能会问
嗯,有一些很好的理由:
- 它减少了代码行数——您必须编写的代码以及当您离开公司时其他人必须维护的代码。
- 它强制 API 函数的命名约定,从而提高代码的可读性和可维护性。您可能见过诸如
getListOfUsers
、getCities
、getAllProducts
、等函数名productIndex
,fetchCategories
它们的作用都是一样的:“获取实体列表”。采用这种方法,您将始终拥有entityName.list()
函数,并且团队中的每个人都知道这一点。
因此,让我们创建该crudBuilder()
函数,然后为其添加一些修饰。
一个非常简单的 CRUD 构建器
对于上面的简单示例,该crudBuilder
函数非常简单:
export function crudBuilder(baseRoute) {
function list(keyword) {
return fetch(`${baseRoute}?keyword=${keyword}`);
}
function show(id) {
return fetch(`${baseRoute}/${id}`);
}
function create(formValues) {
return fetch(baseRoute, { method: "POST", body: formValues });
}
function update(id, formValues) {
return fetch(`${baseRoute}/${id}`, { method: "PUT", body: formValues });
}
function remove(id) {
return fetch(`${baseRoute}/${id}`, { method: "DELETE" });
}
return {
list,
show,
create,
update,
remove
}
}
它假设 API 路径有一个约定,并给出一个实体的路径前缀,它返回在该实体上调用 CRUD 操作所需的所有方法。
但说实话,我们知道现实世界的应用并非如此简单!在将这种方法应用于我们的项目时,有很多事情需要考虑:
- 过滤:列表 API 通常会获得大量过滤参数
- 分页:列表总是分页
- 转换: API 提供的值在实际使用之前可能需要进行一些转换。
- 准备:对象
formValues
在发送到 API 之前需要进行一些准备 - 自定义端点:更新特定项目的端点并不总是
`${baseRoute}/${id}`
因此,我们需要一个可以处理更复杂情况的 CRUD 构建器。
高级 CRUD 构建器
让我们通过解决上述问题来构建一些我们可以在日常项目中真正使用的东西。
过滤
首先,我们应该能够在 outlist
函数中处理更复杂的过滤。每个实体列表可能包含不同的过滤器,并且用户可能应用了其中一些。因此,我们无法对所应用过滤器的形状或值做出任何假设,但我们可以假设任何列表过滤都会生成一个对象,该对象为不同的过滤器名称指定一些值。例如,为了过滤某些用户,我们可以:
const filters = {
keyword: "john",
createdAt: new Date("2020-02-10"),
}
另一方面,我们不知道这些过滤器应该如何传递给 API,但我们可以假设(并与 API 提供商签订合同)每个过滤器在列表 API 中都有一个相应的参数,可以以"key=value"
URL 查询参数的形式传递。
因此,我们需要知道如何将应用的过滤器转换为相应的 API 参数来创建list
函数。这可以通过向 传递transformFilters
参数来实现crudBuilder()
。以下是用户的示例:
function transformUserFilters(filters) {
const params = []
if(filters.keyword) {
params.push(`keyword=${filters.keyword}`;
}
if(filters.createdAt) {
params.push(`created_at=${dateUtility.format(filters.createdAt)}`;
}
return params;
}
现在,我们可以使用此参数来创建list
函数。
export function crudBuilder(baseRoute, transformFilters) {
function list(filters) {
let params = transformFilters(filters)?.join("&");
if(params) {
params += "?"
}
return fetch(`${baseRoute}${params}`);
}
}
转换和分页
从 API 接收的数据可能需要进行一些转换才能在我们的应用中使用。例如,我们可能需要将snake_case
名称转换为用户时camelCase
区,或者将某些日期字符串转换为用户时区等等。
此外,我们还需要处理分页。
假设来自 API 的所有分页数据都具有以下形状(由 API 提供商标准化):
{
data: [], //list of entity objects
pagination: {...}, // the pagination info
}
所以我们需要知道如何转换单个实体对象。然后,我们可以循环遍历列表中的对象来转换它们。为此,我们需要一个transformEntity
函数作为参数crudBuilder
:
export function crudBuilder(baseRoute, transformFilters, transformEntity) {
function list(filters) {
const params = transformFilters(filters)?.join("&");
return fetch(`${baseRoute}?${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination,
}));
}
}
至此,我们的功能就完成了list()
。
准备
对于create
和update
函数,我们需要将 转换formValues
为 API 所需的格式。例如,假设我们的表单中有一个城市选择框,用于选择一个City
对象。但创建 API 只需要city_id
。因此,我们可以编写一个函数来执行如下操作:
const prepareValue = formValues => ({city_id: formValues.city.id})
此函数可能返回一个普通对象或一个FormData
取决于用例的对象,并可用于将数据传递给 API,例如:
export function crudBuilder(
baseRoute,
transformFilters,
transformEntity,
prepareFormValues
) {
function create(formValues) {
return fetch(baseRoute, {
method: "POST",
body: prepareFormValues(formValues),
});
}
}
自定义端点
在一些罕见的情况下,针对实体执行某些操作的 API 端点并不遵循相同的约定。例如,我们不必`/users/${id}`
编辑用户,而是必须使用`/edit-user/${id}`
。对于这些情况,我们应该能够指定自定义路径。
这里,我们允许覆盖 CRUD 构建器中使用的任何路径。需要注意的是,显示、更新和删除操作的路径可能依赖于实体对象的某些信息,因此我们必须使用函数并传递实体对象来获取路径。
我们需要在一个对象中获取这些自定义路径,如果未指定任何路径,则返回默认路径。例如:
const paths = {
list: "list-of-users",
show: (userId) => `users/with/id/${userId}`,
create: "users/new",
update: (user) => `users/update/${user.id}`,
remove: (user) => `delete-user/${user.id}`
}
最终的 CRUD 构建器
这是创建 CRUD API 函数的最终代码。
export function crudBuilder(
baseRoute,
transformFilters,
transformEntity,
prepareFormValues,
paths
) {
function list(filters) {
const path = paths.list || baseRoute;
let params = transformFilters(filters)?.join("&");
if(params) {
params += "?"
}
return fetch(`${path}${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination,
}));
}
function show(id) {
const path = paths.show?.(id) || `${baseRoute}/${id}`;
return fetch(path)
.then((res) => res.json())
.then((res) => transformEntity(res));
}
function create(formValues) {
const path = paths.create || baseRoute;
return fetch(path, {
method: "POST",
body: prepareFormValues(formValues),
});
}
function update(id, formValues) {
const path = paths.update?.(id) || `${baseRoute}/${id}`;
return fetch(path, { method: "PUT", body: formValues });
}
function remove(id) {
const path = paths.remove?.(id) || `${baseRoute}/${id}`;
return fetch(path, { method: "DELETE" });
}
return {
list,
show,
create,
update,
remove
}
}