Thiết kế ứng dụng để hoạt động với nhiều GraphQL server khác nhau
"Lập trình theo giao diện, không theo triển khai cụ thể" là cách thực hành gọi một chức năng không trực tiếp mà thông qua một hợp đồng liệt kê những đầu vào cần thiết và đầu ra mong đợi, đồng thời che giấu cách thức triển khai. Chiến lược này giúp tách biệt ứng dụng khỏi một triển khai, nhà cung cấp hay ngăn xếp cụ thể, cho phép hoán đổi giữa chúng mà không cần thay đổi mã nguồn ứng dụng.
Chúng ta cũng có thể áp dụng chiến lược này với GraphQL. GraphQL có thể đóng vai trò trung gian giữa ứng dụng và server, cho phép chúng ta thực hiện tất cả các sửa đổi cần thiết chỉ trên các GraphQL queries, trong khi vẫn giữ nguyên logic nghiệp vụ.
Một GraphQL query đóng vai trò như một giao diện giữa client và server. Khi thực thi một query, GraphQL server sẽ xử lý nó và trả về dữ liệu cần thiết cho client. Dữ liệu đến từ đâu? Nó được lấy như thế nào? Client không biết và cũng không quan tâm.

Phản hồi của query sẽ có cùng hình dạng với query. Với GraphQL query này:
{
post(by: { id: 1 }) {
id
title
}
}...phản hồi sẽ là:
{
"data": {
"post": {
"id": 1,
"title": "Hello world!"
}
}
}Với cùng một query nhưng có các tham số khác nhau, dữ liệu trả về sẽ khác nhau, nhưng hình dạng sẽ không thay đổi. Điều này có nghĩa là miễn là query không thay đổi, ứng dụng không cần phải thay đổi logic của mình về cách đọc và xử lý dữ liệu, và tương tự, không quan trọng GraphQL server nào đang thực thi query.
Và như vậy chúng ta có thể hoán đổi một GraphQL server này với một server khác một cách liền mạch.
Các queries phụ thuộc vào schema GraphQL
Tuy nhiên, đoạn trên có phần lạc quan quá mức, bởi vì GraphQL query có thể cần thay đổi tùy thuộc vào GraphQL server. Nói chính xác hơn, query được xây dựng dựa trên schema GraphQL, và nếu các server khác nhau công khai các schema khác nhau thì query cũng sẽ khác nhau.
Ví dụ, một GraphQL server sử dụng Cursor Connections Specification có thể thực thi query sau:
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}Và một server khác sử dụng phân trang kiểu WordPress (như Gato GraphQL) sẽ thực thi cùng query theo cách này:
{
postCategories(pagination: { limit: 10000 }) {
id
description
globalID
name
slug
}
}Chúng ta có thể thấy rõ sự khác biệt giữa hai queries:
| Tính năng | Server #1 | Server #2 |
|---|---|---|
| Trường danh mục bài viết | categories | postCategories |
| Đối số trường để giới hạn số kết quả | first | pagination.limit |
Trường id của một đối tượng đại diện cho | ID toàn cục duy nhất của nó | ID duy nhất cho kiểu của nó |
| Hình dạng của query | sâu hơn do edges.node | phẳng hơn |
Chỉ đơn giản thay thế query từ server đầu tiên bằng query tương đương từ server thứ hai trong ứng dụng sẽ không hoạt động. Đó là vì logic vẫn sẽ truy cập dữ liệu từ phản hồi theo hình dạng và các trường của query ban đầu.
Một giải pháp khả thi là cũng thay thế logic lấy dữ liệu ở phía client. Ví dụ, logic sau đây:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);...có thể được thay thế như thế này:
const categories = data?.data.postCategories;Nhưng đó chính xác là điều chúng ta muốn tránh. Chúng ta muốn giữ các thay đổi ở mức tối thiểu, chỉ sửa đổi giao diện (GraphQL query), và giữ nguyên logic nghiệp vụ.
May mắn thay, có thể thu hẹp những khác biệt bằng cách chỉ sửa đổi các GraphQL queries, theo các bước sau:
- Giữ các GraphQL queries tách biệt khỏi ứng dụng
- Điều chỉnh tên trường thông qua alias
- Điều chỉnh hình dạng phản hồi thông qua trường
self
Hãy xem cách, qua 3 bước này, chúng ta có thể điều chỉnh một ứng dụng để trỏ đến một GraphQL server khác.
Giữ các GraphQL queries tách biệt khỏi ứng dụng
Tách biệt các GraphQL queries khỏi logic ứng dụng bao gồm:
- Lưu trữ mỗi GraphQL query (hoặc một nhóm chúng) trong một file riêng biệt, và tất cả chúng trong một thư mục cụ thể
- Xuất các queries và nhập chúng vào ứng dụng
Ví dụ, chúng ta có thể đặt mỗi GraphQL query trong một file riêng biệt dưới src/data, và xuất nó:
// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
{
categories(first: 10000) {
edges {
node {
databaseId
description
id
name
slug
}
}
}
}
`;Ứng dụng sau đó có thể nhập và sử dụng GraphQL query:
import { QUERY_ALL_CATEGORIES } from 'data/categories';
export async function getAllCategories() {
const apolloClient = getApolloClient();
const data = await apolloClient.query({
query: QUERY_ALL_CATEGORIES,
});
const categories = data?.data.categories.edges.map(({ node = {} }) => node);
return {
categories,
};
}Nhờ cách thiết lập này, tất cả các sửa đổi chỉ cần thực hiện trên các file trong src/data.
Điều chỉnh tên trường thông qua alias
Một field alias có thể được dùng để đổi tên một trường trong phản hồi từ GraphQL server thứ hai thành tên của trường đó trong server đầu tiên.
Theo cách này, các trường postCategories, id và globalID có thể được lấy sử dụng các tên mà ứng dụng mong đợi: lần lượt là categories, categoryId và id:
{
categories: postCategories(pagination: { limit: 10000 }) {
categoryId: id
description
id: globalID
name
slug
}
}Lưu ý rằng trường categories có đối số first, trong khi trường tương ứng postCategories sử dụng đối số pagination.limit. Tuy nhiên, vì các đối số trường không được phản ánh trong tên của trường trong phản hồi, chúng ta không cần lo lắng về chúng.
Điều chỉnh hình dạng phản hồi thông qua trường self
Thách thức cuối cùng phức tạp hơn một chút: chúng ta cần sửa đổi hình dạng của phản hồi, thêm các cấp độ bổ sung cho edges và node xuất phát từ spec Cursor Connections.
Để đạt được điều này, chúng ta sẽ giới thiệu một trường self cho tất cả các kiểu trong schema GraphQL, trường này phản chiếu lại chính đối tượng mà nó được áp dụng:
type QueryRoot {
self: QueryRoot!
}
type Post {
self: Post!
}
type User {
self: User!
}Trường self cho phép thêm các cấp độ bổ sung vào query mà không rời khỏi đối tượng đang được truy vấn. Khi chạy query này:
{
__typename
self {
__typename
}
post(by: { id: 1 }) {
self {
id
__typename
}
}
user(by: { id: 1 }) {
self {
id
__typename
}
}
}...sẽ tạo ra phản hồi này:
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"post": {
"self": {
"id": 1,
"__typename": "Post"
}
},
"user": {
"self": {
"id": 1,
"__typename": "User"
}
}
}
}Bây giờ, chúng ta có thể dùng self để giả tạo thêm các cấp độ nodes và edge:
{
categories: self {
edges: postCategories(pagination: { limit: 10000 }) {
node: self {
categoryId: id
description
id: globalID
name
slug
}
}
}
}Kiểu của đối tượng trong schema GraphQL cho edges và cho self rõ ràng là khác nhau. Nhưng điều đó không quan trọng đối với ứng dụng, vì nó không tương tác trực tiếp với đối tượng thực tế được mô hình hóa trong GraphQL server. Thay vào đó, nó nhận dữ liệu dưới dạng đối tượng JSON, và phần dữ liệu đó cho một trường đến từ đối tượng PostConnection hay đối tượng Post sẽ là như nhau.
Lưu ý rằng trường categories được giải quyết thông qua self và edges được giải quyết thông qua postCategories, chứ không phải ngược lại. Điều này nhằm giữ số lượng phần tử trả về khớp với số lượng được định nghĩa bởi các trường sử dụng spec Cursor Connections:
type RootQuery {
categories: RootQueryToCategoryConnection
}
type RootQueryToCategoryConnection {
edges: [RootQueryToCategoryConnectionEdge]
}
type RootQueryToCategoryConnectionEdge {
node: Category
}Nếu GraphQL query đã điều chỉnh bị đảo ngược (tức là truy vấn categories: postCategories và edges: self), việc truy cập dữ liệu sẽ thất bại, vì data.categories sẽ là một mảng, do đó data.categories.edges sẽ ném ra lỗi khi thực thi:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);Điều chỉnh tất cả các queries
Sau khi áp dụng cùng chiến lược cho tất cả các GraphQL queries trong src/data, ứng dụng có thể dễ dàng chuyển đổi từ GraphQL server này sang GraphQL server khác.