如何延迟代码的执行,直到 fetch 返回值



我创建了以下两个函数来从后端获取JWT Token并将其存储在localStorage中。逻辑看起来很简单,但它不起作用,因为在解析我的令牌值之前,代码会继续执行。因此,链中需要令牌的函数无法获取令牌。最终,它确实会获取令牌并将其存储在localStorage但是到那时,启动链中的所有函数都无法获得有效的令牌。

当我的 React 应用程序启动时,我一开始就完成了所有这些工作。因此,我们的想法是获取令牌,然后执行剩余的启动步骤。

因此,我在应用的入口点调用myStartup()函数。我这样做是第一步。从那里,剩余的启动函数作为回调传递。

export const getJwtTokenFromApi = () => {
var request = new Request('/api/token', {
method: 'GET',
mode: 'cors',
credentials: 'include'
});
fetch(request)
.then((response) => {
response.text()
.then((token) => {
if(token.length > 0) {
localStorage.setItem('myToken', token);
return token;
} else {
return null;
}
})
})
.catch(err => {
});
}
export const getJwtToken = () => {
let token = localStorage.getItem('myToken');
if (token == null)
token = getJwtTokenFromApi();
return token;
}
export const myStartup = (callback) => {
const token = getJwtToken();
callback(token);
}

最终发生的事情是,需要令牌的调用函数不会延迟到收到令牌,因此它最终会收到undefined

如何确保我推迟执行需要令牌的函数,直到我有一个合法的令牌 - 或者至少是一个空值,这意味着我的API调用失败?

根本问题是你没有像处理异步一样处理所有异步的东西。这是一个相当复杂的工作流,阻塞和非阻塞任务混合在一起,这意味着我们需要在整个过程中应用异步模式。让我们逐个执行脚本。


这似乎是脚本的入口点:

export const myStartup = (callback) => {
const token = getJwtToken();
callback(token);
}

它不起作用,因为getJwtToken是异步的,这意味着它的值将不可用于下一行的callback

我们怎么知道getJwtToken是异步的?因为getJwtToken调用getJwtTokenFromApi,它调用fetch(规范告诉我们它是异步的)。由于getJwtToken包装异步行为,因此它本身是异步的。

由于getJwtToken是异步的,我们知道token在需要时不会在第二行callback可用。事实上,token在该作用域中永远不会可用,因为getJwtToken返回一个 Promise,其解析值仅在.then处理程序中可用。因此,第 1 步是重写此函数:

export const myStartup = (callback) => {
getJwtToken() // returns a Promise
.then((token) => { // token is available inside Promise's .then
callback(token);
})
}

现在我们看看内部getJwtToken,请记住,由于我们刚刚所做的更改,它必须返回一个承诺。

export const getJwtToken = () => {
let token = localStorage.getItem('myToken');
if (token == null)
token = getJwtTokenFromApi();
return token;
}

这是一个有趣的案例,因为getJwtToken实现了分支行为,其中一个分支是同步的,另一个不是。 (localStorage.getItem块,但getJwtTokenFromApi是异步的。处理此类情况的唯一方法是使整个函数异步:确保它始终返回 Promise,即使它所需的数据可从同步源获得。

由于localStorage.getItem是同步的,如果我们喜欢它给我们的值,我们在返回之前将该值包装在 Promise 中。否则,我们可以只返回getJwtTokenFromApi返回的承诺:

export const getJwtToken = () => {
let token = localStorage.getItem('myToken')
if(token !== null)
return Promise.resolve(token);
return getJwtTokenFromApi();
}

现在,无论我们发现自己处于哪种情况,此函数都将返回一个包含令牌的 Promise。

最后,我们进入getJwtTokenFromApi,它做了几件事:

  • 它构造了一个Request
  • 它执行请求(异步)
  • 如果成功,它将响应转换为文本(异步)
  • 它检查文本

如果所有这些事情都成功了,它想要返回文本值。但是这些任务中有一半是异步的,这再次意味着整个函数必须变得异步。以下是您开始使用的更精简的版本:

export const getJwtTokenFromApi = () => {
var request = new Request('/api/token', {});
fetch(request)
.then((response) => {
response.text()
.then((token) => {
if(token.length > 0) {
localStorage.setItem('myToken', token);
return token;
} else {
return null;
}
})
})
}

这里最大的问题是你没有归还fetch。这很重要,因为嵌套在里面的其他return语句不适用于整个函数。此函数不会返回写入的任何内容,尽管它将执行XHR 调用。因此,第一个解决方法是return fetch.

但仅仅增加return是不够的。为什么?因为在.then处理程序中,您希望访问响应的text,但该访问本身是异步的。当您使用.then访问该值(如token)时,该值将在fetch.then内静默死亡,除非您也返回response.text()。真的,你需要的是这个:

return fetch(request)
.then((response) => {
return response.text()
.then((text) => {
if(text.length > 0) return text;
else return null

但是这段代码是不必要的冗长,而且它以越来越深的嵌套向右爬行的方式使得代码难以阅读或重新排序。这些步骤是连续的,我们希望它们看起来像这样:

STEP 1
STEP 2
STEP 3
(not)
STEP 1
STEP 2
STEP 3

所以,让我们尝试一些更苗条的东西:

return fetch(request)                          // step 1
.then((response) => response.text())           // step 2
.then((text) => text.length > 0 ? text : null) // step 3

此代码更平坦、更纤细。对步骤重新排序或插入新步骤也更容易。当然,它并没有完成将令牌存储在localStorage中的重要工作,这就是为什么我们有稍微强大的最终版本的原因:

export const getJwtTokenFromApi = () => {
var request = new Request('/api/token', {
method: 'GET',
mode: 'cors',
credentials: 'include'
});
return fetch(request)
.then((response) => response.text())
.then((token) => {
if(token.length > 0) {
localStorage.setItem('myToken', token);
return token;
}
return null;
})
})
}

由于嵌套 Promise 解析的方式,我们能够扁平化所有这些代码:当一个 Promise 包含另一个 Promise(以及另一个等)时,引擎将自动解开所有中间 Promise。例如,这两个代码段会产生相同的结果:

var x = Promise.resolve( Promise.resolve( Promise.resolve ( 10 )))
var y = Promise.resolve( 10 )

xy都将像单一的、扁平的承诺一样,决心10,这意味着我们可以将其放在任何一个之后:

.then((value) => {
// value === 10
})

这是最终脚本:

export const getJwtTokenFromApi = () => {
var request = new Request('/api/token', {
method: 'GET',
mode: 'cors',
credentials: 'include'
});
return fetch(request)
.then((response) => response.text())
.then((token) => {
if(token.length > 0) {
localStorage.setItem('myToken', token);
return token;
}
return null;
})
})
}
export const getJwtToken = () => {
let token = localStorage.getItem('myToken')
if(token !== null)
return Promise.resolve(token);
return getJwtTokenFromApi();
}
export const myStartup = (callback) => {
getJwtToken()
.then((token) => {
callback(token);
})
}

还有一个问题:myStartup是异步的吗?

使用上面的经验法则,我们会说,由于它包装了异步行为,因此它本身就是异步的。但是,此脚本混合了异步模式:承诺和回调。我怀疑这是因为您一方面更熟悉节点样式的回调,但fetch返回一个 Promise,并且在实现过程中这两种方法有点"在中间相遇"——或者更确切地说,在模块的 API 上:myStartup。这是一个异步函数,但它似乎对这一事实并不满意。

当调用者调用myStartup时,它不会返回任何内容。这一点是显而易见的,因为没有return的说法。但是,通过接受回调函数,您提供了一种机制,以便在所有潜在的异步工作完成后向调用方发出信号,这意味着它仍然可以使用。

除非支持节点样式的回调模式很重要,否则我建议采取最后一步,使这个模块完全基于 Promise:修改myStartup,使其返回使用令牌解析的 Promise。由于上述解包行为,这是一个非常简单的更改:

export const myStartup = () => {
return getJwtToken();
}

但是现在很明显,myStartup不会给该过程添加任何内容,因此您不妨通过删除函数并将getJwtToken重命名为myStartup来删除包装器。

你的函数getJwtToken应该返回承诺:

export const getJwtToken = () => {   
let token = localStorage.getItem('myToken');
return token ? Promise.resolve(token) : getJwtTokenFromApi(storeToken) 
}

在调用方中,令牌将包装在内部返回的承诺中:

getJwtToken().then(token => doSomething(token))

我使用以下代码得到了这个,但我认为它不是很优雅。

本质上,我将多个函数组合成一个,并添加了一个回调参数,以便我可以创建启动函数链。如果没有收到回调,我只需返回令牌,以使getJwtToken()成为多用途函数,即调用它以获取令牌或传递期望令牌的函数。

我真的很想拥有单独的功能,以便并非所有问题都在一个功能中。另外,当我只需要获取令牌时,不要疯狂地使用回调参数。

我想发布代码,以便我可以得到一些建议,使其更加健壮和优雅。

export const getJwtToken = (callback) => {
// If token is already in localStorage, get it and return it.
const token = localStorage.getItem('myToken');
if (token != null)
return token;
// Token is not in localStorage. Get it from API
var request = new Request('/api/token', {
method: 'GET',
mode: 'cors',
credentials: 'include'
});
fetch(request)
.then((response) => {
response.text()
.then((token) => {
if (token.length > 0) {
// First, save it in localStorage
localStorage.setItem('myToken', token);
// If no callback function received, just return token
if (typeof callback == "undefined") {
return token;
} else {
callback(token);
}
}
})
})
.catch(err => {
});
}

最新更新