Khái niệm, ý tưởng, chiến lược
Khái niệm, ý tưởng, chiến lượcThiết kế ứng dụng để hoạt động với nhiều GraphQL server khác nhau

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.

GraphQL query đóng vai trò như một giao diện giữa client và server

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ăngServer #1Server #2
Trường danh mục bài viếtcategoriespostCategories
Đối số trường để giới hạn số kết quảfirstpagination.limit
Trường id của một đối tượng đại diện choID 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 querysâu hơn do edges.nodephẳ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:

  1. Giữ các GraphQL queries tách biệt khỏi ứng dụng
  2. Điều chỉnh tên trường thông qua alias
  3. Đ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, idglobalID có thể được lấy sử dụng các tên mà ứng dụng mong đợi: lần lượt là categories, categoryIdid:

{
  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 edgesnode 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 độ nodesedge:

{
  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 selfedges đượ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: postCategoriesedges: 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.