如何在Koa/Express中间件中访问Apollo Server的DataSource?
嘿,作为Apollo Server新手,遇到这种跨中间件复用DataSource的困惑太正常了!我来给你梳理下可行的方案,以及为什么官方没有直接提供你想要的“开箱即用”方法。
首先得明确:Apollo Server的DataSource设计初衷是绑定请求生命周期的——每个请求都会触发dataSources函数创建实例,并调用initialize方法注入请求上下文和缓存。而你的Koa auth中间件是在Apollo的请求处理流程之前执行的,这时候Apollo还没初始化任何DataSource实例,所以直接访问Apollo创建的实例是不可能的。
那怎么解决?这里有两种比较规范的方案,比你说的“手动挂载到ctx”更优雅:
方案一:抽离核心业务逻辑到独立服务(推荐)
其实DataSource本质上是Apollo为解析器提供的一层请求封装,核心的业务逻辑(比如查询用户、验证token)完全可以抽离成独立的服务类,让中间件和DataSource都复用这部分逻辑。这样既解耦,又避免了依赖DataSource的生命周期问题。
举个例子:
// 1. 抽离独立的用户服务(无状态,可全局复用) class UserService { async getUserByToken(token) { // 这里写你的核心逻辑:比如解码token、查询数据库等 const decodedToken = this.verifyToken(token); return await this.db.query('SELECT * FROM users WHERE id = $1', [decodedToken.userId]); } // 其他核心方法... } // 创建全局单例(因为无状态,不会有请求污染) const userService = new UserService(); // 2. 在DataSource里复用这个服务 class UserDataSource extends DataSource { async getUserByToken(token) { return userService.getUserByToken(token); } // 其他需要暴露给解析器的方法... } // 3. Koa中间件直接调用userService async function auth(ctx, next) { const token = ctx.headers.authorization?.replace('Bearer ', ''); if (token) { ctx.state.user = await userService.getUserByToken(token); } await next(); } // 4. Apollo Server配置不变 const server = new ApolloServer({ typeDefs, resolvers, dataSources: () => ({ user: new UserDataSource(), }), });
这种方式的好处是:业务逻辑集中维护,中间件和解析器都能复用,完全避开了DataSource的生命周期限制,也是社区推荐的最佳实践。
方案二:手动管理DataSource的请求级实例
如果你一定要复用DataSource实例(比如依赖它的缓存、请求上下文特性),可以在Koa中间件里手动创建并初始化实例,然后传递给Apollo Server。这样中间件和解析器用的是同一个请求级实例,不会有跨请求污染。
代码示例:
class UserDataSource extends DataSource { // 你的DataSource实现... } async function auth(ctx, next) { // 创建DataSource实例并初始化(模拟Apollo的初始化流程) const userDataSource = new UserDataSource(); userDataSource.initialize({ context: ctx, // 把Koa的ctx作为上下文传入 cache: new InMemoryCache(), // 或者复用Apollo的缓存实例 }); // 在这里使用DataSource const token = ctx.headers.authorization?.replace('Bearer ', ''); ctx.state.user = await userDataSource.getUserByToken(token); // 挂载到ctx,供Apollo后续使用 ctx.dataSources = { user: userDataSource }; await next(); } // 配置Apollo Server,复用中间件创建的实例 const server = new ApolloServer({ typeDefs, resolvers, // 从Koa的ctx里获取已初始化的DataSource dataSources: ({ ctx }) => ctx.dataSources, // 把Koa的上下文传递给Apollo的context context: ({ ctx }) => ({ user: ctx.state.user, ...ctx, }), });
注意:这种方法需要你手动处理DataSource的初始化,要确保每个请求都创建新实例,避免跨请求的状态污染。
最后再明确下:Apollo官方确实没有提供直接从前置Koa中间件访问其创建的DataSource的API,因为两者的执行顺序和生命周期不匹配。上面的两种方案都是社区认可的规范做法,其中方案一更推荐,因为它让代码结构更清晰、解耦度更高。
内容的提问来源于stack exchange,提问作者Jess

