— 1 min read
其实去年六七月左右我就有尝试过在 MidwayJS 中使用 GraphQL 及相关技术栈了了, 但是当时真的太菜了, 完全不清楚依赖注入啊容器啊相关的概念, 于是半途而废放弃了, 改成用原生的 Apollo-Server 了.
这一次是在看到了淘系技术黑皮书中 JSCON 老师(不知道花名...)的文章中给出的结合示例我才有了思路, 但是由于文中的例子可能是 Midway 1 的写法, 在 Midway 2 中部分写法已经不能使用了, 因此这里我又扩展了下, 得到了 EggJS 中间件与 Koa 中间件的两种写法, 这里做一个记录(因为真的还挺有成就感).
阅读本文前需要你了解以下技术栈:
由于没有仔细看文档中的示例代码, 导致我额外浪费了一个小时, 淦.
首先在 src/middleware
下新建graphql.ts
,
我最开始的写法是这样的:
1import * as path from "path";2import { Provide, Config, App } from "@midwayjs/decorator";3import {4 IWebMiddleware,5 IMidwayKoaContext,6 IMidwayKoaNext,7 IMidwayKoaApplication,8} from "@midwayjs/koa";9
10import { ApolloServer, ServerRegistration } from "apollo-server-koa";11import { buildSchemaSync } from "type-graphql";12
13type ApolloMwConfig = { [key: string]: any } & ServerRegistration;14
15@Provide("GraphQLMiddleware")16export class GraphqlMiddleware implements IWebMiddleware {17 @Config("apollo")18 config: ApolloMwConfig;19
20 resolve() {21 console.log("Apollo Config", this.config);22 return async (ctx: IMidwayKoaContext, next: IMidwayKoaNext) => {23 const server = new ApolloServer({24 schema: buildSchemaSync({25 resolvers: [path.resolve("./src", "resolver/*")],26 container: ctx.requestContext,27 }),28 });29 console.log("Apollo-GraphQL Invoke");30 await next();31 return server.getMiddleware(this.config);32 };33 }34}
先不说是哪里错了, 在src/configuration.ts
下注册该全局中间件:
1import { Configuration, App } from "@midwayjs/decorator";2import { ILifeCycle } from "@midwayjs/core";3import { IMidwayKoaApplication } from "@midwayjs/koa";4
5@Configuration({6 imports: ["./lib/orm"],7 importConfigs: ["./config"],8})9export class ContainerConfiguration implements ILifeCycle {10 @App()11 app: IMidwayKoaApplication;12
13 async onReady() {14 console.log("onReady Hook");15 this.app.use(await this.app.generateMiddleware("GraphQLMiddleware"));16 }17}
这样使用的结果是 /graphql
路径直接 404 了, 当时觉得没啥问题, 执行链路是这样的:
this.app.generateMiddleware('GraphQLMiddleware')
GraphqlMiddleware
, 调用 resolve 方法, resolve 内部的异步方法返回Apollo-Server
使用getMiddleware
方法生成的中间件, 然后整体就相当于this.app.use(ApolloMiddleware)
, 没毛病啊?直到这里我还是没发现问题, 当我尝试在 resolve 方法中直接ctx.app.use(server.getMiddleware(this.config))
的时候又可以了! 那么问题就很明朗了, 中间件没注册上. 再回去看文档中"第三方中间件"一节, 它的示例代码是这样的:
1import * as koaStatic from "koa-static";2
3@Provide()4export class ReportMiddleware implements IWebMiddleware {5 resolve() {6 return koaStatic(root, opts);7 }8}
好家伙我直接好家伙, 果然嵌套害死人. 修改 GraphQL 中间件的代码:
1import * as path from "path";2import { Provide, Config, App } from "@midwayjs/decorator";3import { IWebMiddleware, IMidwayKoaApplication } from "@midwayjs/koa";4
5import { ApolloServer, ServerRegistration } from "apollo-server-koa";6import { buildSchemaSync } from "type-graphql";7
8// some extra config9type ApolloMwConfig = { [key: string]: any } & ServerRegistration;10
11@Provide("GraphQLMiddleware")12export class GraphqlMiddleware implements IWebMiddleware {13 @Config("apollo")14 config: ApolloMwConfig;15
16 @App()17 app: IMidwayKoaApplication;18
19 resolve() {20 console.log("Apollo Config", this.config);21 const server = new ApolloServer({22 schema: buildSchemaSync({23 resolvers: [path.resolve("./src", "resolver/*")],24 container: this.app.getApplicationContext(),25 }),26 });27 console.log("Apollo-GraphQL Invoke");28
29 return server.getMiddleware(this.config);30 }31}
淦, 然后果然就可以了, 这里说明几个地方:
ctx.requestContext
拿到容器, 现在拿不到 ctx 参数了, 因此需要注入 app 实例然后才能获取到容器.getApplicationContext()
方法是所有上层框架 app 都会实现的接口.然后编写个简单的解析器就能打开/graphql
了:
1import { Provide } from "@midwayjs/decorator";2import { Resolver, Query } from "type-graphql";3
4import User from "../graphql/user";5
6@Provide()7@Resolver((of) => User)8export default class UserResolver {9 constructor() {}10
11 @Query((returns) => User)12 async GetRandomUser(): Promise<User> {13 return {14 id: Math.floor(Math.random() * 100),15 name: "林不渡",16 };17 }18}
(看到GraphQL Playground
我真的眼泪都要下来了)
暂时不太清楚会不会遇到更多诡异的问题...
Egg 中间件需要遵守 EggJS 的相关规范, 有兴趣的同学可以自己去阅读 EggJS 的文档.
在src/app/middleware
下新建eggraphql.ts
(这名字还挺好玩吧), 这里用的是函数式写法:
1import * as path from "path";2import { IMidwayWebNext } from "@midwayjs/web";3
4import { Context } from "egg";5
6import { ApolloServer, ServerRegistration } from "apollo-server-koa";7import { buildSchemaSync } from "type-graphql";8
9export default (options: ServerRegistration) => {10 return async function graphql(ctx: Context, next: IMidwayWebNext) {11 await next();12 const server = new ApolloServer({13 schema: buildSchemaSync({14 resolvers: [path.resolve(ctx.app.baseDir, "resolver/*.ts")],15 container: ctx.app.applicationContext,16 }),17 });18 ctx.app.use(server.getMiddleware(options));19 };20};
在src/config/config.default.ts
中配置这个中间件, 并且传入配置:
1import { ServerRegistration } from "apollo-server-koa";2import { EggAppConfig, EggAppInfo, PowerPartial } from "egg";3import { ConnectionOptions } from "../lib/orm";4
5export type DefaultConfig = PowerPartial<EggAppConfig>;6
7export default (appInfo: EggAppInfo) => {8 const config = {} as DefaultConfig;9
10 // eggjs版本的全局中间件还是要在这里开启11 config.middleware = ["eggraphql"];12
13 config["eggraphql"] = {14 path: "/eggraphql",15 };16
17 config.security = {18 csrf: false,19 };20
21 return config;22};
访问/eggraphql
:
MidwayJS 提供了@midwayjs/orm
这个包来为 TypeORM 提供集成使用, 也就是受这个影响我觉得是否也可以整个@midwayjs/type-graphql
(之前还在实习的时候我有看到过内部一位大佬封装了个 faas 版本的这玩意).
源码也很简单, 大致来看一下:
@EntityModel
@InjectEntityModel
方法实现before/after Create/Close
getRepository
API先从 index.ts 开始, 由于比较简单就不多做讲解了, 直接看注释
有部分我自己新增的代码, 详见官方仓库 midway-components
1import {2 EntityOptions,3 getMetadataArgsStorage,4 ObjectType,5 EntitySchema,6 Repository,7 TreeRepository,8 MongoRepository,9 Connection,10 getRepository,11} from 'typeorm';12import { ViewEntityOptions } from 'typeorm/decorator/options/ViewEntityOptions';13import { saveModule, attachClassMetadata } from '@midwayjs/core';14
15// 容器中的token16export const CONNECTION_KEY = 'orm:getConnection';17export const MANAGER_KEY = 'orm:getManager';18export const ENTITY_MODEL_KEY = 'entity_model_key';19export const EVENT_SUBSCRIBER_KEY = 'event_subscriber_key';20export const ORM_MODEL_KEY = '__orm_model_key__';21
22export { ConnectionOptions } from 'typeorm';23
24// @Entity的包装, 主要逻辑是将实体类注册到内部的模块关系映射中25export function EntityModel(26 nameOrOptions?: string | EntityOptions,27 maybeOptions?: EntityOptions28): ClassDecorator {29 const options =30 (typeof nameOrOptions === 'object'31 ? (nameOrOptions as EntityOptions)32 : maybeOptions) || {};33 const name = typeof nameOrOptions === 'string' ? nameOrOptions : options.name;34
35 return function (target) {36 if (typeof target === 'function') {37 saveModule(ENTITY_MODEL_KEY, target);38 } else {39 saveModule(ENTITY_MODEL_KEY, (target as object).constructor);40 }41
42 // 就是TypeORM的@Entity内部的逻辑43 getMetadataArgsStorage().tables.push({44 target: target,45 name: name,46 type: 'regular',47 orderBy: options.orderBy ? options.orderBy : undefined,48 engine: options.engine ? options.engine : undefined,49 database: options.database ? options.database : undefined,50 schema: options.schema ? options.schema : undefined,51 synchronize: options.synchronize,52 withoutRowid: options.withoutRowid,53 });54 };55}56
57// @EntityView 装饰器 略过58export function EntityView(59 nameOrOptions?: string | ViewEntityOptions,60 maybeOptions?: ViewEntityOptions61): ClassDecorator {62 // ...63}64
65// 在类中使用@InjectEntityModel装饰器将Repository实例注入到属性66export function InjectEntityModel(67 modelKey?: any,68 connectionName = 'default'69): PropertyDecorator {70 return (target, propertyKey) => {71 // 将元数据添加到类上, 这里的类即使用了此装饰器的类72 // 后续的registerDataHandler逻辑会使用此元数据来实例化Repository73 attachClassMetadata(74 ORM_MODEL_KEY,75 {76 key: {77 modelKey,78 connectionName,79 },80 propertyName: propertyKey,81 },82 target83 );84 };85}86
87// 注入连接88export function InjectConnection(89 connectionName = 'default'90): PropertyDecorator {91 return (target, propertyKey) => {92 // ...93}94
95// 注入实体管理器96export function InjectManager(connectionName = 'default'): PropertyDecorator {97 return (target, propertyKey) => {98 // ...99}100
101
102/**103 * for hooks useEntityModel method104 * @param clz105 * @param instanceName106 */107export function useEntityModel<Entity>(108 clz: ObjectType<Entity>,109 connectionName?: string110): Repository<Entity> {111 return getRepository<Entity>(clz, connectionName);112}113
114export { OrmConfiguration as Configuration } from './configuration';
然后是 hook.ts 和 configuration.ts, 先来看看都包含了哪些 hook:
1import { saveModule } from "@midwayjs/core";2import { Connection, ConnectionOptions } from "typeorm";3
4export const ORM_HOOK_KEY = "__orm_hook_for_configuration__";5
6export interface OrmConnectionHook {7 beforeCreate?(opts?: ConnectionOptions): Promise<ConnectionOptions>;8 afterCreate?(9 conn?: Connection,10 opts?: ConnectionOptions11 ): Promise<Connection>;12 beforeClose?(conn?: Connection, connectionName?: string): Promise<Connection>;13 afterClose?(conn?: Connection): Promise<Connection>;14}15
16export function OrmHook(): ClassDecorator {17 return function (target) {18 if (typeof target === "function") {19 saveModule(ORM_HOOK_KEY, target);20 } else {21 saveModule(ORM_HOOK_KEY, (target as object).constructor);22 }23 };24}
configuration.ts 中我们要做的事情主要有:
1import { ILifeCycle, IMidwayContainer } from "@midwayjs/core";2import { Configuration, listModule, Config } from "@midwayjs/decorator";3import {4 createConnection,5 getConnection,6 getRepository,7 getManager,8 ConnectionOptions,9 Connection,10} from "typeorm";11import {12 ENTITY_MODEL_KEY,13 EVENT_SUBSCRIBER_KEY,14 CONNECTION_KEY,15 ORM_MODEL_KEY,16 MANAGER_KEY,17} from ".";18import { ORM_HOOK_KEY, OrmConnectionHook } from "./hook";19import { join } from "path";20
21// 导入config.orm字段22@Configuration({23 importConfigs: [join(__dirname, "./config")],24 namespace: "orm",25})26export class OrmConfiguration implements ILifeCycle {27 @Config("orm")28 private ormConfig: any;29
30 private connectionNames: string[] = [];31
32 async onReady(container: IMidwayContainer) {33 // 注册后才能在类中注入Repository, Connection EntityManager同理34 (container as any).registerDataHandler(35 ORM_MODEL_KEY,36 (key: { modelKey: any; connectionName: string }) => {37 const repo = getRepository(key.modelKey, key.connectionName);38 return repo;39 }40 );41
42 // 在@EntityModel中调用了saveModule保存实体, 在这里就可以获取到了43 const entities = listModule(ENTITY_MODEL_KEY);44 const eventSubs = listModule(EVENT_SUBSCRIBER_KEY);45
46 const opts = this.formatConfig();47
48 for (const connectionOption of opts) {49 connectionOption.entities = entities || [];50 connectionOption.subscribers = eventSubs || [];51 const name = connectionOption.name || "default";52 this.connectionNames.push(name);53 let isConnected = false;54 // 尝试建立连接55 try {56 const conn = getConnection(name);57 if (conn.isConnected) {58 isConnected = true;59 }60 } catch {}61 if (!isConnected) {62 const rtOpt = await this.beforeCreate(container, connectionOption);63 const con = await createConnection(rtOpt);64 await this.afterCreate(container, rtOpt, con);65 }66 }67
68 // 在容器中注册连接69 container.registerObject(CONNECTION_KEY, (instanceName) => {70 if (!instanceName) {71 instanceName = "default";72 }73 return getConnection(instanceName);74 });75 }76
77 async onStop(container: IMidwayContainer) {78 await Promise.all(79 Object.values(this.connectionNames).map(async (connectionName) => {80 const conn = getConnection(connectionName);81
82 await this.beforeClose(container, conn, connectionName);83
84 if (conn.isConnected) {85 await conn.close();86 }87
88 await this.afterClose(container, conn);89 })90 );91
92 this.connectionNames.length = 0;93 }94
95 formatConfig(): any[] {96 const originConfig = this.ormConfig;97 if (originConfig?.type) {98 originConfig.name = "default";99 return [originConfig];100 } else {101 const newArr = [];102
103 for (const [key, value] of Object.entries(originConfig)) {104 (value as any).name = key;105 newArr.push(value);106 }107
108 return newArr;109 }110 }111
112 private async beforeCreate(113 container: IMidwayContainer,114 opts: ConnectionOptions115 ): Promise<ConnectionOptions> {116 let rt = opts;117 const clzzs = listModule(ORM_HOOK_KEY);118 for (const clzz of clzzs) {119 const inst: OrmConnectionHook = await container.getAsync(clzz);120 if (inst.beforeCreate && typeof inst.beforeCreate === "function") {121 rt = await inst.beforeCreate(rt);122 }123 }124 return rt;125 }126
127 private async afterCreate(128 container: IMidwayContainer,129 opts: ConnectionOptions,130 con: Connection131 ): Promise<Connection> {132 let rtCon: Connection = con;133 const clzzs = listModule(ORM_HOOK_KEY);134 for (const clzz of clzzs) {135 const inst: OrmConnectionHook = await container.getAsync(clzz);136 if (inst.afterCreate && typeof inst.afterCreate === "function") {137 rtCon = await inst.afterCreate(con, opts);138 }139 }140 return rtCon;141 }142
143 private async beforeClose(144 container: IMidwayContainer,145 con: Connection,146 connectionName: string147 ) {148 let rt = con;149 const clzzs = listModule(ORM_HOOK_KEY);150 for (const clzz of clzzs) {151 const inst: OrmConnectionHook = await container.getAsync(clzz);152 if (inst.beforeClose && typeof inst.beforeClose === "function") {153 rt = await inst.beforeClose(rt, connectionName);154 }155 }156 return rt;157 }158
159 private async afterClose(container: IMidwayContainer, con: Connection) {160 let rt = con;161 const clzzs = listModule(ORM_HOOK_KEY);162 for (const clzz of clzzs) {163 const inst: OrmConnectionHook = await container.getAsync(clzz);164 if (inst.afterClose && typeof inst.afterClose === "function") {165 rt = await inst.afterClose(rt);166 }167 }168 return rt;169 }170}
大致逻辑即是如此.
具体使用直接参考我给@midwayjs/orm
提的这个文档 PR