从 AsyncStorage 检索 JWT 以使用 ApolloClient 创建 httpLink 中间件时出现问题

Problems Retrieving JWT from AsyncStorage to Create httpLink Middleware with ApolloClient

我几乎 copy/pasting 来自 Apollo GraphQL Network Layer Docs on Creating Middleware 的示例代码,除了我从 apollo-client-preset 中提取我的依赖项并且我正在使用一个简单的实用程序库来 return jwt 令牌。这是 link 代码...

import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, concat } from 'apollo-client-preset';
import { getSessionToken } from './api/localStorage';

const httpLink = new HttpLink({ uri: 'http://localhost:4000/' });

const authMiddleware = new ApolloLink((operation, forward) => {
  const token = getSessionToken();
  const authorizationHeader = token ? `Bearer ${token}` : null;

  operation.setContext({
    headers: {
      authorization: authorizationHeader,
    }
  });

  return forward(operation);
})

const client = new ApolloClient({
  link: concat(authMiddleware, httpLink),
});

下面是 ./api/localStorage 方法...

export const getSessionToken = () => {
    return AsyncStorage.getItem(AUTH_TOKEN);
}

执行上述代码会产生一个包含以下 headers...

的 HTTP 请求
POST / HTTP/1.1
Host: localhost:4000
Content-Type: application/json
User-Agent: Expo/2.2.0.1011489 CFNetwork/893.14 Darwin/17.3.0
Connection: keep-alive
Accept: */*
Accept-Language: en-us
Authorization: Bearer [object Object]
Accept-Encoding: gzip, deflate
Content-Length: 174

Notice the Authorization: Bearer [object Object] bit

我觉得问题很明显。 AsyncStorage 是一个异步函数,在我 assemble 授权字符串之前,我需要等待承诺得到解决。然而,解决方案并不那么明显。试过各种方法,至今没有爱

我见过几个与上述类似的例子。这是 How to GraphQL Authentication Module 中的另一个。两者都没有显示如何等待 asyncstorage 承诺首先解决。我知道 Apollo 有时会自动等待你的承诺,所以我认为这里可能就是这种情况。但似乎不是我的经验。

那么解决方案是什么?

这是我尝试过的几件事,但都失败了。

1。 Promise.then()

const middlewareAuthLink = new ApolloLink((operation, forward) => {
  getSessionToken().then(token => {
    const authorizationHeader = token ? `Bearer ${token}` : null
    operation.setContext({
      headers: {
        authorization: authorizationHeader
      }
    })
  });

  return forward(operation)
})

这确实正确地构造了授权字符串,但似乎没有创建中间件,因为这没有创建授权 header。对我来说,这似乎是我最好的选择,但我一定是做错了,原因对我来说并不明显。

POST / HTTP/1.1
Host: localhost:4000
Content-Type: application/json
Connection: keep-alive
Accept: */*
User-Agent: Expo/2.2.0.1011489 CFNetwork/893.14 Darwin/17.3.0
Content-Length: 174
Accept-Language: en-us
Accept-Encoding: gzip, deflate

Notice the lack of Authorization header

2。 async/await

我认为这会非常简单,将它变成一个 async/await 函数,但不是那么多。这是代码

const middlewareAuthLink = new ApolloLink(async (operation, forward) => {
  const token = await getSessionToken();
  const authorizationHeader = token ? `Bearer ${token}` : null;

  operation.setContext({
    headers: {
      authorization: authorizationHeader,
    }
  });
  return forward(operation)
})

但这导致了这个超级难看的 red-screen-of-death 错误...

Unhandled (in react-apollo:Apollo(EventList)), ApolloError@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106405:36
currentResult@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106516:43
dataForChild@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101608:79
render@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101659:49
finishClassComponent@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:4528:102
performUnitOfWork@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5547:33
workLoop@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5566:142
_invokeGuardedCallback@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2707:23
invokeGuardedCallback@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2681:41
performWork@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5602:41
scheduleUpdateImpl@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5723:105
enqueueForceUpdate@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:4341:179
forceUpdate@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:8265:38
forceRenderChildren@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101579:58
next@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101554:50
error@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:109797:25
forEach@[native code]
error@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106747:44
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107277:47
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107649:29
forEach@[native code]
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107648:27
forEach@[native code]
broadcastQueries@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107644:33
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107239:51
tryCallOne@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:10901:14
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:10987:25
_callTimer@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12157:15
_callImmediatesPass@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12193:17
callImmediates@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12397:31
__callImmediates@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2301:30
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2179:32
__guard@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2287:11
flushedQueue@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2178:19
flushedQueue@[native code]
invokeCallbackAndReturnFlushedQueue@[native code]

那么在设置 header 之前如何等待 AsyncStorage 的结果为 return?

可能还有其他解决方案,甚至更好的解决方案,但这最终对我有用,所以我将其发布为已解决。

import { setContext } from 'apollo-link-context';

const httpLink = new HttpLink({ uri: 'http://localhost:4000/'});

const authHeader = setContext(
  request =>
    new Promise((success, fail) => {
      getSessionToken().then(token => success({ headers: { authorization: `Bearer ${token}` }}))
    })
)

const client = new ApolloClient({
  link: concat(authHeader, httpLink),
  cache: new InMemoryCache(),
})

这是一个替代方案,它从内存缓存中加载令牌,其性能应该比从 AsyncStorage 中获取更好。或者,您可以简单地使用 getter 包装函数来缓存来自 AsyncStorage 的响应,但是将其作为 apollo 缓存的一部分在更新令牌时具有 re-rendering 的额外好处。

import gql from 'graphql-tag'
import ApolloClient from 'apollo-client'
import { setContext } from 'apollo-link-context'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink } from 'apollo-link'
import AsyncStorage from '@react-native-community/async-storage'

const authLink = setContext(async (_, { headers }) => {
  const context = await client.query({
    query: gql`
      query AuthQuery {
        authToken @client
      }
    `
  })
  const {
    data: { authToken: token }
  } = context
  let extra = {}
  if (token) {
    extra = { authorization: `Bearer ${token}` }
  }
  return {
    headers: {
      ...headers,
      ...extra
    }
  }
})

const httpLink = createHttpLink({
  uri: 'http://localhost:4000/'
})

const cache = new InMemoryCache()

const client = new ApolloClient({
  link: ApolloLink.from([authLink, httpLink]),
  cache
})

client.writeData({ data: { authToken: null } })

AsyncStorage.getItem('token').then((token: string | null) => {
  if (token) {
    client.writeData({ data: { authToken: token } })
  }
})

export { client }