Fetch——从简单到可扩展的实现
嘿!👋
无聊透顶,想写点东西。最终我写成了这个,一个循序渐进的指南,教你如何完成一项任务,从最基础的到最符合需求的实现。
我将建造什么?
用于获取数据的代码片段。它将获取一个笑话 API,该 API 返回笑话列表或随机笑话。
然后我会尝试逐步改进它,直到拥有坚实且可扩展的基础。
语境
除了 API 之外什么都没有构建,任务是创建一种获取笑话的方法,以便 UI 团队可以开始做他们的工作。
初步实施
最简单的方案是创建一个函数,用于获取所有笑话,以及一个随机获取笑话的函数。很简单,我们来看看它是如何工作的:
function fetchAllJokes() {
return fetch('https://my-api.com/jokes')
.then(response => response.json());
}
function fetchRandomJoke() {
return fetch('https://my-api.com/jokes/random')
.then(response => response.json());
}
如你所见,这个方法可以立即生效,UI 团队可以立即开始工作。但它的可扩展性不太好,让我们看看如何在不影响 UI 团队工作的情况下进行改进。
迭代 1
我们知道目前我们只能获取笑话,但我们也知道这个 API 将来很可能会扩展。我们需要实现其他功能,比如创建/更新笑话,获取其他资源等等……
在我开始构建或设计一个功能之前,我会提醒自己一件事:
“我可以这样做,这样我就不用再接触这个代码了吗?”
大多数情况下答案是肯定的,通过使用开放-封闭原则,该原则指出,函数/方法/类应该对扩展开放,但对修改关闭。
我尝试应用的另一条规则是,循序渐进。我的意思是,从最简单的、最底层的功能开始,然后在此基础上进行构建。
在这种情况下,最低级别的功能是执行 fetch,并带有一组选项。因此,我首先围绕 fetch 定义一个自定义函数:
function fetcher(url, options = {}) {
return fetch(url, {
method: HttpMethods.GET,
...options,
});
}
它与直接调用 fetch 基本相同,但有一点不同:
-
它将调用 fetch 的位置集中起来,而不是在应用程序的几个地方直接调用 fetch,我们只在 fetcher 函数中使用它。
-
如果 fetch API 发生更改,或者我们想在每次 fetch 请求之前或之后执行某些操作,那么更改/修改会更容易。不过,如果可以避免,我会拒绝这样做,正如您在文章后面会看到的那样。
现在我们有了这个基础,就可以在此基础上进行构建了。让我们来实现最常见的 HTTP 方法,例如 POST、PUT、GET、DELETE。
function fetcherPost(url, options = {}) {
return fetcher(url, {
...options,
method: HttpMethods.POST,
});
}
function fetcherPut(url, options = {}) {
return fetcher(url, {
...options,
method: HttpMethods.PUT,
});
}
// ...
我想你已经明白了它的要点。我们为每个方法创建一个函数。
我们将按如下方式使用它:
function fetchAllJokes() {
return fetcherGet('https://my-api.com/jokes')
.then(response => response.json());
}
function fetchRandomJoke() {
return fetcherGet('https://my-api.com/jokes/random')
.then(response => response.json());
}
这还可以,但我们可以做得更好。
迭代 2
API uri 可能在所有请求中都相同,也可能在其他请求中也一样。因此,我们将其存储在一个环境变量中:
function fetchAllJokes() {
return fetcherGet(`${env.API_URL}/jokes`)
.then(response => response.json());
}
更好的是,现在你可以看到响应转换为 JSON 的过程也重复了。我们如何改进它?
首先,让我们看看如何不这样做,那就是将其添加到 fetcher 函数中,最后,所有请求都会通过它,对吗?
function fetcher(url, options = {}) {
return fetch(url, {
method: HttpMethods.GET,
...options,
})
.then(response => response.json());
}
function fetchAllJokes() {
return fetcherGet(`${env.API_URL}/jokes`);
}
是的,我们在函数中将其删除fetchAllJokes
,但是如果请求没有返回 JSON 怎么办?
然后,我们需要将其从抓取器中移除,然后将其重新添加到仅返回 JSON 的请求中。这样就浪费了修改已完成内容的时间,请记住“我能做到不用再次修改我编写的代码吗?”这条规则。
现在让我们看看如何做:
我想表达的是,解决问题总是有很多正确的方法,这只是其中之一。它绝不是唯一或最好的解决方案。
一种选择是将功能提取到一个函数中,例如:
function jsonResponse(response) {
return response.json();
}
// Then we could use it as follows
function fetchAllJokes() {
return fetcherGet(`${env.API_URL}/jokes`).then(jsonResponse);
}
// And if we receive other format
function fetchAllJokes() {
return fetcherGet(`${env.API_URL}/jokes`).then(xmlResponse);
}
这是一个很好的方法,因为它让我们能够根据返回的数据来处理响应。
我们甚至可以针对每种数据格式扩展获取器功能:
function jsonFetcher(url, options = {}) {
return fetcher(url, options).then(jsonResponse);
}
function xmlFetcher(url, options = {}) {
return fetcher(url, options).then(xmlResponse);
}
从某种意义上说,这种方法甚至更好,因为我们可以在每个请求中检查标题、正文等内容……
例如,我们希望确保通过json'application/json'
请求发送类型的标头。
function jsonFetcher(url, options = {}) {
const isPost = options.method === HttpMethods.POST;
const hasHeaders = options.headers != null;
if (!hasHeaders) options.headers = {};
if (isPost) {
options.headers['Content-Type'] = 'application/json';
}
return fetcher(url, options).then(jsonResponse);
}
现在,任何时候使用 发出帖子请求时jsonFetcher
,内容类型标头始终设置为'application/json'
。
但是,一个很大的问题:用这种方法,你可能已经发现了一个问题。我们现在必须为每个方法(fetcherGet
,fetcherPost
)和每个获取器创建新函数……
迭代 3
我们可以通过重新思考如何创建提取器来改进这一点,而不是覆盖提取器函数,我们可以返回一个包含该特定提取器的所有方法的对象。
解决这个问题的一个方法是创建一个函数,该函数接收一个获取器,并返回一个附加了所有方法的对象:
function crudForFetcher(fetcher) {
return {
get(url, options = {}) {
return fetcher(url, {
...options,
method: HttpMethods.GET,
})
},
post(url, options = {}) {
return fetcher(url, {
...options,
method: HttpMethods.POST,
})
},
// ...more methods ...
}
}
// Create fetch for each fetcher type
const fetchDefault = crudForFetcher(fetcher);
const fetchJson = crudForFetcher(jsonFetcher);
const fetchXml = crudForFetcher(xmlFetcher);
fetchJson.get('my-api.com/hello');
还有一件事让我有点烦恼,那就是我们需要在每个请求中传递完整的 API URI,现在添加这个功能非常简单,因为我们已经把它全部分解了。
crudForFetcher
我们可以做的是进一步改进该功能,让它接收一些选项:
function crudForFetcher(fetcher, options = { uri: '', root: '' }) {
const { uri, root } = options;
return {
get(path, options = {}) {
return fetcher(path.join(uri, root, path), {
...options,
method: HttpMethods.GET,
})
},
// ... more methods ...
}
}
const jokesFetcher = crudForFetcher(
jsonFetcher,
{
uri: env.API_URL,
root: `jokes`
}
);
此更改的作用是将特定请求的 URI、根和路径合并为单个 URI。
对于jokesFetcher
,请求的 URI 总是以 开头https://my-api.com/jokes
。
现在,我们可以安全地替换原来的功能,而 UI 团队无需进行任何更改,但我们现在拥有更强大的功能并准备扩展,耶!!!
function fetchAllJokes() {
return jokesFetcher.get(); // `https://my-api.com/jokes`
}
function fetchRandomJoke() {
return jokesFetcher.get('/random'); // `https://my-api.com/jokes/random`
}
正如您所见,除了 之外,我们没有修改我们所构建的任何东西crudForFetcher
。
一切都放在一起
function fetcher(url, options = {}) {
return fetch(url, {
method: HttpMethods.GET,
...options,
});
}
function jsonResponse(response) {
return response.json();
}
function jsonFetcher(url, options = {}) {
return fetcher(url, options).then(jsonResponse);
}
function crudForFetcher(fetcher, options = { uri: '', root: '' }) {
const { uri, root } = options;
return {
get(path, options = {}) {
return fetcher(path.join(uri, root, path), {
...options,
method: HttpMethods.GET,
})
},
post(path, options = {}) {
return fetcher(path.join(uri, root, path), {
...options,
method: HttpMethods.POST,
})
},
}
}
// Exposed API
const fetchJokes = crudForFetcher(
jsonFetcher,
{
uri: env.API_URL,
root: `jokes`
}
);
function fetchAllJokes() {
return jokesFetcher.get();
}
function fetchRandomJoke() {
return jokesFetcher.get('/random');
}
概括
我们采取了一个简单的实现,一点一点地构建,直到我们拥有一些可以很好地扩展的东西,而不会在此过程中破坏任何东西(当然还需要更多的改进工作)。
在过去的几年里,我一直在各种项目、框架、语言等中使用这种方法......并且效果很好。
它也确实非常高效,因为它大大减少了我需要做的工作量。
再次强调,这只是众多方法中的一种,适用于这种情况。我可能会发布一种使用 oop 的其他方法。
从中我们可以得出什么结论:
- 了解手头的任务
- 看森林,而不只是看树木(不要只实现功能,还要思考它以及它周围的东西)
- 循序渐进,但不要鲁莽行事
- 使函数/方法尽可能封闭
- 保持简单
我真的很喜欢写这篇文章,希望你也喜欢阅读!