精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

91精品国产综合久久久久久久久 | 男人的天堂一区| 成人中文字幕视频| 色综合久久99| 日韩久久久久久久久久久久| 国产福利在线看| 国产电影一区在线| 国产福利成人在线| 国产奶水涨喷在线播放| 大片网站久久| 亚洲国产中文字幕久久网| 三上悠亚av一区二区三区| 免费av不卡在线观看| 国产蜜臀av在线一区二区三区| 国产66精品久久久久999小说| 黄色污污网站在线观看| 影音国产精品| 久久夜色精品国产欧美乱| 丰满圆润老女人hd| 爱爱精品视频| 91精品国产综合久久精品| 蜜臀久久99精品久久久酒店新书| 欧美黑人猛交的在线视频| 国产精品人成在线观看免费| 免费电影一区| 天天操天天干天天舔| 国产一区二区三区四区五区美女| 日本成人激情视频| 在线观看 中文字幕| 欧美日韩亚洲一区三区| 深夜精品寂寞黄网站在线观看| 真人bbbbbbbbb毛片| 中文在线综合| 日韩精品一区二区三区视频播放 | 久久高清免费视频| 午夜精品久久| 久久综合伊人77777蜜臀| 日本免费www| 成人动漫免费在线观看| 亚洲人成免费电影| 草草影院第一页| 你微笑时很美电视剧整集高清不卡| 亚洲第一免费网站| 国产在线不卡av| 麻豆成人入口| 亚洲精品久久久久国产| 亚洲av成人无码一二三在线观看| 无码国模国产在线观看| 欧美大片免费久久精品三p| 91视频福利网| 日韩成人在线看| 欧美r级电影在线观看| 国产调教打屁股xxxx网站| 欧美经典一区| 精品国产亚洲在线| 69亚洲乱人伦| 蜜桃一区二区| 在线日韩日本国产亚洲| 国产又粗又黄又猛| 999成人精品视频线3| 两个人的视频www国产精品| 久久国产波多野结衣| 一区二区三区国产精华| 九九综合九九综合| 亚洲国产成人精品激情在线| 久久国产日韩| 国产欧美在线播放| 国产草草影院ccyycom| 高潮精品一区videoshd| 久久涩涩网站| 91涩漫在线观看| 亚洲精品成人精品456| 福利视频免费在线观看| 成人香蕉视频| 欧美电影一区二区三区| 性高潮免费视频| 国产成人久久| 欧美成人合集magnet| 国产乡下妇女做爰| 免费日本视频一区| 99porn视频在线| 日韩偷拍自拍| 亚洲视频在线观看三级| www.九色.com| 456成人影院在线观看| 69p69国产精品| 中文在线永久免费观看| 成人无号精品一区二区三区| 久久国产精品亚洲| 日韩色图在线观看| 精品在线亚洲视频| 麻豆传媒一区二区| av免费在线免费观看| 欧美性xxxxhd| 久久黄色一级视频| 欧美中文一区二区| 国模叶桐国产精品一区| 伊人22222| 91网址在线看| 国产欧美久久久久| 日韩电影精品| 日韩国产欧美区| 青青草原在线免费观看视频| 日韩—二三区免费观看av| 国产伦一区二区三区色一情| 在线激情网站| 日韩人体视频一二区| 美女被艹视频网站| 日韩欧美高清| 97在线观看视频国产| 国产日韩免费视频| 国产精品麻豆久久久| 人妻内射一区二区在线视频| 日日夜夜精品视频| 久久精品视频在线播放| 波多野结衣视频网址| av综合在线播放| 999久久欧美人妻一区二区| 久久国内精品| 在线看日韩欧美| 无码人妻精品一区二区50| 不卡的av中国片| 国产日产欧美一区二区| 免费一级欧美在线观看视频| 亚洲性线免费观看视频成熟| 日韩少妇裸体做爰视频| 成人激情小说网站| www.激情网| 国产精品一区二区三区四区在线观看 | 亚洲 国产 欧美 日韩| 亚洲国产一区二区视频| 北条麻妃亚洲一区| 911精品美国片911久久久 | 一本一道精品欧美中文字幕| 久久人人爽爽爽人久久久| 国产精品又粗又长| 国产精品qvod| 久久久久久久国产精品| 亚洲国产精品无码久久| 亚洲最新视频在线播放| 免费欧美一级片| 亚洲一级毛片| 3d精品h动漫啪啪一区二区| 超碰在线观看免费版| 欧美高清dvd| 91成人精品一区二区| 青青国产91久久久久久| 亚洲精品永久www嫩草| 精品无人乱码一区二区三区| 在线看日韩欧美| 一区二区三区免费观看视频| 一色桃子久久精品亚洲| 亚洲女人在线观看| 亚洲综合色站| av在线不卡一区| 欧美在线极品| 伊人久久五月天| 亚洲无码久久久久久久| 亚洲视频一区二区在线观看| 在线观看你懂的视频| 亚洲福利国产| 蜜桃麻豆91| 美女视频一区| 欧美激情成人在线视频| 人妻精品一区二区三区| 欧美日韩在线免费观看| 黄色av免费播放| 国产综合久久久久影院| 妞干网视频在线观看| 亚洲精华一区二区三区| 国产精品丝袜高跟| 中文字幕在线观看播放| 日韩精品在线免费| 一级片视频网站| 亚洲一区二区影院| 91成年人网站| 久久99久久99精品免视看婷婷| 亚洲一一在线| 亚洲专区**| 国产高清在线不卡| 成人免费高清| 亚洲欧洲日本专区| 国产毛片在线视频| 韩曰欧美视频免费观看| 一本一本久久a久久| 丁香激情综合国产| 欧美婷婷精品激情| 99视频精品| 一级特黄录像免费播放全99| 国产精品网址| 91九色国产社区在线观看| 国产白浆在线免费观看| 社区色欧美激情 | 成人精品视频一区二区三区| 欧美日韩在线免费观看视频| 国产精品调教视频| 成人h片在线播放免费网站| 大菠萝精品导航| 色婷婷成人综合| 午夜福利理论片在线观看| 欧美电影影音先锋| 男人添女人下面高潮视频| 日韩久久中文字幕| 国产亚洲成av人在线观看导航| 一卡二卡三卡四卡五卡| 日韩精品亚洲专区| 97视频在线免费| 天天做天天爱天天爽综合网| 免费成人看片网址| 91精品国产自产在线丝袜啪| 国产精品欧美日韩久久| 日韩深夜视频| 欧美精品久久久久久久久久| 天天影视久久综合| 亚洲跨种族黑人xxx| 丰满熟妇人妻中文字幕| 欧美日韩二区三区| 国语对白做受69按摩| 亚洲成人福利片| caoporn91| 国产精品毛片无遮挡高清| 爱爱免费小视频| 91蜜桃免费观看视频| 一边摸一边做爽的视频17国产| 精品一区二区三区视频在线观看 | 欧美mv日韩mv亚洲| 99热这里只有精品66| 欧美色网一区二区| wwwwww在线观看| 日本高清无吗v一区| 欧美精品亚洲精品日韩精品| 性感美女极品91精品| 九九视频免费看| 亚洲自拍偷拍欧美| 日本a级片视频| 亚洲欧美日韩中文字幕一区二区三区| 中文字幕av久久爽一区| 久久久91精品国产一区二区精品| 国产 中文 字幕 日韩 在线| 成人精品亚洲人成在线| 在线播放av网址| 波多野结衣中文字幕一区二区三区| 亚洲区 欧美区| 国产成人在线视频免费播放| 男插女视频网站| 国产精品一区久久久久| 一区二区在线免费观看视频| 国产成人99久久亚洲综合精品| 绯色av蜜臀vs少妇| 成人福利视频在线| 亚洲自拍偷拍一区二区| 久久久久国产免费免费| 秋霞网一区二区三区| 日韩美女久久久| www青青草原| 亚洲第一激情av| 亚洲黄色小说图片| 在线视频一区二区三区| 中文字幕福利视频| 欧美丰满少妇xxxxx高潮对白| 国产成人精品白浆久久69| 精品88久久久久88久久久 | www.99久久热国产日韩欧美.com| 色综合久久影院| 美女精品视频一区| bl在线肉h视频大尺度| 日韩av高清不卡| 亚洲精品成人一区| 国产精品日韩二区| 久久不见久久见免费视频7| 伊人久久大香线蕉精品| 亚洲私拍自拍| 免费看污污网站| 国产在线麻豆精品观看| 中文在线观看免费视频| 久久精品男人天堂av| 五月天激情丁香| 粉嫩老牛aⅴ一区二区三区| 国产情侣小视频| 日韩欧美激情一区| 日韩电影在线观看完整版| 色阁综合伊人av| 丁香花电影在线观看完整版| 日本中文字幕成人| 成人免费91| 免费国产在线精品一区二区三区| 999国产精品视频| 日韩av综合在线观看| 极品少妇一区二区| 免费中文字幕av| 亚洲精品视频在线看| 色老头一区二区| 日韩精品一区在线观看| 91在线看片| 55夜色66夜色国产精品视频| 不卡的国产精品| 欧美日韩日本网| 国产综合自拍| 国产又黄又猛的视频| 久久综合国产精品| 欧美日韩免费一区二区| 欧美视频精品在线| 涩涩视频在线观看免费| 欧美理论电影在线播放| 国产精品传媒麻豆hd| 久久大片网站| 午夜日韩在线| 91女神在线观看| 国产亚洲综合在线| 97人人澡人人爽人人模亚洲| 欧美一区二区播放| 都市激情一区| 国产91在线播放九色快色| 欧美影院天天5g天天爽| 日本美女爱爱视频| 久久99久久99精品免视看婷婷| 最近中文字幕在线mv视频在线| 亚洲国产三级在线| 成人av一区二区三区在线观看| 中文字幕亚洲综合久久| 深夜成人影院| 久久婷婷国产综合尤物精品| 国产综合欧美| 免费啪视频在线观看| 亚洲欧美欧美一区二区三区| 中文字幕一区二区三区人妻四季| 亚洲精品综合久久中文字幕| www.51av欧美视频| 国产日韩欧美一区二区三区四区| 欧美国产另类| 在线观看网站黄| 亚洲精品免费播放| 国产999久久久| 久久电影一区二区| 国产一区二区视频在线看 | 国产一区二区三区成人欧美日韩在线观看 | 欧美日韩一区二区视频在线观看| 国产亚洲网站| 在线免费观看a级片| 精品国产乱码久久久久久虫虫漫画 | 国产在线精品免费av| 久草福利资源在线| 777色狠狠一区二区三区| 国产原厂视频在线观看| 亚洲一区二区三区四区视频| 欧美一区91| 在线xxxxx| 懂色av影视一区二区三区| 美女欧美视频在线观看免费| 国产成人中文字幕| 不卡日本视频| av亚洲天堂网| 亚洲婷婷综合久久一本伊一区| av在线亚洲天堂| 国内精品久久影院| 亚洲警察之高压线| 日本精品久久久久中文字幕| 国产女主播一区| 国产精品自偷自拍| 欧美激情中文字幕乱码免费| 久久99精品国产自在现线| 9久久9毛片又大又硬又粗| 久久精品日韩一区二区三区| 一本久道久久综合无码中文| 欧美伦理91i| 神马午夜久久| 超碰在线播放91| 亚洲免费在线观看视频| av免费在线不卡| 91精品国产乱码久久久久久久久| 少妇精品久久久一区二区三区| 天天色综合天天色| 伊人开心综合网| 精品美女视频在线观看免费软件 | 亚洲国产欧美在线| 六十路在线观看| 91久久大香伊蕉在人线| 国产精品久久久亚洲一区| 色偷偷男人天堂| 精品va天堂亚洲国产| 天天综合网天天| 真人做人试看60分钟免费| 久久综合999| 国产黄频在线观看| 欧洲中文字幕国产精品| 天天av综合| 欧美xxxxx精品| 欧美日韩国产综合视频在线观看| 欧美v亚洲v| 亚洲国产一区二区三区在线| 国产成人av电影在线观看| 亚洲色成人www永久网站| 欧美日韩爱爱视频| 欧美视频免费| 尤物网站在线观看| 91麻豆精品国产91久久久久久久久| 三妻四妾的电影电视剧在线观看| 中文字幕在线观看一区二区三区| 99热精品国产| 国产人妖一区二区三区|