ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spectrum API 服务架构解析:基于 Express.js 与 GraphQL 的 GraphQL-first Web 服务器

Spectrum API 服务架构解析:基于 Express.js 与 GraphQL 的 GraphQL-first Web 服务器 后端前端即时通讯社交【免费下载链接】spectrumSimple, powerful online communities.项目地址https://gitcode.com/gh_mirrors/sp/spectrum点击查看免费下载导读本文以 docs/backend/api/README.md 为核心深入剖析 Spectrum 开源社区项目中api服务的整体架构。Spectrum 的 API 是一个基于 Express.js 与 GraphQL 的 Node.js Web 服务器同时内置 WebSocket 订阅服务器承担全部 GraphQL 查询、变更、实时订阅与第三方 OAuth 认证职责。读完本文你将掌握该服务的 GraphQL-first 设计哲学、目录结构与各模块职责划分并能从源码层面理解 schema 组装、resolver 拆分、DataLoader 批量加载等关键实现。一、API 服务总览一个服务器两种协议Spectrum 的 API 服务不是单纯的 REST 接口而是整个产品的数据中枢。它同时承载两个协议通道HTTP 通道处理常规 GraphQL 查询Query与变更Mutation由 Express.js Apollo Server 支撑WebSocket 通道处理订阅Subscription实现消息、通知等实时推送。从 api/index.js 的入口代码可以看到服务启动时会先创建 Express 应用并挂载 Apollo Server 中间件随后单独创建一个 HTTP Server 用于安装订阅处理// api/index.js const app express(); // ... 各类中间件与路由注册 apolloServer.applyMiddleware({ app, path: /api, cors: corsOptions }); // 订阅走独立的 WebSocket 服务器 const httpServer createServer(app); apolloServer.installSubscriptionHandlers(httpServer); httpServer.listen(PORT);PORT默认为3001可通过环境变量覆盖开发环境下 GraphQL Playground 就运行在http://localhost:3001/api访问根路径/时服务会按环境重定向到主应用生产环境为https://spectrum.chat开发环境为http://localhost:3000。若在浏览器中直接访问根地址却跳转到了前端页面这正是这段逻辑在起作用。二、GraphQL-first 设计哲学该服务采用GraphQL-first的开发顺序先设计 GraphQL Schema再实现业务逻辑。文档明确指出这样做能带来清晰的关注点分离业务逻辑与 Schema 解耦也是 Facebook 官方推荐的 GraphQL 使用方式。这种哲学在技术选型上体现为使用graphql-tools的makeExecutableSchema先用 GraphQL Schema Language 编写类型定义typeDefs再与独立维护的 resolvers 组合成最终可执行的 schema。api/schema.js 中就是这一组合过程的真实实现// api/schema.js const schema makeExecutableSchema({ typeDefs: [ scalars.typeDefs, generalTypes, Root, Community, CommunityMember, Channel, Thread, ThreadParticipant, Message, Reaction, User, DirectMessageThread, Invoice, ], resolvers, schemaDirectives: {}, });值得注意的细节是Schema 根类型中定义了dummy占位字段——这是因为 graphql-js 不允许空的根类型而项目的所有业务类型都是通过extend type Query / Mutation / Subscription追加的// api/schema.js 中的 Root 定义 type Query { dummy: String } type Mutation { dummy: String } type Subscription { dummy: String }在开发环境下NODE_ENV development且 debug 开启schema.js 还会用graphql-log包装所有 resolvers 以记录每次执行的日志若设置了REACT_APP_MAINTENANCE_MODE enabled则通过addSchemaLevelResolveFunction为整个 schema 注入维护模式拦截器任何请求都会抛出维护提示错误。三、目录结构与模块职责原文档给出了一份经过实际项目验证的目录注释这正是理解该服务的关键骨架先完整保留如下server/ ├── migrations # Migrations for seeding the database with some initial data ├── models # Handle talking to the database ├── mutations # Mutation resolvers ├── queries # Query resolvers ├── subscriptions # Subscription resolvers ├── types # The schema, split up into many smaller parts │ └── scalars.js # The custom scalars we use in our schema and their resolvers ├── README.md ├── index.js # Runs the actual servers (GraphQL WebSocket for subscriptions) └── schema.js # Combines the types from types/ and the resolvers together with graphql-tools在仓库中这个server/目录对应 api/ 目录源码目录名即为api。下面结合源码逐一展开每个目录的职责1.types/Schema 拆分单元类型定义被拆分为多个小文件每个业务实体一个文件例如api/types/Thread.js线程类型包含ThreadMessagesConnection分页连接、ThreadContenttitle/body/media、ThreadType枚举SLATE / DRAFTJS / TEXT、deprecated字段标记等api/types/Channel.js、api/types/Community.js、api/types/User.js 等对应各业务实体api/types/general.js跨实体复用的通用类型如分页用的PageInfohasNextPage/hasPreviousPage、权限类型ChannelPermissions/CommunityPermissions、EntityTypes枚举等。值得注意的是每个类型文件都在自身内部通过extend type Query/extend type Mutation声明属于自己的根操作例如 api/types/Thread.js 中声明了thread(id: ID!): Thread查询和deleteThread(threadId: ID!): Boolean变更实现了「类型与它相关的操作放在一起」的模块化组织方式。2.types/scalars.js自定义标量api/types/scalars.js 定义了三个自定义标量及其 resolverconst typeDefs /* GraphQL */ scalar Date scalar Upload scalar LowercaseString ; const resolvers { Date: GraphQLDate, Upload: GraphQLUpload, LowercaseString: LowercaseString, };Date基于graphql-date的日期标量Upload来自apollo-server-express的文件上传标量配合general.js中的uploadImagemutation 使用LowercaseString项目自定义标量见 api/types/custom-scalars/LowercaseString.js用于强制将字符串转为小写典型应用是general.js中邮箱邀请输入的email: LowercaseString!。3.queries/与mutations/按业务实体拆分的 resolvers查询与变更 resolver 均按业务实体拆分子目录每个子目录一个index.js汇出。以查询为例api/queries/thread/index.js 的结构是module.exports { Query: { thread, }, Thread: { attachments, channel, community, participants, isAuthor, messageConnection, author, content, reactions, metaImage, messageCount: ({ messageCount }: DBThread) messageCount || 0, editedBy, }, };可以看到queries/thread/目录下同时包含根查询 resolverrootThread.js和 Thread 类型各字段的字段级 resolver如channel.js、community.js、author.js每个字段一个文件便于维护与测试。变更侧同理api/mutations/message/index.js 汇出Mutation.deleteMessage其实现位于 api/mutations/message/deleteMessage.js内部通过UserError处理「消息不存在」「无权限删除」等业务错误并维护线程参与者数据的一致性。4.subscriptions/订阅 resolverapi/subscriptions/ 目录下按 community、directMessageThread、message、notification、thread 拆分子文件每个文件导出Subscription对象。实际推送在 api/apollo-server.js 中通过subscriptions配置启用WebSocket 路径为/websocket连接建立时onConnect从 upgradeReq 解析用户并为其创建无缓存的 DataLoader 注入订阅上下文。5.models/数据库访问层models 层封装对 RethinkDB 的全部读写操作queries/mutations 的 resolver 不直接碰数据库。以 api/models/message.js 为例export const getMessage (messageId: string): PromiseDBMessage { return db .table(messages) .get(messageId) .run() .then(message { if (!message || message.deletedAt) return null; return message; }); };该文件还实现了基于复合索引threadIdAndTimestamp的正向/反向分页查询getForwardMessages/getBackwardsMessages与 types 中定义的 connection 分页语义一一对应。6.migrations/数据库初始化与演进api/migrations/ 存放 RethinkDB 迁移脚本包含初始数据填充20170410074258-initial-data.js以及大量业务演进迁移通知、回复数、Slack 导入、Stripe 表、头像 URL 修复等并有seed/目录负责预置演示数据配合迁移配置 api/migrations/config.js 使用。四、服务器入口中间件、认证路由与错误处理api/index.js 完整展示了 Express 应用的装配顺序中间件注册顺序本身就有讲究statsd 指标采集第一时间挂载保证计时准确信任代理 toobusyapp.set(trust proxy, true)配合 shared/middlewares/toobusy.js 做过载保护安全中间件addSecurityMiddleware(app, { enableNonce: false, enableCSP: false })见 shared/middlewares/security.js生产环境额外启用 CSRF 防护压缩compression()路由注册/auth挂认证路由/api挂 API 路由GraphQL 中间件Apollo Server 挂载到/api错误处理最后挂 shared/middlewares/error-handler.js。认证路由与 Passport 多 OAuthapi/routes/auth/index.js 将认证路由按第三方平台拆分authRouter.use(/twitter, twitterAuthRoutes); authRouter.use(/facebook, facebookAuthRoutes); authRouter.use(/google, googleAuthRoutes); authRouter.use(/github, githubAuthRoutes); authRouter.use(/logout, logoutRoutes);对应的策略注册集中在 api/authentication.jsinit()函数完成 passport 序列化/反序列化配置并注册 Twitter、Facebook、Google、GitHub 四种 OAuth2/OAuth1 策略。其序列化实现比较特别优先把完整用户数据 JSON 序列化进 cookie快速路径避免每请求查库仅在数据不是序列化 JSON 时才回退到按 userID 查库的慢路径。生产与开发环境通过IS_PROD区分不同的 OAuth Client ID / Secret 与回调基址生产https://spectrum.chat开发http://localhost:3001。API 路由与用户数据导出api/routes/api/index.js 目前暴露了/user.json用户数据导出路由export-user-data满足用户数据可携带性需求GraphQL 部分则由 Apollo Server 直接承载在/api。进程级兜底入口文件最后为unhandledRejection与uncaughtException注册了兜底处理先通过 RavenSentry上报异常再以非零状态码退出进程避免服务在异常状态下继续运行。五、Apollo Server 配置安全防护、缓存与上下文GraphQL 执行层配置集中在 api/apollo-server.js几项关键配置体现了生产级实践成本分析cost analysis服务继承 ApolloServer 并注入graphql-cost-analysis验证规则maximumCost为 750、默认单字段成本 1超限请求会得到明确的错误提示「GraphQL query exceeds maximum complexity...」深度限制validationRules: [depthLimit(10)]防止深嵌套查询拖垮数据库响应缓存通过apollo-server-plugin-response-cache接入 Redis 缓存apollo-server-cache-redis且只对未登录用户的公开响应生效shouldReadFromCache/shouldWriteToCache均判断!context.user同时cacheControl.defaultMaxAge设为 60 秒上下文构建每个 HTTP 请求都会调用createLoaders()创建一套 DataLoader并把当前用户ban 用户会被排除、updateCookieUserData回调等注入 context订阅连接则复用连接建立时解析出的用户开发体验非生产环境开启 Playground浅色主题预置一个user(username: mxstbr)示例查询 Tab与 introspection文件上传限制maxFileSize为 25MB。六、DataLoader 与请求级批量加载为了让 GraphQL 的 N1 查询问题得到缓解API 为每个请求创建独立的 DataLoader 实例。api/loaders/index.js 一次性创建了 20 余个 loader覆盖 user、thread、channel、community、message、reaction、directMessageThread 等核心实体以及派生计数channelThreadCount、communityMemberCount和权限查询userPermissionsInCommunity、userPermissionsInChannel。loader 的工厂函数由 api/loaders/create-loader.js 统一封装其核心逻辑改编自 DataLoader 官方文档的 RethinkDB 示例批量函数先对 keys 去重再在返回结果时按indexField默认id也支持函数形式的复合键建立 Map最后按原始 keys 顺序归一化输出保证每个 key 都有对应槽位const createLoader (batchFn, indexField id, cacheKeyFn key key) ( options ) { return new DataLoader(keys { return batchFn(unique(keys)).then( normalizeRethinkDbResults(keys, indexField, cacheKeyFn) ); }, options); };上下文中的loaders类型定义api/loaders/types.js暴露load/loadMany/clear三个方法订阅场景下则传入{ cache: false }关闭缓存以获取始终最新的实时数据。七、启动与运行API 服务属于 monorepo 的一部分其独立依赖清单见 api/package.json启动脚本为NODE_ENVproduction node main.js由 backpack 构建产出。运行时依赖的关键环境变量包括变量说明PORTHTTP/WebSocket 监听端口默认3001NODE_ENVproduction时启用 CSRF、关闭 Playground 与 introspectionFORCE_DEV强制以开发模式运行即使NODE_ENVproductionTWITTER/FACEBOOK/GOOGLE/GITHUB_OAUTH_CLIENT_SECRET及_DEVELOPMENT后缀变体第三方 OAuth 凭据按环境区分REACT_APP_MAINTENANCE_MODE置为enabled时整个 GraphQL schema 进入维护模式需要注意的是该服务依赖仓库内的 RethinkDB、Redis缓存/会话/订阅、Sentry 等基础设施配置见 shared/db/db.js、shared/middlewares/ 等直接运行前需先完成相应环境的初始化。结语Spectrum 的api服务是典型的 GraphQL-first 落地范本先以 Schema Language 在 api/types/ 定义类型契约再通过 api/schema.js 用graphql-tools将分散的 queries/mutations/subscriptions resolvers 组装为可执行 schema最后由 api/index.js 同时托起 Express HTTP 服务与 WebSocket 订阅服务。这种「类型契约驱动、按实体拆分 resolver、DataLoader 聚合取数」的组织方式在业务规模扩大后依然能保持清晰的边界与可测试性值得在同类 Node.js GraphQL 项目中借鉴。赞分享后端前端即时通讯社交【免费下载链接】spectrumSimple, powerful online communities.项目地址https://gitcode.com/gh_mirrors/sp/spectrum点击查看免费下载相关推荐Spectrum API 架构解析Express GraphQL WebSocket 的 GraphQL-first 服务端实践Spectrum API 架构解析Express GraphQL WebSocket 的 GraphQL first 服务端实践 Spectrum 的后端前端即时通讯社交notepad-- 快速上手从安装到常用功能的实用指南notepad 快速上手从安装到常用功能的实用指南 notepad 是一款跨 Windows、Linux、macOS 的文本编辑器由国内开发者编写。你平时改桌面应用prisma-binding 完全指南基于 GraphQL Binding 构建 Prisma 服务上的 GraphQL 服务器prisma binding 完全指南基于 GraphQL Binding 构建 Prisma 服务上的 GraphQL 服务器 导读 prisma bind后端数据库GraphQL创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进