干货 | 携程基于 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

2.13寸电子墨水屏HINK-E0213+esp8266

很久以前买的一块电子墨水屏,运气很好,这个型号HINK-E0213资料很全,而且微雪官网也有相关电路资料等。

梦境虽美,却不长. 3003

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

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

携程技术 1350

IAR下载Linux环境适配:入门级安装指导

详细介绍如何在Linux系统中完成iar下载与环境配置,帮助开发者快速上手IAR集成开发环境,解决常见安装问题,提升开发效率。

weixin_35754676的博客 762

华为OD机试真题2025双机位A卷 Python&JavaScript 实现【采购订单】

在一个采购系统中,采购申请(PR)需要经过审批后才能生成采购订单(PO)。每个PR包含商品的单价(假设相同商品的单价一定是一样的)及数量信息。系统要求对商品进行分类处理:单价高于100元的商品需要单独处理,单价低于或等于100元的相同商品可以合并到同一采购订单PO中。针对单价低于100的小额订单,如果量大可以打折购买。

MISAYAONE的博客 1459

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

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

勇往直前的专栏 699

实现电脑同时上内网和外网(或通过外网访问到该电脑通过该电脑访问内网)

一、需求描述 目前使用的公司网络只能够上内网,但是又有上外网查资料的需求: ①想要实现电脑既能上内网的同时也可以上外网; ②想要使用其他外网电脑使用外网连接到该电脑,通过该电脑访问内部的网络内容。 二、准备内容 电脑必须具备两个网卡,即:可以同时连接内网和外网(比如我的笔记本就可以:使用网线连接笔记本网口与内网通讯,使用WIFI连接手机热点访问外部网络); 注意:如果只有一个网口,可以另外购买USB的无线网卡来实现上网卡。 三、实现电脑同时上内网和外网的操作步骤 3.1、网络拓扑图 ..

提供C#相关开发 、云计算、运维测试、网络安全相关技术分享与服务,有意向可联系。 2万+

20191006

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

qq_44291044的博客 264

1、硬件工程师笔面试真题汇总(2025版本)

答:按照使用习惯,电源指示灯用红色,信号指示灯用绿色这两种颜色的LED 灯技术最成熟,价格最便宜通孔是贯穿整个PCB 的过孔盲孔是从PCB 表层连接到内层的过孔埋孔是埋在PCB 内层的过孔。大多数PCB厂家的加工能力是这样的:大于等于8mil 的过孔可以做机械孔小于等于6mil 的过孔需要做激光孔对小于等于6mil 的微型孔,在钻孔空间不够时,允许一部分过打在PCB 焊盘上。

XU157303764的博客 2万+

基于Python的军事装备数据爬取与分析:从网页抓取到数据可视化实战

本文将详细介绍如何使用Python最新技术栈构建一个完整的军事装备数据爬虫系统。我们将从爬虫基础讲起,逐步深入到异步爬取、反反爬策略、数据存储与分析等高级主题,最终实现一个能够自动抓取、清洗、存储和分析军事装备数据的完整解决方案。文章包含大量实战代码示例,涉及requests-html、aiohttp、Playwright等现代爬虫库,以及MongoDB存储、Pandas分析和Pyecharts可视化等技术。关键词:Python爬虫、军事数据、异步爬取、反反爬、数据可视化。

2201_76125261的博客 1188

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

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

weixin_30263073的博客 445

uniapp [全端兼容] - 实现集成配置百度语音识别转文字,长按或开启录音并将录音文件实时转换为文字,百度智能云API实现上传录音文件并转换成文字案例(苹果安卓App、微信小程序、移动端H5网页)

uniapp对接百度智能云语音识别,uniapp中实现语音识别(app+小程序),百度语音识别录音转文字_百度智能云,uniapp使用百度语音识别语音转文字,uniapp小程序调用百度语音识别,uni-app 百度语音识别文字并展示功能,uniapp配置自定义界面百度语音识别转文字和原生使用方式,uniapp使用百度语音,uni-app语音转文字功能,使用UniApp技术实现录音并将录音转换为文字,uniapp如何实现语音输入功能功能,Uniapp集成百度智能云API实现录音转文字详细教程完整示例代码dem

🏆 前端领域优质创作者 1万+

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

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

qq_46094651的博客 494

LVGL 组件对齐方式说明

本文说明 LVGL(以 v8 为例)中常用的 lv_align_t对齐选项的含义与用法,对应界面设计工具里下拉列表中的以及等命名。

摸鱼专家 131

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

BFF困境与思考

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

YcoeXu的博客 7562

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

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

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

weixin_39739395的博客 274

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

浅谈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、付费专栏及课程。

余额充值