精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

攜程基于 GraphQL 的前端 BFF 服務開發實踐

開發 前端
過去兩三年,攜程度假前端團隊一直在實踐基于 GraphQL/Node.js 的 BFF (Backend for Frontend) 方案,在度假BU多端產品線中廣泛落地。最終該方案不僅有效支撐前端團隊面向多端開發 BFF 服務的需要,而且逐步承擔更多功能,特別在性能優化等方面帶來顯著優勢。

作者:

  • 工業聚,攜程高級前端開發專家,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 的成本。

圖片

如上所示,原本 RESTful API 的接口,只是掛載到 GraphQL 的 Query 或 Mutation 的根節點下,未作其它改動。

這種實踐模式,只能有限發揮 GraphQL 合并請求、裁剪數據集的作用。它仍然是面向數據接口,而非面向數據網絡的。

圖片

如此無限堆砌數據接口,最終仍然是一個發散的模型,每增加一個數據消費場景需求,就追加一個接口字段。并且,當某些接口字段的參數,依賴其它接口的返回值,常常得重新發起一次 GraphQL 請求。

而面向數據網絡,呈現的是收斂的模型。

圖片

如上所示,我們將用戶收藏的產品列表,放到了 User 的 favorites 字段中;將關聯的推薦產品列表,放到了 Product 的 recommends 字段中;構成一種層級關聯,而非并列在 Query 根節點下作為獨立接口字段。

相比一維的接口列表,我們構建了高維度的數據關聯網絡。子字段總是可以訪問到它所在得上下文里的數據,因此很多參數是可以省略的。我們在一次 GraphQL 查詢中,通過這些關聯字段,獲取到所需的數據,而不必再次發起請求。

當逐漸打通多個數據節點之間的關聯關系,GraphQL 服務所能提供的查詢能力可以不斷增加,最后會收斂在一個完備狀態。所有可能的查詢路徑都已被支持,新的數據消費場景,也無須開發新的接口字段,可以通過數據關聯網絡查詢出來。

3.2 用 union 類型做錯誤處理

在 GraphQL 里做錯誤處理,有相當多的陷阱。

第一個陷阱是,通過 throw error 將錯誤拋到最頂層。

假設我們實現了以下 GraphQL 接口:

圖片

當查詢 addTodo 節點時,其 resolver 函數拋出的錯誤,將會出現在頂層的 errors 數組里,而 data.addTodo 則為 null。

圖片

不僅僅在 Query/Mutation 節點下的字段拋錯會出現在頂層的 errors 數組里,而是所有節點的錯誤都會被收集起來。這種功能看似方便,實則會帶來巨大的麻煩。

我們很難通過 errors 數組來查找錯誤的節點,盡管有 path 字段標記錯誤節點的位置,但由于以下原因,它帶來的幫助有限:

  • 總是需要過濾 errors 去找到自己關心的錯誤節點
  • 查詢語句是易變的,錯誤節點的位置可能會發生變化
  • 任意節點都可能產生錯誤,要處理的潛在情形太多

這個陷阱是導致 GraphQL 項目失敗的重大誘因。

錯誤處理在 GraphQL 項目中,比 RESTful API 更重要。后者常常只需要處理一次,而 GraphQL 查詢語句可以查詢多個資源。每個資源的錯誤處理彼此獨立,并非一個錯誤就意味著全盤的錯誤;每個資源所在的節點未必都是根節點,可以是任意層級的節點。

因此,GraphQL 項目里的錯誤處理發生的次數跟位置都變得多樣。如果無法有效地管理異常,將會帶來無盡的麻煩,甚至是生產事件。長此以往,項目宣告失敗也在意料之內了。

第二個陷進是,用 Object 表達錯誤類型。

圖片

如上所示,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。

圖片

如上所示,AddTodoResult 類型是一個 union,包含 AddTodoError 和 AddTodoSuccess 兩個類型,表示或的關系。

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

這正是錯誤處理的精確表達:要么出錯,要么成功。

圖片

查詢數據時,我們用 ... on Type {} 的語法,同時查詢兩個類型下的字段。由于它們是或的關系,是互斥的,因此查詢結果總是只有一組。

圖片

失敗節點的查詢結果如上所示,命中了 AddTodoError 節點,伴隨有 message 字段。

圖片

成功節點的查詢結果如上所示,命中了 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 種的互斥場景。

圖片

如上所示,我們把 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 接口設計:

圖片

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

我們可以為 Grandchild 類型編寫如下 GraphQL Resolver:

圖片

我們概率性地分配 null 給 ctx.result(它表示該類型的結果)。盡管 Grandchild 是非空節點,但 resolver 里也能夠給它置空。通過置空,告訴 GraphQL 去冒泡到父節點。否則我們就需要在 Grandchild 的層級去控制 parent 節點的值。

這是很難做到,且不那么合理的。因為 Grandchild 可以被掛到任意對象節點作為字段,不一定是當前 parent。所有 Grandchild 都可以共用一個 resolver 實現。這種情況下,Grandchild 不假設自己的父節點,只處理自己負責的數據部分,更加內聚和簡單。

我們用如下查詢語句查詢 GraphQL 服務:

圖片

當 Grandchild 的 value 結果為 1 時,查詢結果如下:

圖片

我們得到了符合 GraphQL 類型的結果,所有數據都有值。

當 Grandchild 的 value 結果為 null 時,查詢結果如下:

圖片

通過空值冒泡,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 服務端并行

圖片

如上圖頂部的產品詳情和下面的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 異常處理

圖片

在 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 里面對節點進行了埋點。

圖片

6.4 單元測試

我們利用 jest 搭建了一個測試框架來對 GQL BFF 進行單元測試。與一般單測不同的是,我們選擇在當前運行環境內單獨起一個服務進程,并且引入“@apollo/client”來模擬客戶端對服務進行查詢,并校驗結果。

圖片

其他諸如 CI/CD、接口數據 mock、甚至服務的心跳檢測等更多的屬于 node.js 的解決方案,就不在這里贅述了。

七、總結

鑒于篇幅原因,只能分享部分我們應用 GraphQL 開發 BFF 服務的思考與實踐。由前端團隊開發維護一套完整的服務層,在設計和運維方面還是有不小的挑戰,但是能賦予前端團隊更大的靈活自主性,對于研發迭代效率的提升也是顯著的。

責任編輯:未麗燕 來源: 攜程技術
相關推薦

2024-09-10 16:09:58

2022-07-15 12:58:02

鴻蒙攜程華為

2023-08-11 09:13:27

2022-06-03 09:21:47

Svelte前端攜程

2016-09-04 15:14:09

攜程實時數據數據平臺

2022-05-19 17:50:31

bookie集群延遲消息存儲服務

2018-04-23 14:31:02

微服務GraphQLBFF

2023-08-25 09:51:21

前端開發

2023-06-06 11:49:24

2022-05-13 09:27:55

Widget機票業務App

2023-06-28 14:01:13

攜程實踐

2022-08-20 07:46:03

Dynamo攜程數據庫

2023-07-07 12:26:39

攜程開發

2023-11-24 09:44:07

數據攜程

2022-08-12 08:34:32

攜程數據庫上云

2023-02-08 16:34:05

數據庫工具

2022-07-15 09:20:17

性能優化方案

2022-07-08 09:38:27

攜程酒店Flutter技術跨平臺整合

2022-12-16 09:29:23

攜程微服務

2022-09-09 15:49:03

攜程火車票組件化管理優化
點贊
收藏

51CTO技術棧公眾號

丝袜美腿高跟呻吟高潮一区| 韩国中文字幕hd久久精品| 久久99青青| 欧美欧美欧美欧美| 国产中文一区二区| 久久久久久久久久久久久久免费看| 在线视频亚洲欧美中文| 国产精品污网站| av日韩中文字幕| 日韩在线中文字幕视频| 红杏视频成人| 欧美日韩日日摸| 五月天色一区| 亚洲国产www| 国产精品成人a在线观看| 日韩欧美色综合网站| 国产精品第12页| 在线观看免费视频你懂的| 久久一区二区三区四区| 亚洲综合av影视| 日韩免费av网站| 国产在线成人| 亚洲成人精品久久久| 北条麻妃av高潮尖叫在线观看| 日本高清在线观看wwwww色| 欧美日韩国产免费观看| 在线综合视频播放| 中文字幕日韩一区二区三区| 欧美一级做a爰片免费视频| 制服丝袜在线一区| julia一区二区三区中文字幕| 国产精品久久午夜夜伦鲁鲁| 日本精品一区二区三区在线| 开心激情五月网| 激情综合婷婷| 亚洲精品日韩综合观看成人91| 精品久久精品久久| 国产麻豆精品一区| 麻豆91精品| 国产亚洲一区二区精品| 黄色一级一级片| 91麻豆一二三四在线| 国产精品亚洲第一| 国产精品久久久久高潮| 国产91av视频| 这里只有精品在线| 中文字幕一区电影| 右手影院亚洲欧美| 日韩精品免费视频一区二区三区 | 五月天欧美精品| 亚洲精品影院| 欧洲亚洲在线| 不卡影院免费观看| 91亚洲国产成人久久精品网站| 中文字幕资源站| 国产精品美女久久久久久不卡 | 天天色综合av| 国产麻豆精品在线| 久久久久久久久电影| 91香蕉一区二区三区在线观看| 欧美美乳视频| 日韩av一区在线观看| 另类小说色综合| 澳门av一区二区三区| 欧美日韩国产精品一区二区不卡中文| 亚洲五码在线观看视频| 亚洲一区二区三区四区av| 免费黄色网页在线观看| 成人欧美一区二区三区白人| 亚洲一卡二卡区| 1769在线观看| 国产麻豆日韩欧美久久| 亚洲a中文字幕| 日韩一区二区视频在线| 日韩午夜电影| 原创国产精品91| 欧美狂猛xxxxx乱大交3| 日本免费一区二区视频| 色婷婷激情综合| 97xxxxx| 日本成人网址| 中文字幕日本乱码精品影院| 亚洲人久久久| 黄色免费在线看| 91首页免费视频| 久久久久久久久久久久久久久久av| 国产成人精品一区二区色戒| 日本欧美韩国一区三区| 国产精品美女视频网站| 亚欧视频在线观看| 亚洲成人精品| 亚洲女同精品视频| 永久免费毛片在线观看| 成人av激情人伦小说| 欧美影视一区在线| 亚洲a级黄色片| 在线精品国产亚洲| 欧美美女直播网站| 超碰人人cao| 久久免费影院| 欧美日韩亚洲系列| 日本成人黄色网| 亚洲电影二区| 亚洲а∨天堂久久精品喷水| 中文字幕一区二区三区人妻| 国产成人免费av一区二区午夜| 欧美三级视频在线观看| 尤物国产在线观看| 免费欧美电影| 欧美日韩中国免费专区在线看| 欧美在线观看www| 国产美女av在线| 国产精品免费看片| 日韩欧美激情一区二区| a级在线观看| 欧美色播在线播放| aaa一级黄色片| 少妇高潮一区二区三区99| 色婷婷国产精品| 午夜一区二区视频| 老牛影视av一区二区在线观看| 亚洲最新av网址| 亚洲自拍偷拍图| 秋霞蜜臀av久久电影网免费| 中文字幕亚洲无线码a| 精品少妇theporn| 免费在线看成人av| 激情小说网站亚洲综合网| www.色呦呦| 国产乱子伦视频一区二区三区| 久久成人资源| 嫩草研究院在线观看| 91丨国产丨九色丨pron| 久草精品电影| 影音先锋在线视频| 亚洲综合清纯丝袜自拍| 中文字幕第80页| 国产精品15p| 久久五月天色综合| 久久久久久国产精品免费播放| 亚洲精品二区三区| 国产精国产精品| 日韩一级免费毛片| 亚洲精品自拍动漫在线| 婷婷六月天在线| 亚洲美女久久| 最近中文字幕2019免费| 性生交大片免费全黄| 久久精品麻豆| 成人在线一区二区| aaa日本高清在线播放免费观看| 天天色综合天天| 亚洲欧美日韩中文字幕在线观看| 大片网站久久| 欧美美女18p| 在线观看视频中文字幕| 久久天堂av综合合色蜜桃网| 青青草精品视频在线| 色尼玛亚洲综合影院| 亚洲国产99精品国自产| 国产在线视频卡一卡二| 国产盗摄视频一区二区三区| 99热这里只有精品7| 久久夜夜久久| 日韩亚洲成人av在线| 中文天堂在线视频| 亚洲国产精品t66y| 欧美激情亚洲天堂| 蜜桃在线一区| 欧美噜噜久久久xxx| www.黄色国产| 95精品视频在线| 欧美一区二区三区四区夜夜大片 | 欧美日韩国产a| 欧美性猛交乱大交| 自由日本语亚洲人高潮| 91免费观看网站| a级网站在线播放| 欧美一级精品在线| 偷拍夫妻性生活| 影视一区二区| 91精品天堂| 里番在线播放| 欧美色国产精品| 国产黑丝在线观看| 一区二区三区四区五区精品视频 | 成人黄色免费短视频| 亚洲精品自在久久| 免费看污视频的网站| 国产成人午夜视频| 精品国产一区二区三区无码| 欧美wwwwww| 久久999免费视频| 亚洲精品成人电影| 国产精品入口麻豆原神| 女人高潮一级片| 亚洲第一网站| 日本高清不卡三区| 国产夫妻在线播放| 亚洲女人被黑人巨大进入al| 国产精品成人久久| 91丨九色丨黑人外教| 无需播放器的av| 欧美激情aⅴ一区二区三区| 国产精品久在线观看| 免费大片黄在线| 精品视频一区二区三区免费| 久久久精品人妻无码专区| 欧美精品一卡| 亚洲在线视频观看| 成人在线app| 欧美日韩国产首页在线观看| 玖玖爱免费视频| 久久久99久久精品欧美| av天堂永久资源网| 久久精品国产99久久| www.成人三级视频| 日本综合视频| 在线电影欧美日韩一区二区私密| 性无码专区无码| 国产亲近乱来精品视频| 亚洲少妇中文字幕| 麻豆9191精品国产| 欧美成人综合一区| 国产999精品在线观看| 欧美激情视频免费观看| 成人在线免费视频| 欧美一区日本一区韩国一区| 欧美成人精品欧美一级私黄| caoporn国产一区二区| 日韩在线不卡一区| aa国产精品| 国产在线拍揄自揄拍无码| 国产精品一站二站| 久久手机免费视频| 国产一区二区三区四区视频| 亚洲色图.com| 能免费看av的网站| 成人一区二区三区在线观看| 国产成人一区二区三区别| 成人在线免费观看91| 欧美aaaaa喷水| 精品人人人人| 99精品国产高清在线观看| 欧洲一区二区三区| 精品久久国产精品| 浮生影视网在线观看免费| 欧美日韩免费观看一区二区三区| 天天干天天干天天操| 亚洲成a人片在线不卡一二三区| 国产午夜手机精彩视频| 国产精品入口麻豆九色| 国产精品igao网网址不卡| 国内精品嫩模av私拍在线观看| 国产一区免费在线| 亚洲精品国产九九九| 91久久精品国产91久久性色| 午夜av在线免费观看| 最近2019年日本中文免费字幕| 亚洲欧美日本在线观看| 亚洲成人亚洲激情| 免费黄色小视频在线观看| 图片区小说区国产精品视频| 精品少妇theporn| 日本一区二区在线不卡| 亚洲а∨天堂久久精品2021| 久久九九国产精品| 国产在线观看h| 国产视频911| 日韩不卡av在线| 成人国产精品免费观看动漫 | 久久免费视频99| 久久综合九色综合97婷婷女人 | 精品在线一区二区三区| 国产精品久久a| 免费成人av在线播放| 最新中文字幕2018| 久久99精品久久久| 国产资源中文字幕| 日韩成人伦理电影在线观看| 玩弄japan白嫩少妇hd| 欧美日韩国产高清| 欧美这里只有精品| 日韩欧美国产精品综合嫩v| 日韩成人av电影在线| 欧美日韩麻豆| 欧美一区少妇| 风间由美一区二区av101| 国产欧美日韩高清| 麻豆国产精品| 国产欧美日韩精品丝袜高跟鞋| 亚洲aⅴ网站| 国产99午夜精品一区二区三区| 久久精品国产精品亚洲毛片| 亚洲一区二区少妇| 久久精品国产亚洲blacked| 亚洲在线www| 9999精品免费视频| 国产成人精品一区二区三区福利| 亚洲影视资源| 国产精品午夜视频| 日韩精品一区国产| 精品在线一区| 首页国产精品| 日本在线xxx| 免费观看30秒视频久久| 性生活在线视频| 久久伊人蜜桃av一区二区| 国产美女高潮视频| 精品日韩美女的视频高清| 一级黄色a毛片| 亚洲第一福利视频| 亚洲爱爱综合网| 亚洲偷熟乱区亚洲香蕉av| 国产视频网址在线| 欧美成人小视频| 色呦呦在线免费观看| 美日韩精品免费观看视频| 久久亚洲导航| 国产精品一香蕉国产线看观看| 4438全国亚洲精品观看视频| 亚洲不卡中文字幕| 一区二区高清| 青青草原成人网| 国产毛片精品视频| 手机免费看av| 国产精品日日摸夜夜摸av| a一级免费视频| 亚洲欧美日韩综合aⅴ视频| chinese国产精品| 精品欧美一区二区在线观看| 蜜桃视频久久一区免费观看入口| 日韩欧美亚洲一区二区| 99中文字幕一区| 日本成人在线视频网址| 亚洲乱码一区| 欧美aaa在线观看| 免费在线亚洲欧美| 日韩欧美中文在线视频| 91麻豆国产在线观看| 国产性生活毛片| 久久久噜噜噜久久中文字幕色伊伊 | 成人在线爆射| 国产九色精品| 午夜国产精品视频免费体验区| 日韩爱爱小视频| 精品一区二区三区在线播放| 麻豆精品免费视频| 日韩久久一区二区| 中文字幕日日夜夜| 日韩精品中文字幕一区| 亚洲人午夜射精精品日韩| 欧美大胆在线视频| 日韩一区二区三区四区五区| 北条麻妃高清一区| 香蕉视频国产精品| 污色网站在线观看| 日本一区二区在线不卡| 中文字幕手机在线视频| 亚洲色图50p| 四虎影院观看视频在线观看| 成人黄色影片在线| 久久中文字幕二区| 日韩一级片免费视频| 葵司免费一区二区三区四区五区| 黄色录像a级片| 国产精品久久二区二区| 免费在线不卡av| 亚洲大胆人体视频| 在线观看的av| 久热99视频在线观看| 99精品视频在线免费播放| 最新av在线免费观看| 国产乱妇无码大片在线观看| 青青草手机视频在线观看| 日韩欧美不卡一区| av在线免费播放网站| 97国产suv精品一区二区62| 精品久久在线| 性做爰过程免费播放| 日韩影院免费视频| 国产伦精品一区三区精东| 亚欧色一区w666天堂| 国产三级在线观看视频| 久久久精品久久| 日本电影欧美片| 欧美高清性xxxxhd | 久久精品一区中文字幕| 黑人巨大亚洲一区二区久| 麻豆亚洲一区| 久久狠狠婷婷| 无码少妇精品一区二区免费动态| 亚洲欧美日本韩国| 国产91免费在线观看| 久久久999精品免费| 成人av资源网址| 日韩av播放器| 久久久亚洲午夜电影| 日本三级一区二区| 欧美成人a∨高清免费观看|