บล็อก

🤔 ทำไม Gato GraphQL ใหม่ถึงใช้เวลา 1.5 ปีกว่าจะปล่อยออกมา?

Leonardo Losoviz
โดย Leonardo Losoviz ·

เวอร์ชัน 0.9 ของ Gato GraphQL เพิ่งได้รับการปล่อยออกมาแล้ว โดยใช้เวลาพัฒนาเกือบ 1.5 ปีและมีกว่า 16,000 คอมมิต นั่นคือเวลาที่นานมากจริงๆ!

หลังจากแชร์ประกาศบน Hacker News ผมได้รับคำถามต่อไปนี้:

[...] ผมอยากรู้ว่า 16k คอมมิตมีอะไรบ้าง โปรเจกต์ที่ผมเคยทำงานด้วยที่มีคอมมิตเกินหมื่นนั้นมีคนทำงานเต็มเวลาหลายสิบหรือหลายร้อยคน [...] มีความซับซ้อนบางอย่างที่ต้องเอาชนะซึ่งโพสต์นั้นไม่ได้พูดถึงหรือเปล่า?

จำนวนคอมมิตไม่ใช่ตัวชี้วัดที่น่าเชื่อถือมากนัก เพราะผมอาจจะแก้ไขเล็กน้อยแล้วพุชเป็นคอมมิตเดียว คอมมิตจำนวนมากใน 16k นั้นเป็นคอมมิต "typo" หรือแค่ปรับปรุงคำอธิบายใน README เท่านั้น

อย่างไรก็ตาม จำนวนคอมมิตก็พอให้เห็นภาพความพยายามที่แท้จริงได้ นอกจากนี้ยังมีคอมมิตจำนวนมากที่เต็มไปด้วยการเปลี่ยนแปลง รวมถึงหลายสิบ และแม้แต่หลายร้อยการเปลี่ยนแปลงในครั้งเดียว การเปลี่ยนแปลงระหว่างเวอร์ชัน 0.8 และ 0.9 นั้นใหญ่มากจริงๆ และต้องใช้ความพยายามและเวลาในการทำให้สำเร็จ

ในบล็อกโพสต์นี้ ผมจะอธิบายว่าการเปลี่ยนแปลงเหล่านั้นคืออะไร เพื่ออธิบายว่าทำไมถึงใช้เวลานานขนาดนี้ และในการทำเช่นนั้น ผมจะให้ตัวอย่างคุณสมบัติขั้นสูงบางอย่างที่ถูกเพิ่มเข้าไปในโค้ดเบส ซึ่งจะเปิดเผยตัวเองพร้อมกับเวอร์ชัน 1.0 ที่กำลังจะมาถึง

ภูมิหลังของ GraphQL server

ขอเล่าประวัติของ engine นี้สักเล็กน้อย และรายละเอียดทางเทคนิคของวิธีการทำงาน

(ส่วนนี้เกี่ยวข้องกับนักพัฒนาเป็นหลัก หากคุณไม่สนใจเรื่องทางเทคนิค ยินดีต้อนรับให้ข้ามไปยังหัวข้อถัดไป)

Gato GraphQL สร้างขึ้นบน PoP ซึ่งเป็น engine ที่ render คอมโพเนนต์ใน PHP (คล้ายกับ React หรือ Vue บน JavaScript) การพึ่งพา engine นี้เป็นแบบสมบูรณ์ ซึ่งเป็นเหตุผลที่ปลั๊กอินนี้โฮสต์อยู่ใน monoRepo GatoGraphQL/GatoGraphQL บน GitHub

ภายใต้ฝากระโปรง การพึ่งพานี้มีลักษณะดังนี้:

Gato GraphQL แก้ GraphQL query โดยแรกสุดแปลงเป็น component model ที่เทียบเท่า จากนั้น PoP จะแก้ไขโดยดึงข้อมูลที่จำเป็นทั้งหมด แล้วข้อมูลนั้นจะถูกจัดรูปแบบให้ตรงกับ GraphQL query

เมื่อผมเริ่มทำงานกับ PoP ประมาณปี 2013/2014 ยังไม่มี GraphQL และวิธีการในการแปลง component model เป็นข้อมูลได้รับการออกแบบและนำไปใช้งานจากศูนย์ การขาด model ที่จะอ้างอิง (เช่น GraphQL สำหรับแนวคิด และ โปรเจกต์อ้างอิง graphql-js สำหรับการนำไปใช้) เป็นทั้งอุปสรรคและพร ดังที่จะอธิบายในภายหลัง

PoP ถูกออกแบบในตอนแรกเพื่อ render เว็บไซต์ทั้งหมดเป็น HTML ฝั่ง server ในขณะที่เปิดเผยข้อมูลดิบในรูปแบบ JSON เมื่อเพิ่ม ?output=json ต่อท้าย URL ของหน้า และยังเลือกข้อมูลที่จะดึง (การตั้งค่า, ข้อมูล DB object) ด้วย URL params เพิ่มเติม

กรุณาคลิกที่ลิงก์ต่อไปนี้ (ทั้งหมดชี้ไปที่หน้าเว็บเดียวกัน แต่มี URL params ต่างกัน) และสังเกตว่ามันแตกต่างกันอย่างไร:

เมื่อคลิกลิงก์สุดท้าย จะมีความตระหนักเกิดขึ้น: นี่ก็เหมือน GraphQL มากเลย! ความแตกต่างที่สำคัญเพียงอย่างเดียวคือข้อมูลใน response เป็นแบบ implicit เพราะถูกกำหนดไว้แล้วโดยคอมโพเนนต์ (ใน PHP) ที่รวมอยู่ในหน้า ส่วน GraphQL นั้น อนุญาตให้เราตัดสินใจว่าจะดึงข้อมูลอะไรผ่าน query

ดังนั้น เมื่อผมได้เรียนรู้เกี่ยวกับ GraphQL ประมาณปี 2019 มันจึงเป็นเรื่องที่ชัดเจนสำหรับผมที่จะให้ PoP รองรับ GraphQL server ด้วย สิ่งที่ต้องทำคือรับ GraphQL query เป็น input และสร้าง component model แบบ on-the-fly ตาม query

และนั่นคือสิ่งที่ผมทำ มันทำงานได้ดี แต่มันช้า เพราะ PoP เข้าใจรูปแบบ input ของตัวเอง ดังนั้น GraphQL query จึงต้องถูกปรับให้เข้ากับรูปแบบ PoP:

  1. แยก GraphQL query; แล้วจึง
  2. แปลง query เป็นรูปแบบ PoP; แล้วจึง
  3. แยก PoP format

การแยก GraphQL query จึงทำสองครั้ง (ครั้งหนึ่งสำหรับ GraphQL อีกครั้งสำหรับ PoP) และรูปแบบ PoP ไม่ได้ถูกแก้ไขผ่าน AST แต่แค่แยก query string ซ้ำๆ (การไม่ใช้ AST เป็นการเขียนโค้ดที่แย่ แต่ผมไม่มี spec ที่จะตาม และการพัฒนาเกิดขึ้นแบบ organic ซึ่ง substr(...) ง่ายๆ จะแก้ปัญหาได้ในแต่ละวัน)

นี่คือเหตุผลที่ผมบอกว่าการไม่มี GraphQL spec เป็นอุปสรรค เพราะโซลูชันของผมช้า (และนี่คือสถานการณ์ในเวอร์ชัน 0.8) ผมจึงตัดสินใจแก้ไขมัน

การแปลง engine ให้เป็น GraphQL-first

โซลูชันที่ผมตัดสินใจคือให้ PoP พูดภาษา GraphQL ได้โดยธรรมชาติ จากนั้น การส่ง GraphQL query ไปยัง PoP เป็น input จะถูกแปลงเป็น component model ทันที โดยไม่ต้องการ adapter เพิ่มเติมใดๆ หรือทำสิ่งต่างๆ ซ้ำสองครั้ง

ซึ่งหมายความว่าโปรเจกต์ PoP ต้องถูกเปลี่ยนวัตถุประสงค์ใหม่ จากการเป็น PHP library ที่ render คอมโพเนนต์สำหรับเว็บไซต์ฝั่ง server ที่ถูกปรับให้แก้ GraphQL queries ไปเป็นการเป็น GraphQL server จริงๆ

โค้ดเบสจึงผ่านการแปลงครั้งใหญ่ โดยนำ GraphQL AST มาเป็นรากฐานในการสื่อสาร state ระหว่างบริการ PHP ทั้งหมดใน engine GraphQL AST objects ปัจจุบันเป็น input สำหรับ PoP (แทน query strings)

GraphQL servers อื่นๆ ใน PHP พึ่งพา graphql-php แต่ปลั๊กอิน Gato GraphQL ไม่ได้เป็นเช่นนั้น นี่เป็นข่าวร้ายในแง่ของความพยายามในการดูแลรักษา (เพราะผมไม่สามารถนำสิ่งที่คนอื่นเขียนมาใช้ซ้ำได้) แต่เป็นข่าวดีในแง่ของความเป็นอิสระ: ผมสามารถตัดสินใจเพิ่มคุณสมบัติที่กำหนดเองให้กับปลั๊กอินของผมด้วยความเร็วของตัวเองและตามเกณฑ์ของตัวเอง (ซึ่งเป็นเหตุผลที่ปลั๊กอินนี้มี "oneof" input object อยู่แล้ว)

และดังที่จะแสดงในหัวข้อด้านล่าง นี่เป็นข้อได้เปรียบที่ยอดเยี่ยม

การนำคุณสมบัติดั้งเดิมมาใส่ใน GraphQL

GraphQL มักถูกเชื่อมโยงกับการดึงข้อมูล โดยธรรมชาติ คุณสามารถดึงข้อมูลชิ้นใดก็ได้ (โพสต์, ผู้ใช้, ความคิดเห็น ฯลฯ) จาก 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
    }
  }
}

แต่นี่เป็นแค่ผลลัพธ์ที่ได้มาง่าย GraphQL ยังสามารถใช้กับกรณีการใช้งานอื่นๆ อีกมากมาย รวมถึงการจัดการและแปลงข้อมูล และแม้แต่การวาง GraphQL ในไปป์ไลน์เพื่อทำหน้าที่เป็นตัวกลางระหว่างบริการต่างๆ

ตัวอย่างบางส่วนที่ GraphQL มีประโยชน์:

  • การดึงข้อมูลจากหนึ่งหรือหลายแหล่ง (เช่น ผู้ใช้จากไซต์ WordPress และข้อมูลติดต่อ newsletter จาก Mailchimp) รวมข้อมูล และวิเคราะห์ทั้งหมดด้วยกันเป็น dataset เดียว
  • การดำเนินการเพื่อปรับเนื้อหาบนไซต์:
    • แบบครั้งเดียว เช่น เมื่อย้ายไซต์ไปยัง domain อื่นและแทนที่ "www.myoldsite.com" ด้วย "mynewsite.com" ทุกที่ในเนื้อหาและ metadata
    • แบบต่อเนื่อง เช่น แทนที่ "http://" ด้วย "https://" ทุกครั้งที่นักเขียนเผยแพร่บล็อกโพสต์ใหม่
  • เชื่อมต่อกับ Google Translate API เพื่อแปลบล็อกโพสต์ทั้งหมดเป็นภาษาอื่น
  • ส่งทวีตโดยอัตโนมัติหลังจากเผยแพร่บล็อกโพสต์

PoP ถูกออกแบบมาเพื่อรองรับกรณีการใช้งานอื่นๆ เหล่านี้ ผ่านคุณสมบัติที่ GraphQL ไม่รองรับ (โดยธรรมชาติ) เช่น:

  • รองรับ "functionality" fields (เพิ่มเติมจาก "data" fields) ซึ่งถูกเพิ่มในทุก type ใน schema
  • ส่งผลลัพธ์ของ field เป็น input ให้กับ field อื่นภายใน query เดียวกัน
  • การประกอบ directives เพื่อให้ directive หนึ่งปรับเปลี่ยนพฤติกรรมของ directive อื่น
  • ตัดสินใจแบบ dynamic ว่าจะใช้ directive หรือไม่ ตามค่าของ field

และผมแน่ชัดว่าไม่ต้องการลบคุณสมบัติเหล่านี้ออกจาก GraphQL server: ผมเขียนโค้ดมันไปแล้ว และมันมีคุณค่าอย่างแน่นอน

ดังนั้นเหตุผลที่สองว่าทำไม v0.9 ถึงใช้เวลานานก็คือ ผมยังต้องหาวิธีที่จะนำความสามารถใหม่ๆ เหล่านี้มาใส่ใน GraphQL โดยไม่ทำลาย GraphQL spec (เช่น การเพิ่ม elements ใหม่ใน GraphQL syntax เป็นสิ่งที่ทำไม่ได้)

ตัวอย่างการจัดการข้อมูลใน GraphQL

ความสามารถใหม่ที่แนะนำใน GraphQL ในปลั๊กอินจะมองเห็นได้ชัดขึ้นในอนาคตอันใกล้ เมื่อเวอร์ชัน 1.0 ได้รับการปล่อยออกมา แต่คุณสามารถลิ้มรสบางส่วนของมันได้แล้วตอนนี้

GraphQL query ต่อไปนี้ดึงรายการ user entries จาก external REST API (ซึ่งสามารถ @remove ออกจาก response ได้); ป้อนข้อมูลนี้เข้าสู่อีก field หนึ่ง ภายใน query เดียวกัน; ดึง email property จากแต่ละ entry; และในที่สุดแปลง email เป็นตัวพิมพ์ใหญ่ แต่เฉพาะในกรณีที่ภาษาในแต่ละ entry นั้นเป็นภาษาอังกฤษหรือเยอรมัน:

###################################################################
# 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
}

นี่คือ response (โปรดสังเกตว่าเฉพาะ email บางรายการเท่านั้นที่ถูกแปลงเป็นตัวพิมพ์ใหญ่):

{
  "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"
    ]
  }
}

ลองด้วยตัวเอง! กด "Run" เพื่อ execute 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
}

ผมได้กล่าวถึงว่าการไม่ได้รับการชี้นำจาก GraphQL เป็นอุปสรรค แต่ (เมื่อมองย้อนกลับไป) ก็เป็นพรด้วยเช่นกัน เพราะผมไม่มีข้อจำกัดของ GraphQL spec จึงสามารถฝันถึงความสามารถใหม่ๆ เหล่านี้ได้

และตอนนี้ที่คุณสมบัติเหล่านี้ได้ถูกย้ายมายัง Gato GraphQL แล้ว มันสามารถเป็นผู้ช่วยที่มีประโยชน์อย่างเหลือเชื่อสำหรับทุกอย่างที่เกี่ยวข้องกับการดึงข้อมูล การจัดการ และการแปลงสำหรับไซต์ WordPress ของคุณ (แม้ว่าจะเข้าถึงได้เฉพาะกับ v1.0 ที่กำลังจะมาถึงเท่านั้น)

ใช้เวลาสักพัก แต่ความพยายามนั้นคุ้มค่าอย่างแน่นอน

ลองใช้ดูเลย!

คุณเชื่อแล้วหรือยังว่าการรอนานนั้นคุ้มค่า? ผมหวังว่าจะเป็นเช่นนั้น!

ไปเลย ดาวน์โหลดปลั๊กอิน แล้วลองดู:

สนใจรับข่าวสารเกี่ยวกับการพัฒนา เอกสารใหม่ และ releases ที่กำลังจะมาถึง รวมถึง v1.0 หรือไม่? ยินดีต้อนรับให้สมัคร newsletter

ต้องการสำรวจโค้ด open source บน GitHub ไหม? ดู GatoGraphQL/GatoGraphQL (และยินดีต้อนรับให้ติดดาว... เราชอบดาว! ⭐️⭐️⭐️)

อนึ่ง คุณต้องการทำ content transformation อะไรใน WordPress บ้าง (ซึ่งคุณอาจใช้ปลั๊กอินเชิงพาณิชย์เฉพาะทางอยู่แล้ว)? กรุณาส่งข้อความบอกกรณีการใช้งานของคุณ

ถ้าคุณชอบสิ่งที่เห็น กรุณาแชร์ให้เพื่อนและเพื่อนร่วมงาน ช่วยกันแพร่ความรัก ❤️


สมัครรับจดหมายข่าวของเรา

ติดตามการอัปเดตทั้งหมดของ Gato GraphQL