干货 | 携程基于 GraphQL 的前端 BFF 服务开发实践

基于 GraphQL 的 BFF 实践 🚀 优质资源分享 🚀 学习路线指引(点击解锁) 知识定位 人群定位 🧡 Python实战微信订餐小程序 🧡 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 💛Python量化交易实战💛 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 随着软件工程的发展,系统架构越来越复杂,分层越来越多,分工也越来越细化。我们知道,互联网是离用户最近的行业,前端页面可以说无时无刻不在变化。前端本质上还是用户交互和数据 阅读详情

作者简介

 

工业聚,携程高级前端开发专家,react-lite, react-imvc, farrow 等开源项目作者。

兰迪咚,携程高级前端开发专家,对开发框架及前端性能优化有浓厚兴趣。

一、前言

过去两三年,携程度假前端团队一直在实践基于 GraphQL/Node.js 的 BFF (Backend for Frontend) 方案,在度假BU多端产品线中广泛落地。最终该方案不仅有效支撑前端团队面向多端开发 BFF 服务的需要,而且逐步承担更多功能,特别在性能优化等方面带来显著优势。

我们观察到有些前端团队曾尝试过基于 GraphQL 开发 BFF 服务,最终宣告失败,退回到传统 RESTful BFF 模式,会认为是 GraphQL 技术自身的问题。

这种情况通常是由于 GraphQL 的落地适配难度导致的,GraphQL 的复杂度容易引起误用。因此,我们期望通过本文分享我们所理解的最佳实践,以及一些常见的反模式,希望能够给大家带来一些启发。

二、GraphQL 技术栈

以下是我们 GraphQL-BFF 项目中所采用的核心技术栈:

  • • graphql

    •   基于 JavaScript 的 GraphQL 实现

  • • koa v2

    •   Node.js Web Framework 框架

  • • apollo-server-koa

    •   适配 koa v2 的 Apollo Server

  • • data-loader

    •   优化 GraphQL Resolver 内发出的请求

  • • graphql-scalars

    •   提供业务中常用的 GraphQL Scalar 类型

  • • faker

    •   提供基于类型的 Mock 数据

    •   结合 GraphQL Schema 可自动生成 Mock 数据

  • • @graphql-codegen/typescript

    •   基于 GraphQL Schema 生成 TypeScript 文件

  • • graphql-depth-limit

    •   限制 GraphQL Query 的查询深度

  • • jest

    •   单元测试框架

其他非核心或者公司特有的基础模块不再赘述。

三、GraphQL 最佳实践

携程度假 GraphQL 的主要应用场景是 IO 密集的 BFF 服务,开发面向多端所用的 BFF 服务。

所有面向外部用户的 GraphQL 服务,我们会限制只能调用其他后端 API,以避免出现密集计算或者架构复杂的情况。只有面向内部用户的服务,才允许 GraphQL 服务直接访问数据库或者缓存。

对 RESTful API 服务来说,每次接口调用的开销基本上是稳定的。而 GraphQL 服务提供了强大的查询能力,每次查询的开销,取决于 GraphQL Query 语句查询的复杂度。

因此,在 GraphQL 服务中,如果包含很多 CPU 密集的任务,其服务能力很容易受到 GraphQL Query 可变的查询复杂度的影响,而变得难以预测。

将 GraphQL 服务约束在 IO 密集的场景中,既可以发挥出 Node.js 本身的 IO 友好的优势,又能显著提高 GraphQL 服务的稳定性。

3.1 面向数据网络(Data Graph),而非面向数据接口

我们注意到有相当多 GraphQL 服务,其实是披着 GraphQL 的皮,实质还是 RESTful API 服务。并未发挥出 GraphQL 的优势,但却承担着 GraphQL 的成本。

fd0f37bf41b18ec4cf089f0f4526ef67.png

如上所示,原本 RESTful API 的接口,只是挂载到 GraphQL 的 Query 或 Mutation 的根节点下,未作其它改动。

这种实践模式,只能有限发挥 GraphQL 合并请求、裁剪数据集的作用。它仍然是面向数据接口,而非面向数据网络的。

60c34c1a69e139e9238013f57806453d.png

如此无限堆砌数据接口,最终仍然是一个发散的模型,每增加一个数据消费场景需求,就追加一个接口字段。并且,当某些接口字段的参数,依赖其它接口的返回值,常常得重新发起一次 GraphQL 请求。

而面向数据网络,呈现的是收敛的模型。

763e62dc3a5f8e8fe7d265c0859c2bf2.png

如上所示,我们将用户收藏的产品列表,放到了 User 的 favorites 字段中;将关联的推荐产品列表,放到了 Product 的 recommends 字段中;构成一种层级关联,而非并列在 Query 根节点下作为独立接口字段。

相比一维的接口列表,我们构建了高维度的数据关联网络。子字段总是可以访问到它所在得上下文里的数据,因此很多参数是可以省略的。我们在一次 GraphQL 查询中,通过这些关联字段,获取到所需的数据,而不必再次发起请求。

当逐渐打通多个数据节点之间的关联关系,GraphQL 服务所能提供的查询能力可以不断增加,最后会收敛在一个完备状态。所有可能的查询路径都已被支持,新的数据消费场景,也无须开发新的接口字段,可以通过数据关联网络查询出来。

3.2 用 union 类型做错误处理

在 GraphQL 里做错误处理,有相当多的陷阱。

第一个陷阱是,通过 throw error 将错误抛到最顶层。

假设我们实现了以下 GraphQL 接口:

31d79c06211fc20c6424e6a29a7ea339.png

当查询 addTodo 节点时,其 resolver 函数抛出的错误,将会出现在顶层的 errors 数组里,而 data.addTodo 则为 null。

37ebf38db4ec11844277cfc5e50a0615.png

不仅仅在 Query/Mutation 节点下的字段抛错会出现在顶层的 errors 数组里,而是所有节点的错误都会被收集起来。这种功能看似方便,实则会带来巨大的麻烦。

我们很难通过 errors 数组来查找错误的节点,尽管有 path 字段标记错误节点的位置,但由于以下原因,它带来的帮助有限:

  • • 总是需要过滤 errors 去找到自己关心的错误节点

  • • 查询语句是易变的,错误节点的位置可能会发生变化

  • • 任意节点都可能产生错误,要处理的潜在情形太多

这个陷阱是导致 GraphQL 项目失败的重大诱因。

错误处理在 GraphQL 项目中,比 RESTful API 更重要。后者常常只需要处理一次,而 GraphQL 查询语句可以查询多个资源。每个资源的错误处理彼此独立,并非一个错误就意味着全盘的错误;每个资源所在的节点未必都是根节点,可以是任意层级的节点。

因此,GraphQL 项目里的错误处理发生的次数跟位置都变得多样。如果无法有效地管理异常,将会带来无尽的麻烦,甚至是生产事件。长此以往,项目宣告失败也在意料之内了。

第二个陷进是,用 Object 表达错误类型。

ad5cb2760766fa2dc32a46f13ad44c4e.png

如上所示,AddTodoResult 类型是一个 Object:

  • • data 字段是一个 Object,它包含了查询结果

  • • code 字段是一个 Int,它表示错误码

  • • message 字段是一个 String,它表示错误信息

这种模式,即便在 RESTful API 中也很常见。但是,在 GraphQL 这种错误节点可能在任意层级的场景中,该模式会显著增加节点的层级。每当一个节点需要错误处理,它就多了一层 { code, data, message },增加了整体数据复杂性。

此外,code 和 message 字段的类型都带 !,表示非空。而 data 字段的类型不带 !,即可能为空。这就带来一个问题,code 为 1 表达存在错误时,data 也可能不为空。从类型上,并不能保证,code 为 1 时,data 一定为空。

也就是说,用 Object 表达错误类型是含混的。code 和 data 的关系全靠服务端的逻辑来决定。服务端需要保证 code 和 data 的出现关系,一定满足 code 为 1 时,data 为空,以及 code 为 0 时,data 不为空。

其实,在 GraphQL 中处理错误类型,有更好的方式——union type。

e852c050f247accc68229c7876fe0905.png

如上所示,AddTodoResult 类型是一个 union,包含 AddTodoError 和 AddTodoSuccess 两个类型,表示或的关系。

要么是 AddTodoError,要么是 AddTodoSuccess,但不能是两者都是。

这正是错误处理的精确表达:要么出错,要么成功。

b75e9acc217153e956a29a1835a4f287.png

查询数据时,我们用 ... on Type {} 的语法,同时查询两个类型下的字段。由于它们是或的关系,是互斥的,因此查询结果总是只有一组。

5e3bcd31552b1d7910ec97a9d353b4b5.png

失败节点的查询结果如上所示,命中了 AddTodoError 节点,伴随有 message 字段。

c1dd113608ee068976444b5a88c6e06f.png

成功节点的查询结果如上所示,命中了 AddTodoSuccess 节点,伴随有 newTodo 字段。

当使用 graphql-to-typescript 后,我们可以看到,AddTodoResult 类型定义如下:

export type AddTodoResult =
  | {
      __typename: 'AddTodoError';
      message: string;
    }
  | {
      __typename: 'AddTodoSuccess';
      newTodo: Todo;
    };


declare const result: AddTodoResult;


if (result.__typename === 'AddTodoError') {
  console.log(result.message);
} else if (result.__typename === 'AddTodoSuccess') {
  console.log(result.newTodo);
}

我们可以很容易通过共同字段 __typename 区分两种类型,不必猜测 code 和 data 字段之间的可能搭配。

union type 不局限于组合两个类型,还可以组合更多类型,表达超过 2 种的互斥场景。

c2e5037cdce1f2525dab5178de6cb19d.png

如上所示,我们把 getUser 节点的可能结果,都用 union 类型组织起来,表达更精细的查询结果,可以区分更多错误种类。

此外,union type 也不局限于做错误处理,而是任意互斥的类型场景。比如获取用户权限,我们可以把 Admin | Owner | Normal | Guest 等多种角色,作为互斥的类型,放到 UserRole 类型中。而非用 { isAdmin, isOwner, isNormal, isGuest, ... } 这类含混形式,难以处理它们同时为 false 或同时为 true 等无效场景。

3.3 用 ! 表达非空类型

在开发 GraphQL 服务时,有个非常容易疏忽的地方,就是忘记给非空类型标记 !,导致客户端的查询结果在类型上处处可能为空。

客户端判空成本高,对查询结果的结构也更难预测。

这个问题在 TypeScript 项目中影响重大,当 graphql-to-typescript 后,客户端会得到一份来自 graphql 生成的类型。由于服务端没有标记 !,令所有节点都是 optional 的。TypeScript 将会强制开发者处理空值,前端代码因而变得异常复杂和冗赘。

如果前端工程师不愿意消费 GraphQL 服务,久而久之,GraphQL 项目的用户流失殆尽,项目也随之宣告失败了。

这是反常的现象,GraphQL 的核心优势就是用户友好的查询接口,可以更灵活地查询出所需的数据。因为服务端的疏忽而丢失了这份优势,非常可惜。

善用 ! 标记,不仅有利于前端消费数据,同时也有利于服务端开发。

在 GraphQL 中,空值处理有个特性是,当一个非空字段却没有值时,GraphQL 会自动冒泡到最近一个可空的节点,令其为空。

Since Non-Null type fields cannot be null, field errors are propagated to be handled by the parent field. If the parent field may be null then it resolves to null, otherwise if it is a Non-Null type, the field error is further propagated to its parent field.

由于非空类型的字段不能为空,字段错误被传播到父字段中处理。如果父字段可能是null,那么它就会解析为null,否则,如果它是一个非null类型,字段错误会进一步传播到它的父字段。

如上,在 GraphQL Specification 的 6.4.4Handling Field Errors 中,明确了如何置空的问题。

假设我们有如下 GraphQL 接口设计:

c74839a3a097a3cb24caf3b26408bd80.png

其中,只有根节点 Query.parent 是可空的,其他节点都是非空的。

我们可以为 Grandchild 类型编写如下 GraphQL Resolver:

e059afc2c0d262c18bf884b0e6930128.png

我们概率性地分配 null 给 ctx.result(它表示该类型的结果)。尽管 Grandchild 是非空节点,但 resolver 里也能够给它置空。通过置空,告诉 GraphQL 去冒泡到父节点。否则我们就需要在 Grandchild 的层级去控制 parent 节点的值。

这是很难做到,且不那么合理的。因为 Grandchild 可以被挂到任意对象节点作为字段,不一定是当前 parent。所有 Grandchild 都可以共用一个 resolver 实现。这种情况下,Grandchild 不假设自己的父节点,只处理自己负责的数据部分,更加内聚和简单。

我们用如下查询语句查询 GraphQL 服务:

33e2702fb4910f38ce7efc727ca24a00.png

当 Grandchild 的 value 结果为 1 时,查询结果如下:

55760550a141a2e0c3e0352df9084d99.png

我们得到了符合 GraphQL 类型的结果,所有数据都有值。

当 Grandchild 的 value 结果为 null 时,查询结果如下:

d30f49f794833995ee56811db8a0555d.png

通过空值冒泡,Grandchild 的空值,被冒泡到 parent 节点,令 parent 的结果也为空。这也是符合我们编写的 GraphQL Schema 的类型约束的。如果只有 Grandchild 的 value 为 null,反而不符合类型,因为该节点是带 ! 的非空类型。

3.4 最佳实践小结

在 GraphQL 中,还有很多实践和优化技巧可以展开,大部分可以在官方文档或社区技术文章里可以找到的。我们列举的是在实践中容易出错和误解的部分,分别是:

  • • 数据网络

  • • 错误处理

  • • 空值处理

深入理解上述三个方面,就能掌握住 GraphQL 的核心价值,提高 GraphQL 成功落地的概率。

在对 GraphQL (以下简称GQL) 有一定了解的基础上,接下来分享一些我们具体的应用场景,以及项目工程化的实践。

四、GraphQL 落地

一个新的 BFF 层规划出来之后,前端团队第一个关注问题就是“我有多少代码需要重写?”,这是一个很现实的问题。新服务的接入应尽量减少对原有业务的冲击,这包括前端尽可能少的改代码以及尽可能减少测试的回归范围。由于主要工作和测试都是围绕服务返回的报文,因此首先应该让 response 契约尽可能稳定。对老功能进行改造时,接口契约可以按照以下步骤柔性进行:

  • • 保持原有服务 response 契约不变

  • • 对原有契约提供剪裁能力

  • • 在有必要的前提下设计新的字段,并且该字段也应能被剪裁。

假设之前有个前端直接调用的接口,得到 ProductData 这个JSON结构的数据。

const Query = gql`
    type ProductInfo {
        "产品全部信息"
        ProductData: JSON
    }
    extend type Query {
        productInfo(params: ProductArgs!): ProductInfo
    }
`

如上所示,一般情况我们可能会在一开始设计这样的 GQL 对象。即对服务端下发的字段不做额外的设计,而直接标注它的数据类型是JSON。这样的好处是可以很快的对原客户端调用的API进行替换。

这里 ProductData 是一个“大”对象,属性非常多,未来如果希望利用 GQL 的特性对它进行动态裁剪则需要将结构进行重新设计,类似如下代码:

const Query = gql`
    type ProductStruct {
        "产品id"
        ProductId: Int
        "产品名称"
        ProductName: String
        ......
    }
    type ProductInfo {
        "产品全部信息"
        ProductData: ProductStruct
    }
    extend type Query {
        productInfo(params: ProductArgs!): ProductInfo
    }
`

但这样做就会引入一个严重的问题:这个数据结构的修改是无法向前兼容的,老版本的 query 语句查询 ProductInfo 的时候会直接报错。为了解决这个问题,我们参考 SQL 的「Select *」扩展了一个结构通配符「json」。

4.1 JSON:查询通配符

const Query = gql`
    type ProductStruct {
        "原始数据"
        json: JSON
        "未来扩展"
        ProductId: Int
        ......
    }
    type ProductInfo {
        "产品全部信息"
        ProductData: ProductStruct
    }
    extend type Query {
        productInfo(params: ProductArgs!): ProductInfo
    }
`

如上,对一个节点提供一个 json 的查询字段,它将返回原节点全部内容,同时框架里对最终的 response 进行处理,如果碰到了 json 字段则对其解构,同时删除 json 属性。

利用这个特性,初始接入时只需要修改 BFF 请求的 request 报文,而 response 和原服务是一致的,因此无需特别回归。而未来即使需要做契约的剪切或者增加自定义字段,也只需要将 query 内容从 {json} 改成 {ProductId, ProductName, etc....} 即可。

五、GraphQL 应用场景

作为 BFF 服务,在解决单一接口快速接入之后,通常会回到聚合多个服务端接口这个最初的目的,下面是常见几种的串、并调用等应用场景。

5.1 服务端并行

21bfaa3ca4189e7670a876b7332b536c.png

如上图顶部的产品详情和下面的B线产品,分别是两个独立的产品。如果需要一次性获取,我们一般要设计一个批量接口。但利用 GQL 合并多个查询请求的特性,我们可以用更好的方式一次获取。

首先 GQL 内只需要实现单一产品的查询即可,非常简洁:

ProductInfo.resolve('Query', {
    productInfo: async (ctx) => {
        ctx.result = await productSvc.fetch(ctx.args.productId)
    }
})


const ProductInfoHandle: ProductInfo = {
    BasicInfo: async ctx => {
        let {BasicInfo} = ctx.parent
        ctx.result = {
            json: BasicInfo,
            ...BasicInfo
        }
    },
    .....
}
ProductInfo.resolve('ProductInfo', ProductInfoHandle);

客户端在查询的时候,只需要重复添加查询语句,并且传入另外一个产品参数。GQL 内会分别执行上述 resolve,如果是调用 API,则调用是并行的。

query getProductData(
    $mainParams: ProductArgs!
    $routeParams: ProductArgs!
) {
    mainProductInfo(params: $mainParams) {
        BasicInfo{json}
        .....
    }
    routeProductInfo(params: $routeParams) {
        BasicInfo{json}
        .....
    }
}


//主产品查询请求
[Node] [Inject Soa Mock]: 12345/productSvc 开始:11ms 耗时: 237ms 结束: 248ms
//子产品查询请求
[Node] [Inject Soa Mock]: 12345/productSvc 开始: 12ms 耗时: 202ms 结束: 214ms

事实上这种方式不局限在同一接口,任何客户端希望并行的接口,都可以通过这样的方式实现。即在 GQL 内单独实现查询,然后由客户端发起一次“总查询”实现服务端聚合,这样的方式避免了 BFF 层因为前端需求变更不停跟随修改的困境。这种“拼积木”的方式可以用很小的成本实现服务的快速聚合,而且配合上面提到的“json”写法,未来也具备灵活的扩展性。

5.2 服务端串行

在应用中经常还会有事务型(增删改)的操作夹在这些“查”之中。比如:

mutation TicketInfo(
    $ticketParams: TicketArgs!
    $shoppingParams: ShoppingArgs!
) {
    //查询门票 并 添加到购物车
    ticketInfo(params: $ticketParams) {
        ticketData {json}
    }
    //根据“更新后”的购物车内的商品 获取价格明细
    shoppingInfo(params: $shoppingParams) {
        priceDetail {json}
    }
}

如上所示,获取价格明细的接口调用必须串行在「添加购物车」之后,这样才不会造成商品遗漏。而此例中的「mutation」操作符可以使各查询之间串行执行,如下:

//查询门票
[Node] [Inject Soa Mock]: 12345/getTicketSvc 开始: 16ms 耗时: 111ms 结束: 127ms
//添加到购物车
[Node] [Inject Soa Mock]: 12345/updateShoppingSvc 128ms 耗时: 200ms 结束: 328ms


//根据「更新后」的购物车内的商品 获取价格明细
[Node] [Inject Soa Mock]: 12345/getShoppingSvc 开始: 330ms 耗时: 110ms 结束: 440ms

同时,在 GQL 代码里也应按照前端查询的操作符来决定是否执行“事务性”操作。

async function recommendExtraResource(ctx){
  //查询门票
  const extraResource = await getTicketSvc.fetch()
    const { operation } = ctx.info.operation;
    if (operation === 'mutation'){
        //添加到购物车内
        await updateShoppingSvc.fetch(extraResource)
    }
    ctx.result = extraResource
}


ExtraResource.resolve('Query', { recommendExtraResource });
ExtraResource.resolve('Mutation', { recommendExtraResource });

这样的设计使查询就变得非常灵活。如前端仅需要查询可用门票和价格明细并不需要默认添加到购物车内,仅需要将 mutation 换成 query 即可,服务端无需为此做任何调整。而且因为没有执行更新,且操作符变成了 query,两个获取数据的接口调用又会变成并行,提高了响应速度。

//查询门票
[Node] [Inject Soa Mock]: 12345/getTicketSvc 开始: 16ms 耗时: 111ms 结束: 127ms
//根据「当时」的购物车内的商品 获取价格明细
[Node] [Inject Soa Mock]: 12345/getShoppingSvc 开始: 18ms 耗时: 104ms 结束: 112ms

5.3 父子查询中的重复请求

我们经常会碰到一个接口的入参,依赖另外一个接口的 response。这种将串行调用从客户端移到服务端的做法可以有效的降低端到端的次数,是 BFF 层常见的优化手段。但是如果我们有多个节点一起查询时,可能会出现同一个接口被调用多次的问题。对应这种情况,我们可以使用 GQL 的 data-loader。

ProductInfo.resolve('Query', {
      productInfo: async (ctx) => {
        let productLoader = new DataLoader(async RequestType => {
            // RequestType 为数组,通过子节点的 load 方法,去重后得到。
            let response = await productSvc.fetch({ RequestType })
            return Array(RequestType.length).fill(response)
        })
        ctx.result = { productLoader }
    }
})


ExtendInfo.resolve('Product',{
    extendInfo: async (ctx) => {
        const BasicInfo = await ctx.parent.productLoader.load("BasicInfo")
        ctx.result = await extendSvc.fetch(BasicInfo)
    }
})

如上,在父节点的 resolve 里构造 loader,通过 ctx.result 传递给子节点。子节点调用 load(arg) 方法将参数添加到 loader 里,父节点的 loader 根据“积累”的参数,发起真正的请求,并将结果分别下发对应地子节点。在这个过程中可以实现相同的请求合并只发一次。

六、工程化实践

6.1 异常处理

7caf79e2398f68313375c859931cc76f.png

在 GQL 关联查询中父节点失败导致子节点异常的情况很常见。而这个父子关系是由前端 query 报文决定的,因此需要我们在服务端处理异常的时候,清晰地通过日志等方式准确描述原因,上图可以看出 imEnterInfo 节点异常是由于依赖的 BasicInfo 节点为空,而根因是依赖的 API 返回错误。这样的异常处理设计对排查 GQL 的问题非常有帮助。

6.2 虚拟路径

由于 GQL 唯一入口的特性,服务捕获到的访问路径都是 /basename/graphql,导致定位错误很困难。因此我们扩展了虚拟路径,前端查询的时候使用类似「/basename/graphql/productInfo」。这样无论是日志、还是 metric 等平台等都可以区分于其他查询。

并且这个虚拟路径对 GQL 自身不会造成影响,前端甚至可以利用这个虚拟路径来测试 query 的节点和 BFF 响应时长的关系。如:H5 平台修改了首屏 query 的内容之后将请求路径改成 “/basename/graphql/productInfo_h5”,这样就可以通过性能监控95线等方式,对比看出这个“h5”版本对比其他版本性能是否有所下降。

在很多优化首屏的实践中,利用 GQL 动态查询,灵活剪切契约等是非常有效的手段。并且在过程中,服务端并不需要跟随前端调整代码。降低工作量的同时,也保证了其他平台的稳定性。

6.3 监控运维

GQL 的特性也确实造成了现有的运维工具很难分析出哪个节点可以安全废弃(删除代码)。因此需要我们在 resolve 里面对节点进行了埋点。

e8bfcb4c0a68288fc9757cab49feaf6c.png

6.4 单元测试

我们利用 jest 搭建了一个测试框架来对 GQL BFF 进行单元测试。与一般单测不同的是,我们选择在当前运行环境内单独起一个服务进程,并且引入“@apollo/client”来模拟客户端对服务进行查询,并校验结果。

5b1fd07a19c379e2ebfa4939a3d0c691.png

其他诸如 CI/CD、接口数据 mock、甚至服务的心跳检测等更多的属于 node.js 的解决方案,就不在这里赘述了。

七、总结

鉴于篇幅原因,只能分享部分我们应用 GraphQL 开发 BFF 服务的思考与实践。由前端团队开发维护一套完整的服务层,在设计和运维方面还是有不小的挑战,但是能赋予前端团队更大的灵活自主性,对于研发迭代效率的提升也是显著的。

希望对大家有所帮助,欢迎更多关于 GraphQL 的实践和交流。

【推荐阅读】

0e5c949bbf32271a57eff70aff76b647.png

 “携程技术”公众号

  分享,交流,成长

GraphQL及元数据驱动架构在后端BFF中的实践 1 BFF的由来2 BFF背景下的核心矛盾3 BFF应用模式分析3.1 后端BFF模式3.2 前端BFF模式4 基于GraphQL及元数据的信息聚合架构设计4.1 整体思路4.2 核心设计5 针对GraphQL的优化实践5.1 使用简化5.2 性能优化6 新架构对研发模式的影响6.1 聚焦业务的开发模式6.2 研发流程升级7 总结8 参考文献9 招聘信息1 BFF的由来BFF一词来自Sam Newman的一篇博文《Pattern:Backends For Frontends》,指的是服务于前端的后端。BFF 阅读详情

相关推荐

后端架构模式之-BFF(Backend-For-Frontend)

在传统架构中,前端通常直接与一个通用的后端 API(如 RESTful 或 GraphQL API)交互,而 BFF 引入了一层专门的后端,让不同类型的前端(Web、iOS、Android、小程序等)有各自独立的 BFF 层。,为不同前端提供定制化 API,以优化数据传输、减少 API 调用次数、提升前端开发效率,并降低后端改动对前端的影响。: ✅ 需要支持多个前端(Web、移动端、小程序等)。,它为特定的前端应用(Web、移动端、桌面端等)提供。,作为前端与通用后端(或微服务)之间的中间层。

踏踏实实,贵在坚持 3767

干货 | 基于 GraphQL 的前端 BFF 服务开发实践

过去两三年,携程度假前端团队一直在实践基于 GraphQL/Node.js 的 BFF (Backend for Frontend) 方案,在度假BU多端产品线中广泛落地。最终该方案不仅有效支撑前端团队面向多端开发 BFF 服务的需要,而且逐步承担更多功能,特别在性能优化等方面带来显著优势。我们观察到有些前端团队曾尝试过基于 GraphQL 开发 BFF 服务,最终宣告失败,退回到传统 RESTful BFF 模式,会认为是 GraphQL 技术自身的问题。

2401_83384536的博客 1307

Unity2020 发布Android包,下载安装配置JDK、SDK、NDK详细教程

配置成功页面 JDK、SDK、NDK

qq_41263535的博客 1万+

干货 | 万字长文全面解析GraphQL,携程微服务背景下的前后端数据交互方案

作者简介古映杰,携程研发高级经理,负责前端框架和基础设施的设计、研发与维护。开源项目react-lite和react-imvc作者。前言随着多终端、多平台、多业务形态、多...

携程技术 1350

【2022KaliLinux】openvas/gvm 没有scan configs 或者是 scan config error “XXXXXXXXXXX”

gvm/openvas的configuration的scan config为空

zbossz的学习小分队 3606

人工势场法三大致命缺陷?我用ROS+Gazebo做了组对比实验

本文通过ROS与Gazebo仿真实验,深入剖析了人工势场法(APF)在机器人路径规划中的三大经典问题:局部最小值、目标不可达和路径震荡。文章不仅复现了这些缺陷,还对比测试了虚拟目标点法、改进斥力场及运动平滑等主流改进方案,为工程选型提供了基于数据的实用建议。

dapp9builder的博客 219

干货 | 携程度假无线前端架构演进之路

作者简介 Jade Gu,携程高级前端开发专家,负责度假前端框架设计和 Node.js 基础设施建设等工作。 这篇文章将简略地介绍我们当前的无线前端架构设计及其演进之路。主要内容包含以下几个部分,希望我们的经验能带给大家一些启发。 1)当前的前端方案及其解决的问题 2)现在面对的新挑战 3)我们的前端方案设计和选择。 一、当前的前端方案及其解决的问题 1.1 当前方案的技术背景 将时间调回到 2016 年。我们已经将几个核心的前端应用,从 C# ASP.NET 迁移到了 Node.js。.

勇往直前的专栏 699

TMS320F28377D的sys/bios双核工程配置详解——从零搭建到RAM优化

本文详细解析了TMS320F28377D双核系统在sys/bios环境下的工程配置与RAM优化策略。从开发环境搭建、SYS/BIOS工程创建到双核CMD文件配置,提供了实战技巧和常见问题解决方案,帮助开发者高效实现电力电子控制系统中的双核协作与性能优化。

weixin_28292761的博客 600

20191006

☐干货满满的 Go Modules 和 GoProxy: https://segmentfault.com/a/1190000020522261 ☐GraphQL-BFF:微服务背景下的前后端数据交互方案:https://media.weibo.cn/article?id=2309404398215846821987&from=timeline ☐解决Django应用程序中的内存泄漏问题:...

qq_44291044的博客 264

LIS3DHTR三轴加速度计Arduino驱动与嵌入式集成指南

加速度计是嵌入式系统中实现运动感知、姿态解算和振动分析的核心传感器。其工作原理基于MEMS微机械结构在惯性力作用下的电容变化,经片内ADC转换为数字信号;I²C接口因其硬件资源占用少、布线简洁,成为低功耗物联网节点的主流通信方式。技术价值体现在高分辨率(12位)、多量程(±2g~±16g)与低功耗(待机电流仅2µA)的平衡,支撑电池供电设备长期运行。典型应用场景包括可穿戴设备的姿态识别、工业预测性维护中的振动监测,以及智能硬件的敲击/跌落检测。本文围绕LIS3DHTR芯片及其Grove模块,详解Arduin

weixin_42584758的博客 595

SIMULINK开发项目实例 1000 例专栏之第802例--基于电机驱动系统的场景实例:永磁同步电机(PMSM)在电动汽车中的矢量控制仿真

随着全球对环境保护的关注增加,电动汽车(Electric Vehicle, EV)因其环保特性而受到广泛关注。永磁同步电机(Permanent Magnet Synchronous Motor, PMSM)由于其高效率、高功率密度和良好的动态响应性能,在电动汽车驱动系统中得到了广泛应用。矢量控制(Field-Oriented Control, FOC)是实现PMSM高性能控制的关键技术之一,它能够实现对电机转矩和磁场的独立控制,从而达到类似直流电机的控制效果。本文将详细介绍如何使用来构建一个。

xiaoheshang_123的博客 269

现代前端工程师核心职责与能力演进:从切图仔到体验架构师

前端开发作为连接用户与数字世界的桥梁,其核心在于将设计转化为可交互的界面。其技术原理基于HTML、CSS和JavaScript三大基石,通过组件化、状态管理等工程化实践,构建复杂应用。这一过程的技术价值在于实现高性能、可访问且安全的用户体验,直接影响用户留存与产品成功。在现代Web开发中,前端工程师的职责已从简单的页面制作,扩展到涵盖性能优化、工程化协作、跨端开发乃至架构设计的立体体系。随着React、Vue等框架生态的成熟,以及Node.js带来的全栈能力拓展,前端岗位要求开发者不仅要精通视觉还原与交互实

weixin_30263073的博客 445

华为OD机试双机位A卷 - VLAN资源池 (C++ & Python & JAVA & JS & GO)

华为OD机试2025双机位A卷 VLAN资源池,提供Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解

qq_45776114的博客 879

【收藏级实战】一周搞定研发平台 Agent 接入!TQL 专属 Agent 开发全攻略(附源码思路)

作为一名深耕技术领域的创作者,深知大家对大模型 Agent 落地的迫切需求 —— 尤其像你这样掌握 Java、后端技术的同学,想切入 AI 大模型领域却不知从何下手?今天这篇超全实战干货,手把手教你 7 天内为传统研发平台接入专属 Agent,解决连续对话、Token 消耗高、上下文失控等核心难题,看完就能上手实操!背景简介奥德赛研发平台是 ICBU 买家技术的 TQL(淘宝基于开源 GraphQL 的定制版本) 研发平台,大量开发者会在上面通过编写 TQL 脚本来实现 BFF 接口。

qq_46094651的博客 494

【25届数字IC秋招总结】面试经验6——经纬恒润

25届数字ic面试记录——经纬恒润

lalala0635的博客 991

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

BFF困境与思考

目录标题BFF困境与思考一、BFF在整体微服务架构中的位置二、BFF是什么1.前端与BFF服务由同一个团队负责2.BFF层应该足够薄3.BFF有明确的服务终端三、BFF服务如何划分1. 按产品拆分2. 按模块拆分 BFF困境与思考 一、BFF在整体微服务架构中的位置 微服务化带来的最大问题是服务边界的划分。可以从两个维度看:纵向职能和横向业务的划分。   纵向划分,BFF在网关与领域服务之间比较容...

YcoeXu的博客 7562

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

react项目_GraphQL + Koa + React 项目实践

项目背景2019 年 11 月 16 日FCC 成都社区主办的Web 全栈大会上,尹吉峰老师关于 GraphQL 的分享让我产生了浓厚的兴趣。GraphQL 是一个用于 API 的查询语言,是使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。一个 GraphQL 服务是通过定义类型和类型上的字段来创建的,然后给每个类型上的每个字段提供解析函数。参考学习资料:http...

weixin_39739395的博客 274

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

浅谈BFF架构

BFF是服务于前端的后端,全称。BFF的位置在与客户端与服务端中间,起到一个中间件的作用。

weixin_51220967的博客 7577

GraphQL学习第四篇 -在Koa中使用GraphQL

在Koa中使用GraphQL主要有以下几步: 1. 安装 graphql、koa-graphql 和 koa-mount。 2.引入koa-mount 和 koa-graphql。 3.引入自定义的schema,其中定义schema又分为以下几步: (1).定义查询字段的schema类型。 (2).定义一个根, 根里面定义调用schema类型的方法。 (3).把根挂载到G..................

分享前端工程化、AI 应用、Agent 开发与大模型实践! 1095

都快2022年了GraphQL还值得学吗?

GraphQL感觉前两年火过一小段时间,说是能让前端只取我所需,解决后端接口过多划分地过细的问题,再加上是Facebook出品确实吸引了一些开发者的关注,不过我还没来得及学,好像它就过气了...

kevin_tech的博客,微信搜「网管叨bi叨」 6939

GraphQL入门 GraphQL入门有这一篇就足够了

GraphQL入门有这一篇就足够了 一、GraphQL是什么? 关于GraphQL是什么,网上一搜一大堆。根据官网的解释就是一种用于 API 的查询语言。 GraphQL 是由 Facebook 创造的用于描述复杂数据模型的一种查询语言。这里查询语言所指的并不是常规意义上 的类似 sql 语句的查询语言,而是一种用于前后端数据查询方式的规范。 官网(中文):https://graphql.cn/ 规范地址:http://spec.graphql.cn/ 引用官方文档的..

liuerchong的博客 993
上一篇: 干货 | 携程实体链接技术的探索及实践
下一篇: 干货 | 百亿节点,毫秒级延迟,携程金融基于nebula的大规模图应用实践
携程技术
博客等级 码龄10年 3716粉丝 324原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值