首頁 > 軟體

停止編寫 API函數原因範例分析

2022-09-30 14:01:38

正文

RESTFUL API 通常提供在不同實體上執行增刪改查(CRUD)操作的一組介面。我們通常在我們的前端專案中為這些每一個介面提供一個函數,這些函數的功能非常的相似,只是為了服務於不用的實體。舉個例子,假設我們有這些函數。

// api/users.js
// 建立
export function createUser(userFormValues) {
  return fetch('users', { method: 'POST', body: userFormValues });
}
// 查詢
export function getListOfUsers(keyword) {
  return fetch(`/users?keyword=${keyword}`);
}
export function getUser(id) {
  return fetch(`/users/${id}`);
}
// 更新
export updateUser(id, userFormValues) {
  return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
}
// 刪除
export function removeUser(id) {
  return fetch(`/users/${id}`, { method: 'DELETE' });
}

類似的功能可能存在於其他實體,例如:城市、產品、類別...但是我們可以用一個簡單的函數呼叫來代替這些函數:

// 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 總是分頁的
  • 轉換:API 返回的值在實際使用之前可能需要進行一些轉換
  • 準備:formValues物件在傳送給 API 之前需要做一些準備工作
  • 自定義介面:更新特定項的介面不總是${baseRoute}/${id}

因此,我們需要可以處理更多複雜場景的 CRUD 構造器。

高階 CRUD 構造器

讓我們通過上述方法來構建一些日常中我們真正使用的東西。

過濾

首先,我們應該在 list輸出函數中處理更加複雜的過濾。每個實體列表可能有不同的過濾器並且使用者可能應用了其中的一些過濾器。因此,我們不能對應用過濾器的形狀和值有任何假設,但是我們可以假設任何列表過濾都可以產生一個物件,該物件為不同的過濾器名稱指定了一些值。例如,我們可以過濾一些使用者:

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(`create_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 轉換成駝峰命名或將一些日期字串轉換成使用者時區。

此外,我們還需要處理分頁。

我們假設來自 API 的分頁資料都按照如下格式(與 API 提供者約定):

{
  data: [], // 實體物件列表
  pagination: {...} // 分頁資訊
}

因此,我們需要知道如何轉換單個實體物件。然後我們可以遍歷列表物件來轉換他們。為此,我們需要一個 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() 函數我們就完成了。

準備

對於 createupdate 函數,我們需要將 formValues 轉換成 API 需要的格式。例如,假設我們在表單中有一個 City 的城市選擇物件。但是 create API 只需要 city_id。因此,我們需要一個執行以下操作的函數:

const prepareValue = formValue => ({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 builder 中使用的任何路徑。注意,展示、更新、移除操作的路徑可能取決於具體實體物件的資訊,因此我們必須使用函數並傳遞實體物件來獲取路徑。

我們需要在物件中獲取這些自定義路徑,如果沒有指定,就退回到預設路徑。像這樣:

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}`
};

最終的 BRUD 構造器

這是建立 CRUD 函數的最終程式碼。

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(() => ({
        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
  }
}

Saeed Mosavat: Stop writing API functions

以上就是停止編寫 API函數原因範例分析的詳細內容,更多關於停止編寫 API 函數的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com