Blog

🤔 Tại sao Gato GraphQL mới mất 1,5 năm mới được phát hành?

Leonardo Losoviz
Bởi Leonardo Losoviz ·

Phiên bản 0.9 của Gato GraphQL đã vừa được phát hành. Phải mất gần 1,5 năm phát triển và hơn 16000 commit mới hoàn thiện. Quả thực là một khoảng thời gian rất dài!

Khi chia sẻ thông báo trên Hacker News, tôi nhận được câu hỏi sau:

[...] Tôi tò mò muốn biết điều gì đã cần đến 16k commit. Những dự án mà tôi từng tham gia với hơn mười nghìn commit thường có hàng chục hoặc hàng trăm người làm việc toàn thời gian. [...] Có sự phức tạp nào cần vượt qua mà bài viết không đề cập đến không?

Số lượng commit không phải là một chỉ số đáng tin cậy lắm, vì đôi khi tôi chỉ thực hiện một thay đổi rất đơn giản và đẩy nó như một commit đơn lẻ. Nhiều trong số 16k commit đó là các commit "typo", hoặc chỉ đơn giản là cải thiện mô tả trong một README nào đó.

Tuy nhiên, số lượng commit đó vẫn cho thấy công sức thực sự đã bỏ ra. Cũng có rất nhiều commit chứa đầy các thay đổi, bao gồm hàng chục, thậm chí hàng trăm thay đổi cùng một lúc. Những thay đổi giữa các phiên bản 0.80.9 thực sự rất lớn, và điều đó đòi hỏi nhiều công sức và thời gian.

Trong bài viết này, tôi sẽ mô tả những thay đổi đó là gì, để giải thích tại sao nó mất nhiều thời gian như vậy. Và trong quá trình đó, tôi cũng sẽ giới thiệu trước một số tính năng nâng cao đã được thêm vào codebase, và sẽ ra mắt cùng với phiên bản 1.0 sắp tới.

Bối cảnh của máy chủ GraphQL

Đầu tiên, tôi sẽ chia sẻ một chút về lịch sử của engine và các chi tiết kỹ thuật về cách nó hoạt động.

(Phần này chủ yếu liên quan đến các nhà phát triển; nếu bạn không quan tâm đến các chi tiết kỹ thuật, hãy thoải mái bỏ qua sang phần tiếp theo.)

Gato GraphQL được xây dựng trên nền tảng PoP, một engine render các component bằng PHP (tương tự như React hoặc Vue trên JavaScript). Sự phụ thuộc của nó vào engine này là tuyệt đối, đó là lý do tại sao plugin được lưu trữ trong monorepo GatoGraphQL/GatoGraphQL trên GitHub.

Ở cấp độ nền tảng, sự phụ thuộc này trông như sau:

Gato GraphQL giải quyết một GraphQL query bằng cách đầu tiên chuyển đổi nó thành một mô hình component tương đương, sau đó PoP giải quyết bằng cách lấy tất cả dữ liệu cần thiết, và rồi dữ liệu này được định hình theo GraphQL query.

Khi tôi bắt đầu làm việc với PoP vào khoảng năm 2013/2014, GraphQL chưa tồn tại, và phương pháp luận để giải quyết một mô hình component thành dữ liệu được thiết kế và triển khai từ đầu. Việc không có một mô hình để theo dõi (chẳng hạn như GraphQL cho các khái niệm, và dự án tham chiếu graphql-js cho một triển khai) vừa là trở ngại vừa là may mắn, như tôi sẽ giải thích sau.

PoP ban đầu được thiết kế để render toàn bộ trang web dưới dạng HTML phía máy chủ, đồng thời hiển thị dữ liệu thô ở định dạng JSON khi thêm ?output=json vào URL của trang, và cho phép chọn thêm dữ liệu cần lấy (cài đặt, dữ liệu đối tượng DB) với các tham số URL bổ sung.

Hãy nhấp vào các liên kết sau (tất cả đều trỏ đến cùng một trang web, chỉ với các tham số URL khác nhau) và chú ý sự khác biệt giữa chúng:

Khi nhấp vào liên kết cuối cùng, một nhận thức bừng sáng: Đây gần như là GraphQL! Sự khác biệt lớn duy nhất là dữ liệu trong phản hồi là ngầm định, vì nó đã được xác định bởi các component (trong PHP) được đưa vào trang. GraphQL, ngược lại, cho phép chúng ta quyết định lấy dữ liệu gì thông qua một query.

Vì vậy, khi tôi tìm hiểu về GraphQL vào khoảng năm 2019, đối với tôi điều hiển nhiên là cũng phải làm cho PoP thỏa mãn một máy chủ GraphQL. Tất cả những gì nó cần làm là chấp nhận GraphQL query làm đầu vào, và tạo một mô hình component ngay lập tức dựa trên query đó.

Và đó là điều tôi đã làm. Và nó hoạt động tốt. Nhưng nó chậm, vì PoP hiểu định dạng đầu vào của chính nó, vì vậy GraphQL query phải được điều chỉnh sang định dạng PoP:

  1. Phân tích GraphQL query; sau đó
  2. Chuyển đổi query sang định dạng PoP; sau đó
  3. Phân tích định dạng PoP

GraphQL query khi đó được phân tích hai lần (một lần cho GraphQL, một lần cho PoP), và định dạng PoP không được giải quyết thông qua một AST, mà chỉ bằng cách phân tích chuỗi query lặp đi lặp lại. (Không sử dụng AST là cách lập trình tệ hại, nhưng tôi không có đặc tả để tuân theo, và quá trình phát triển của nó diễn ra tự nhiên, nơi một substr(...) đơn giản có thể giải quyết vấn đề hàng ngày.)

Đó là lý do tôi nói rằng không có đặc tả GraphQL là một trở ngại, vì giải pháp của tôi chậm (và đó là tình trạng ở phiên bản 0.8). Vì vậy tôi quyết định khắc phục nó.

Chuyển đổi engine thành GraphQL-first

Giải pháp tôi quyết định là làm cho PoP nói ngôn ngữ GraphQL một cách tự nhiên. Khi đó, việc truyền một GraphQL query vào PoP làm đầu vào sẽ được chuyển đổi trực tiếp thành mô hình component, mà không cần bất kỳ adapter bổ sung nào, hoặc phải làm mọi thứ hai lần.

Điều này có nghĩa là dự án PoP phải được tái định hướng, từ một thư viện PHP render các component cho các trang web phía máy chủ được điều chỉnh để giải quyết các GraphQL queries, thực sự trở thành một máy chủ GraphQL.

Codebase sau đó đã trải qua một quá trình biến đổi lớn, giới thiệu GraphQL AST như nền tảng để truyền thông tin trạng thái giữa tất cả các dịch vụ PHP trong engine. Các đối tượng GraphQL AST hiện là đầu vào cho PoP (thay vì các chuỗi query).

Các máy chủ GraphQL khác trong PHP dựa vào graphql-php, nhưng plugin Gato GraphQL thì không. Đây là tin xấu về mặt nỗ lực bảo trì (vì tôi không thể tái sử dụng những gì người khác đã lập trình), nhưng là tin tốt về sự độc lập: Tôi có thể quyết định thêm các tính năng tùy chỉnh vào plugin của mình theo tốc độ và tiêu chí của riêng tôi (đó là lý do tại sao plugin đã cung cấp sẵn input object "oneof").

Và như sẽ được trình bày trong phần bên dưới, đây là một lợi thế rất lớn.

Tích hợp các tính năng nguyên bản vào GraphQL

GraphQL thường được liên kết với việc lấy dữ liệu. Đương nhiên, bạn có thể lấy bất kỳ phần dữ liệu nào (bài đăng, người dùng, bình luận, v.v.) từ Gato GraphQL:

query {
  posts(
    pagination: { limit: 5, offset: 20 }
    sort: { by: DATE, order: ASC }
  ) {
    id
    title
    content
    url
    author {
      id
      name
      url
    }
    comments {
      id
      date
      content
    }
  }
}

Nhưng đây chỉ là mức cơ bản. GraphQL cũng có thể được sử dụng cho nhiều trường hợp sử dụng khác, bao gồm thao tác và biến đổi dữ liệu, thậm chí đặt GraphQL trong một pipeline để làm trung gian giữa các dịch vụ.

Một số ví dụ mà GraphQL hữu ích là:

  • Trích xuất thông tin từ một hoặc nhiều nguồn (chẳng hạn như người dùng từ các trang WordPress và dữ liệu liên hệ bản tin từ Mailchimp), kết hợp dữ liệu và phân tích tất cả cùng nhau như một tập dữ liệu duy nhất
  • Thực hiện các thao tác để điều chỉnh nội dung trên trang:
    • Một lần, như khi di chuyển một trang web sang tên miền khác và thay thế "www.myoldsite.com" bằng "mynewsite.com" ở khắp nơi trong nội dung và siêu dữ liệu
    • Liên tục, như để thay thế bất kỳ "http://" nào bằng "https://" mỗi khi một người viết xuất bản một bài đăng blog mới
  • Kết nối với API Google Translate để dịch tất cả các bài đăng blog sang ngôn ngữ khác
  • Tự động gửi tweet sau khi một bài đăng blog được xuất bản

PoP đã được thiết kế để hỗ trợ các trường hợp sử dụng khác này, thông qua các tính năng không được (tự nhiên) hỗ trợ bởi GraphQL, chẳng hạn như:

  • Hỗ trợ các trường "chức năng" (ngoài các trường "dữ liệu"), được thêm vào tất cả các kiểu trong schema
  • Truyền kết quả của một trường làm đầu vào cho trường khác, trong cùng một query
  • Kết hợp các directive, để một directive có thể thay đổi hành vi của một directive khác
  • Quyết định có áp dụng một directive hay không một cách động, dựa trên giá trị của trường

Và tôi chắc chắn không muốn loại bỏ các tính năng này khỏi máy chủ GraphQL: Tôi đã lập trình chúng, và chúng chắc chắn có giá trị.

Vì vậy, lý do thứ hai tại sao v0.9 mất nhiều thời gian như vậy là tôi cũng phải tìm cách tích hợp các khả năng mới này vào GraphQL, theo cách không vi phạm đặc tả GraphQL (ví dụ, việc giới thiệu các phần tử mới vào cú pháp GraphQL là điều không thể chấp nhận).

Ví dụ về thao tác dữ liệu trong GraphQL

Các khả năng mới được giới thiệu vào GraphQL trong plugin sẽ trở nên rõ ràng hơn trong tương lai gần, khi phiên bản 1.0 được phát hành. Nhưng bạn đã có thể có cái nhìn trước về một số trong số chúng.

GraphQL query sau đây lấy danh sách các mục người dùng từ một REST API bên ngoài (có thể được @removed khỏi phản hồi); nhập dữ liệu này vào một trường khác, ngay trong cùng một query; trích xuất thuộc tính email từ mỗi mục; và cuối cùng chuyển đổi email thành chữ hoa, nhưng chỉ khi ngôn ngữ trong cùng mục đó là tiếng Anh hoặc tiếng Đức:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes
{
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  ) # @remove   # <= Uncomment this directive to not print the API data
 
  emails: _echo(value: $__userEntries)
 
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
 
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "lang"
          }
        }
        passOnwardsAs: "userLang"
      )
 
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: {
          value: $userLang,
          array: ["en", "de"]
        }
        passOnwardsAs: "isSpecialLang"
      )
 
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "email"
          }
        }
        setResultInResponse: true
      )
 
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase` 
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Đây là phản hồi (hãy chú ý cách chỉ một số email được chuyển thành chữ hoa):

{
  "data": {
    "userEntries": [
      {
        "email": "abracadabra@ganga.com",
        "lang": "de"
      },
      {
        "email": "longon@caramanon.com",
        "lang": "es"
      },
      {
        "email": "rancotanto@parabara.com",
        "lang": "en"
      },
      {
        "email": "quezarapadon@quebrulacha.net",
        "lang": "fr"
      },
      {
        "email": "test@test.com",
        "lang": "de"
      },
      {
        "email": "emilanga@pedrola.com",
        "lang": "fr"
      }
    ],
    "emails": [
      "ABRACADABRA@GANGA.COM",
      "longon@caramanon.com",
      "RANCOTANTO@PARABARA.COM",
      "quezarapadon@quebrulacha.net",
      "TEST@TEST.COM",
      "emilanga@pedrola.com"
    ]
  }
}

Hãy tự kiểm tra! Nhấn nút "Run" để thực thi query:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes {
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  )
  # @remove   # <= Uncomment this directive to not print the API data
  emails: _echo(value: $__userEntries)
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "lang" } }
        passOnwardsAs: "userLang"
      )
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: { value: $userLang, array: ["en", "de"] }
        passOnwardsAs: "isSpecialLang"
      )
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "email" } }
        setResultInResponse: true
      )
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase`
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Tôi đã đề cập rằng không được hướng dẫn bởi GraphQL là một trở ngại, nhưng (nhìn lại) cũng là một may mắn. Điều này là vì tôi không bị ràng buộc bởi đặc tả GraphQL, nên tôi có thể cho phép mình mơ về những khả năng mới này.

Và bây giờ khi những tính năng này đã được chuyển sang Gato GraphQL, nó có thể là một đồng minh cực kỳ hữu ích cho bất cứ điều gì liên quan đến việc truy xuất, thao tác và biến đổi nội dung cho trang WordPress của bạn. (Mặc dù chúng chỉ có thể truy cập được với phiên bản v1.0 sắp tới).

Mất một khoảng thời gian, nhưng công sức bỏ ra chắc chắn xứng đáng.

Hãy thử ngay!

Bạn có bị thuyết phục rằng thời gian chờ đợi dài đó xứng đáng không? Tôi hy vọng vậy!

Hãy tiến hành, tải xuống plugin và xem thử:

Bạn có muốn nhận tin tức về quá trình phát triển, tài liệu mới và các phiên bản sắp tới, bao gồm v1.0? Hãy thoải mái đăng ký nhận bản tin.

Muốn khám phá mã nguồn mở trên GitHub? Hãy xem GatoGraphQL/GatoGraphQL (và hãy thoải mái để lại một ngôi sao... Chúng tôi yêu các ngôi sao! ⭐️⭐️⭐️)

Nhân tiện, bạn cần thực hiện những biến đổi nội dung nào trong WordPress (mà bạn có thể đang dùng một số plugin thương mại chuyên dụng cho việc đó)? Hãy gửi cho tôi một tin nhắn kể về trường hợp sử dụng của bạn.

Nếu bạn thích những gì bạn thấy, hãy chia sẻ với bạn bè và đồng nghiệp, hãy giúp lan tỏa tình yêu ❤️.


Đăng ký nhận bản tin của chúng tôi

Cập nhật tất cả những điều mới từ Gato GraphQL.