GraphQL là cú pháp mô tả dữ liệu mà máy khách yêu cầu từ máy chủ. Trong trường hợp này, máy khách là ứng dụng di động. GraphQL thường được so sánh với REST API, một cú pháp phổ biến mà hầu hết các nhà phát triển ứng dụng di động sử dụng. Chúng tôi sẽ chia sẻ cách GraphQL có thể giải quyết một số điểm khó khăn của REST API trong phát triển ứng dụng di động và thảo luận về các mẹo và phương pháp hay nhất mà chúng tôi đã học được tại Shopify bằng cách sử dụng GraphQL trong các ứng dụng di động của mình.

Tại sao nên sử dụng GraphQL?

Một ứng dụng di động thường có bốn lớp cơ bản trong cơ sở mã:

  1. Lớp mạng: xác định kết nối và máy chủ để kết nối để gửi/nhận dữ liệu.
  2. Lớp mô hình dữ liệu: chuyển đổi dữ liệu đến từ lớp mạng thành dữ liệu dễ hiểu cho các mô hình ứng dụng cục bộ.
  3. Lớp mô hình xem: chuyển đổi các mô hình dữ liệu thành các mô hình dễ hiểu cho giao diện người dùng.
  4. Lớp giao diện người dùng: trình bày/nhận dữ liệu đến/từ người dùng.
Bốn lớp trong ứng dụng di động: Lớp mạng, Lớp mô hình dữ liệu, Lớp mô hình xem, Lớp giao diện người dùng


Một lớp mạng và lớp mô hình dữ liệu là cần thiết để ứng dụng giao tiếp với máy chủ và dịch thông tin đó thành các lớp xem. GraphQL có thể phù hợp với hai lớp này và dựa trên lớp đồ thị dữ liệu và giải quyết hầu hết các điểm khó khăn mà các nhà phát triển di động từng gặp phải khi sử dụng API REST.

Một trong những điểm khó khăn khi sử dụng API REST là dữ liệu đến từ máy chủ phải được ánh xạ nhiều lần thành các loại đối tượng khác nhau để được hiển thị trên màn hình hoặc ngược lại từ đầu vào trên màn hình để được gửi đến máy chủ. Các ứng dụng đơn giản hơn có thể có ít ánh xạ này hơn tùy thuộc vào việc ứng dụng có cơ sở dữ liệu cục bộ để lưu trữ dữ liệu hay ứng dụng chỉ trực tuyến. Nhưng các ứng dụng di động chắc chắn có ánh xạ để chuyển đổi dữ liệu JSON đến từ API thành đối tượng lớp (ví dụ: đối tượng Swift).

Khi làm việc với các điểm cuối REST, các ánh xạ này về cơ bản là khớp mã được gõ tĩnh với các phản hồi JSON không có cấu trúc. Nói cách khác, các nhà phát triển di động được yêu cầu mã hóa cứng loại của một trường và ép kiểu giá trị JSON thành loại được giả định. Đôi khi, các nhà phát triển xác thực và khẳng định loại. Các bản đúc hoặc xác thực này có thể không thành công vì chúng ta biết máy chủ luôn thay đổi và loại bỏ các trường và đối tượng. Nếu điều đó xảy ra, chúng ta không thể sửa ứng dụng di động đã phát hành trên thị trường mà không thay thế các mã cứng và giả định đó. Đây là một trong những phần dễ xảy ra lỗi của ứng dụng di động khi làm việc với các điểm cuối REST. Những thay đổi này sẽ xảy ra liên tục trong suốt vòng đời của ứng dụng. Công việc của nhà phát triển di động là duy trì các mã cứng đó và giữ tính tương đương giữa phản hồi của API và logic ánh xạ ứng dụng. Bất kỳ thay đổi nào trên API máy chủ đều phải được thông báo và điều đó buộc các nhà phát triển di động phải cập nhật mã của họ.

Vấn đề được mô tả ở trên có thể được giảm bớt phần nào bằng cách thêm các khuôn khổ để kiểm soát luồng và cung cấp thêm tài liệu API, chẳng hạn như Đặc tả OpenAPI (OAS). Tuy nhiên, điều này không thực sự giải quyết được vấn đề như một phần của chính điểm cuối và thêm một giải pháp thay thế hoặc các phụ thuộc vào các khuôn khổ khác nhau.

Mặt khác, GraphQL giải quyết các mối quan tâm đã đề cập ở trên. API GraphQL được gõ mạnh và là hợp đồng tự ghi chép giữa máy chủ và máy khách. Được gõ mạnh có nghĩa là mỗi loại dữ liệu được xác định trước là một phần của ngôn ngữ. Điều này giúp khách hàng dễ dàng đồng bộ với các kiểu dữ liệu máy chủ. Không còn kiểu tĩnh trong ứng dụng di động của bạn và không có ánh xạ JSON với các kiểu dữ liệu tĩnh trong cơ sở mã. Các đối tượng của ứng dụng di động sẽ luôn được đồng bộ với các đối tượng máy chủ và các nhà phát triển sẽ nhận được các bản cập nhật và lỗi thời tại thời điểm biên dịch.

Các điểm cuối GraphQL được xác định bởi các lược đồ . Nội quanlà hệ thống trong GraphQL cho phép các hệ thống công cụ tạo mã cho các ngôn ngữ và nền tảng khác nhau. Deprecation là một ví dụ hay về việc mô tả nội quan. Nó có thể được thêm vào để mỗi trường có một isDeprecatedboolean và một replicationReason. Công cụ GraphQL này trở nên rất hữu ích vì nó hiển thị các cảnh báo và phản hồi về thời gian biên dịch trong một dự án di động.

Ví dụ, JSON bên dưới là phản hồi từ điểm cuối API:

“products”: [
{
“title”: “title of product 1”,
“quantity”: 2,
“price”: “200$”,
},
{
“title”: “title of a sweet product”,
“quantity”: 3,
“price”: “300$”,
}
]

view rawsample JSON response hosted with ❤ by GitHub

Trường pricetrên sản phẩm được nhận dưới dạng kiểu String và máy khách có ánh xạ bên dưới để chuyển đổi dữ liệu JSON thành mô hình Swift:

Let price = product[“price”] as? String

Kiểu ép kiểu này là cách ứng dụng di động chuyển dữ liệu JSON thành dữ liệu dễ hiểu cho các lớp UI. Về cơ bản, máy khách di động phải định nghĩa tĩnh kiểu của từng trường và điều này độc lập với các đối tượng của máy chủ.

Mặt khác, GraphQL loại bỏ các kiểu ép kiểu tĩnh này. Máy khách và máy chủ sẽ luôn được kết hợp chặt chẽ và đồng bộ. Trong ví dụ trên, Productkiểu sẽ nằm trong lược đồ trong tài liệu GraphQL dưới dạng hợp đồng giữa máy khách và máy chủ, và giá sẽ luôn là kiểu được định nghĩa trong hợp đồng đó. Vì vậy, máy khách không còn giữ các kiểu tĩnh cho từng trường nữa.

Sự đánh đổi

Lưu ý rằng tùy chỉnh đi kèm với chi phí. Nhà phát triển khách hàng có trách nhiệm duy trì hiệu suất cao trong khi tận dụng lợi thế của tùy chỉnh. Lựa chọn giữa việc sử dụng REST API hay GraphQL tùy thuộc vào nhà phát triển dựa trên dự án nhưng nhìn chung, điểm cuối REST API được định nghĩa theo cách tối ưu hơn. Điều đó có nghĩa là mỗi điểm cuối chỉ nhận được một đầu vào được xác định và trả về một đầu ra được xác định và không nhiều hơn thế. Điểm cuối GraphQL có thể được tùy chỉnh và khách hàng có thể yêu cầu bất cứ điều gì trong một yêu cầu duy nhất. Nhưng khách hàng cũng cần phải cẩn thận về chi phí của tính linh hoạt này. Chúng ta sẽ nói về chi phí truy vấn GraphQL sau nhưng có chi phí không có nghĩa là chúng ta không thể đạt được mức tối ưu hóa tương tự như REST API với GraphQL. Chi phí truy vấn nên được xem xét khi tận dụng tính năng tùy chỉnh.

Mẹo và Thực hành Tốt nhất

Để sử dụng truy vấn GraphQL trong một dự án di động, bạn cần có một công cụ tạo mã để tạo các tệp phía máy khách đại diện cho các truy vấn GraphQL, đột biến và phản hồi của bạn. Công cụ chúng tôi sử dụng tại Shopify có tên là Syrup . Syrup là mã nguồn mở và tạo ra các mã Swift và Kotlin được gõ mạnh dựa trên các truy vấn GraphQL được sử dụng trong ứng dụng của bạn. Hãy cùng xem một số ví dụ về truy vấn GraphQL trong ứng dụng di động và tìm hiểu một số mẹo. Các ví dụ và ảnh chụp màn hình được lấy từ ứng dụng Shopify POS.

Các mảnh vỡ và màn hình trong ứng dụng di động

Việc xác định các đoạn thường phụ thuộc vào giao diện người dùng của ứng dụng. Trong ví dụ này, màn hình chi tiết đơn hàng trong ứng dụng Shopify POS hiển thị lineItemstrên một đơn hàng nhưng cũng có một màn hình phụ hiển thị sự kiện trên đơn hàng có liên quan lineItems. Ví dụ, chi tiết đơn hàng trên hình ảnh trên cùng và màn hình sự kiện trả lại với lineItemsđược trả lại ở phía dưới.

Các mảnh vỡ và màn hình trong ứng dụng di động
Fragments: trả về màn hình sự kiện với các lineItems được trả về ở phía dưới


Trong ví dụ này, lineItemcác hàng trong cả hai màn hình đều giống hệt nhau và chế độ xem để tạo hàng đó nhận được chính xác cùng một thông tin để tạo chế độ xem. Giả sử mỗi màn hình gọi một truy vấn để lấy thông tin chúng cần. Cả hai đều cần cùng một trường trên lineItemđối tượng. Vì vậy, OrderLineItemđối tượng về cơ bản là một đối tượng được chia sẻ giữa nhiều màn hình và cũng giữa nhiều truy vấn trong ứng dụng. Với truy vấn GraphQL, chúng tôi định nghĩa orderLineItemlà một đoạn mã để có thể tái sử dụng và đảm bảo rằng chế lineItemđộ xem nhận được tất cả các trường cần thiết mỗi khi ứng dụng truy xuất lineItembằng đoạn mã này. Xem các ví dụ truy vấn bên dưới:

query OrderDetails($id: ID!) { order(id: $id) { lineItems() { …OrderLineItem } }} query OrderDetails($id: ID!) { order(id: $id) { refund { refundLineItems { lineItems(…) { …OrderLineItem } } } }}view rawQuery example 1 hosted with ❤ by GitHubCác mảnh có trường chung nhưng tên khác nhau

Các đoạn có thể được tùy chỉnh ở phía máy khách và thường thì trong các ứng dụng di động phụ thuộc rất nhiều vào UI. Việc xác định nhiều đoạn hơn không ảnh hưởng đến chi phí truy vấn nên nó miễn phí và cung cấp cho truy vấn của bạn một cấu trúc tốt. Một mẹo hay về việc sử dụng các đoạn là bạn không chỉ có thể chia nhỏ các trường thành nhiều đoạn mà còn có thể đặt cùng một trường vào nhiều đoạn và một lần nữa, nó không làm tăng chi phí cho truy vấn. Ví dụ: đôi khi các ứng dụng trình bày dữ liệu lặp lại trên nhiều màn hình. Trong ví dụ về màn hình OrderDetails của chúng tôi, ứng dụng POS trình bày thông tin thanh toán cấp cao về đơn hàng trong màn hình orderDetails (chẳng hạn như: tổng phụ, chiết khấu, tổng, v.v.), nhưng đơn hàng có thể có lịch sử thanh toán dài hơn (bao gồm thay đổi, giao dịch không thành công, v.v.). Lịch sử đơn hàng được trình bày trong các màn hình phụ nếu người dùng chọn xem thông tin đó. Giả sử chúng ta chỉ gọi một truy vấn để lấy tất cả thông tin, chúng ta có thể có hai đoạn: OrderPayments, OrderHistory.

Xem các đoạn trích dưới đây:

fragment OrderHistory on Order {
subtotalPriceSet
totalPriceSet
}
fragment OrderPayments on Order {
subtotalPriceSet
totalPriceSet
transactions(..) {
}

view rawfragment example 1 hosted with ❤ by GitHubViệc xác định các đoạn này giúp việc truyền dữ liệu dễ dàng hơn và không ảnh hưởng đến hiệu suất hoặc chi phí truy vấn. Chúng ta sẽ nói thêm về chi phí truy vấn sau.

Tùy chỉnh phản hồi truy vấn để mang lại lợi ích cho UX của ứng dụng của bạn

Với GraphQL, bạn có thể tùy chỉnh phản hồi truy vấn/đột biến của mình để có lợi cho UI ứng dụng. Nếu bạn đã sử dụng REST API cho ứng dụng di động trước đây, bạn sẽ đánh giá cao sức mạnh mà GraphQL có thể mang lại cho ứng dụng của mình. Ví dụ, sau khi gọi một đột biến trên một đối tượng Order, bạn có thể định nghĩa phản hồi của lệnh gọi đột biến với các trường bạn cần để xây dựng màn hình tiếp theo. Nếu đột biến là thêm a lineItemvào một đối tượng order và màn hình tiếp theo của bạn là hiển thị tổng giá của đơn hàng, bạn có thể định nghĩa đối tượng phản hồi để bao gồm totalPricetrường trên order để bạn có thể dễ dàng xây dựng UI của mình mà không cần phải lấy đối tượng order đã cập nhật. Xem ví dụ về đột biến bên dưới:

mutation OrderUpdate($input: OrderInput!) {
orderUpdate(input: $input) {
order {
id
totalPrice
}
}
}

view rawmutation example 1 hosted with ❤ by GitHub
Tính linh hoạt này không thể thực hiện được với REST API nếu không yêu cầu nhóm máy chủ thay đổi đối tượng phản hồi cho điểm cuối REST API cụ thể.

Sử dụng Biệt danh để Có Mô hình Dữ liệu Có thể Đọc được Dựa trên Giao diện Người dùng của Ứng dụng của bạn

Nếu bạn đang xây dựng UI dựa trên việc sử dụng trực tiếp các đối tượng GraphQL, bạn có thể sử dụng các bí danh để đổi tên các trường thành bất kỳ tên nào bạn muốn. Một mẹo nhỏ về việc sử dụng các bí danh là bạn có thể sử dụng các bí danh để đổi tên một trường nhưng nếu bạn thêm phần mở rộng vào đối tượng, bạn có thể có tên trường gốc là một biến mới với logic được thêm vào. Xem ví dụ bên dưới:

query Order($id: ID!) { order(id: $id) { id serverFinancialStatus: displayFinancialStatus serverFulfillmentStatus: displayFulfillmentStatus }}view raworder query alias hosted with ❤ by GitHubextension Order { var displayFinancialStatus: String? { switch serverFinancialStatus { case .partiallyPaid: return “outstandingBalance” case .partiallyRefunded: return “partiallyRefunded” case .refunded: return “refunded” default: return nil } }}view rawswift extension hosted with ❤ by GitHub
Sử dụng Chỉ thị và Thêm Logic vào Truy vấn của Bạn

Các chỉ thị được đề cập trong tài liệu GraphQL như một cách để tránh thao tác chuỗi cho mã phía máy chủ, nhưng nó cũng có lợi thế cho máy khách di động. Ví dụ, đối với màn hình Chi tiết đơn hàng, POS cần các trường khác nhau trên đơn hàng dựa trên loại đơn hàng. Nếu đơn hàng là đơn hàng nhận hàng, màn hình Chi tiết đơn hàng cần nhiều thông tin hơn về việc hoàn thành và không cần thông tin về chi tiết giao hàng. Với các chỉ thị, bạn có thể truyền các biến boolean từ UI của mình vào truy vấn để bao gồm hoặc bỏ qua các trường. Xem ví dụ truy vấn bên dưới:

order(id: $id) {
…FulfillmentOrders @include(if: $pickupOrder)
shippingAddress @skip(if: $pickupOrder)
}

view rawquery example 2 hosted with ❤ by GitHub

Chúng ta có thể thêm chỉ thị vào các đoạn hoặc trường. Điều này cho phép các ứng dụng di động chỉ lấy dữ liệu mà UI cần và không nhiều hơn thế. Tính linh hoạt này không thể thực hiện được với các điểm cuối REST API nếu không có hai điểm cuối khác nhau và có mã trong cơ sở dữ liệu mã ứng dụng di động để chuyển đổi giữa các điểm cuối dựa trên biến boolean.

Hiệu suất GraphQL

GraphQL cung cấp toàn bộ sức mạnh và sự đơn giản cho ứng dụng di động của bạn và một số công việc hiện được chuyển sang phía máy chủ để mang lại sự linh hoạt cho khách hàng. Về phía máy khách, chúng ta phải cân nhắc đến chi phí của một truy vấn mà chúng ta xây dựng. Chi phí của truy vấn ảnh hưởng trực tiếp đến hiệu suất vì nó ảnh hưởng đến khả năng phản hồi của ứng dụng và tài nguyên trên máy chủ. Đây không phải là điều thường được đề cập khi nói về GraphQL, nhưng tại Shopify, chúng tôi quan tâm đến hiệu suất ở cả phía máy khách và phía máy chủ.

Các máy chủ GraphQL khác nhau có thể có các phương pháp giới hạn tốc độ API khác nhau. Tại Shopify, các lệnh gọi đến API GraphQL bị giới hạn dựa trên chi phí truy vấn được tính toán , nghĩa là chi phí truy vấn mỗi phút và quan trọng hơn số lượng lệnh gọi truy vấn mỗi phút. Mỗi trường trong lược đồ có một giá trị chi phí nguyên và tổng của tất cả các chi phí này sẽ là chi phí của truy vấn mà chúng ta xây dựng ở phía máy khách.

Nói một cách đơn giản, mỗi người dùng có một nhóm chi phí truy vấn tối đa mỗi phút và mỗi giây nhóm sẽ được nạp lại sau mỗi lần thực hiện truy vấn. Rõ ràng, các truy vấn phức tạp sẽ chiếm một lượng lớn hơn theo tỷ lệ của nhóm đó. Để có thể bắt đầu thực hiện một ứng dụng bucket truy vấn phải có đủ chỗ cho độ phức tạp của truy vấn yêu cầu. Đó là lý do tại sao ở phía máy khách, chúng ta nên quan tâm đến chi phí truy vấn được tính toán của mình. Có những mẹo và cách để cải thiện chi phí truy vấn nói chung, như được mô tả tại đây .

Tương lai của GraphQL

GraphQL không chỉ là một ngôn ngữ truy vấn đồ thị. Nó độc lập với ngôn ngữ và linh hoạt để phục vụ nhu cầu của bất kỳ nền tảng nào. Nó được xây dựng để phục vụ khách hàng khi băng thông mạng, độ trễ và UX là rất quan trọng. Chúng tôi đã đề cập đến những điểm khó khăn khi sử dụng REST trong các ứng dụng di động và cách GraphQL có thể giải quyết nhiều mối quan tâm đó. GraphQL cho phép bạn xây dựng bất cứ thứ gì bạn cần cho khách hàng và thực hiện theo cách của riêng bạn. GraphQL đã là một bước tiến lớn so với thiết kế API REST, giải quyết trực tiếp các mô hình dữ liệu cần được chuyển giữa mỗi máy khách và máy chủ để thực hiện công việc. Tại Shopify, chúng tôi tin tưởng vào tương lai của GraphQL và đó là lý do tại sao Shopify đã cung cấp API trong GraphQL kể từ năm 2018.

Reference: https://shopify.engineering/using-graphql-for-high-performing-mobile-applications