• typescript
  • monorepo

TypeScript ครบทุกเรื่อง: Monorepo กับปัญหาและประโยชน์ของมัน

ในบทความนี้ ฉันจะไม่เปรียบเทียบ monorepo และ polyrepo เพราะมันเป็นเรื่องของปรัชญา แทนที่จะเปรียบเทียบ ฉันจะเน้นที่ประสบการณ์การสร้างและพัฒนาและถือว่าคุณคุ้นเคยกับระบบนิเวศของ JS/TS แล้ว

Gao
Gao
Founder

บทนำ

ฉันมักจะมีความฝันเกี่ยวกับ monorepo

ฉันได้เห็นแนวทาง monorepo ในขณะทำงานให้กับ Airbnb แต่เป็นเพียงสำหรับ frontend เท่านั้น ด้วยความรักที่ลึกซึ้งต่อระบบนิเวศของ JavaScript และประสบการณ์การพัฒนา TypeScript ที่ "แฮปปี้" ฉันเริ่มที่จะจัดโค้ด frontend และ backend ในภาษาเดียวกันตั้งแต่ประมาณสามปีที่แล้ว มันเยี่ยมมาก (สำหรับการจ้างงาน) แต่มันก็ไม่ดีนักสำหรับการพัฒนาเพราะโปรเจ็กต์ของเรายังคงกระจัดกระจายอยู่ในหลาย repos.

ดังที่คำกล่าวว่า “วิธีที่ดีที่สุดในการปรับปรุงโครงสร้างโปรเจ็กต์คือการเริ่มต้นใหม่” ดังนั้นเมื่อฉันเริ่มต้นธุรกิจ startups ประมาณหนึ่งปีที่แล้ว ฉันตัดสินใจที่จะใช้กลยุทธ์ total monorepo: ใส่โปรเจ็กต์ frontend และ backend แม้กระทั่งสคีมาฐานข้อมูลเข้าใน repo เดียว

ในบทความนี้ ฉันจะไม่เปรียบเทียบ monorepo และ polyrepo เพราะมันเป็นเรื่องของปรัชญา แทนที่จะเปรียบเทียบ ฉันจะเน้นที่ประสบการณ์การสร้างและพัฒนาและถือว่าคุณคุ้นเคยกับระบบนิเวศของ JS/TS แล้ว

ผลลัพธ์สุดท้ายสามารถดูได้ที่ GitHub.

ทำไมต้อง TypeScript?

พูดตามตรง ฉันเป็นแฟนของ JavaScript และ TypeScript ฉันรักความเข้ากันได้ของความยืดหยุ่นและความเข้มข้นของมัน: คุณสามารถกลับไปใช้ unknown หรือ any (แม้ว่าเราจะแบนฟอร์มของ any ทุกรูปแบบในฐานข้อมูลโค้ดของเรา) หรือใช้ชุดกฎ lint ที่เข้มงวดมากเพื่อจัดโค้ดสไตล์ให้ตรงกันในทีม

เมื่อเรากำลังพูดคุยเกี่ยวกับแนวคิดของ “fullstack” ก่อนหน้านี้ เรามักจะจินตนาการถึงอย่างน้อยสองระบบนิเวศและภาษาการเขียนโปรแกรม: หนึ่งสำหรับ frontend และหนึ่งสำหรับ backend

วันหนึ่ง ฉันได้ตระหนักว่าอาจจะง่ายขึ้น: Node.js เร็วพอ (เชื่อฉันเถอะ ในกรณีส่วนใหญ่ คุณภาพของโค้ดสำคัญกว่าความเร็วในการทำงาน) TypeScript ก็โตพอ (ทำงานได้ดีในโปรเจ็กต์ frontend ขนาดใหญ่) และแนวคิด monorepo ก็ได้รับการฝึกฝนจากทีมน้อยชื่อไม่กี่ทีม (React, Babel, เป็นต้น) - ดังนั้นทำไมเราไม่นำโค้ดทั้งหมดมารวมกัน ตั้งแต่ frontend ไปจนถึง backend? นี่สามารถทำให้นักพัฒนาทำงานได้โดยไม่ต้องเปลี่ยนบริบทใน repo หนึ่งและทำ feature ที่สมบูรณ์ใน (เกือบ) ภาษาหนึ่งเดียว

การเลือกตัวจัดการแพ็คเกจ

ในฐานะนักพัฒนาและอย่างที่เคย ฉันอดใจไม่ไหวที่จะเริ่ม coding แต่ครั้งนี้สิ่งต่าง ๆ เป็นอย่างต่างไป

การเลือกตัวจัดการแพ็คเกจเป็นสิ่งสำคัญต่อประสบการณ์การพัฒนาใน monorepo

ความเจ็บปวดของความเฉื่อย

มันคือกรกฎาคม 2021 ฉันเริ่มด้วย [email protected] เพราะฉันได้ใช้มันมานานแล้ว Yarn เร็ว แต่ไม่นานฉันก็พบปัญหาหลายอย่างกับ Yarn Workspaces เช่น ไม่ยกเลิกการขึ้นทะเบียนที่ถูกต้อง และได้รับการติดแท็กมากมายว่า "แก้ไขในเวอร์ชันใหม่" ซึ่งพาฉันไปยัง v2 (berry).

“โอเค งั้นฉันจะอัพเกรดเดี๋ยวนี้” ฉันหยุดฝืนกับ v1 และเริ่มที่จะแปลง แต่คู่มือการโยกย้ายที่ยาวของเบอร์รี่ทำให้ฉันกลัว และฉันยอมแพ้หลังจากความพยายามล้มเหลวหลายครั้ง

มันก็แค่ใช้งานได้

ดังนั้นการวิจัยเกี่ยวกับตัวจัดการแพ็คเกจก็เริ่มขึ้น ฉันหลงไหลใน pnpm หลังจากทดลอง: เร็วเท่า yarn, การสนับสนุน native monorepo, คำสั่งที่คล้ายกับ npm, hard links, เป็นต้น สิ่งสำคัญที่สุดคือมันเพียงแค่ทำงาน ในฐานะนักพัฒนาที่ต้องการเริ่มต้นด้วยผลิตภัณฑ์แต่ไม่ใช่พัฒนาตัวจัดการแพ็คเกจ ฉันแค่ต้องการเพิ่ม dependencies บางส่วนและเริ่มโปรเจ็กต์โดยไม่ต้องรู้ว่าตัวจัดการแพ็คเกจทำงานอย่างไรหรือมีแนวคิดที่แฟนซีใด ๆ

บนพื้นฐานของแนวคิดเดียวกัน เราเลือก lerna ซึ่งเป็นเพื่อนเก่า สำหรับการเรียกใช้งานคำสั่งระหว่างแพ็คเกจและการเผยแพร่แพ็คเกจของ workspace

การกำหนดขอบเขตของแพ็คเกจ

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

โครงสร้างเริ่มต้นของเรา ประกอบด้วยสี่แพ็คเกจ:

  • core: การบริการ backend monolith.
  • phrases: คีย์ i18n → แหล่งที่มา phrases.
  • schemas: สคีมาฐานข้อมูลและ TypeScript schemas แบ่งปัน.
  • ui: web SPA ที่โต้ตอบกับ core.

เทคโนโลยีชุดสำหรับ fullstack

เนื่องจากเรายอมรับระบบนิเวศ JavaScript และใช้ TypeScript เป็นภาษาการเขียนโปรแกรมหลักของเรา จึงมีหลายตัวเลือกที่ชัดเจน (สมตามรสนิยมของฉัน 😊):

  • koajs สำหรับบริการ backend (core): ฉันมีประสบการณ์ที่ยากลำบากในการใช้ async/await ใน express ดังนั้นฉันตัดสินใจใช้สิ่งที่มีการสนับสนุน native.
  • i18next/react-i18next สำหรับ i18n (phrases/ui): ชอบความเรียบง่ายของ APIs และการสนับสนุน TypeScript ที่ดี.
  • react สำหรับ SPA (ui): เป็นรสนิยมส่วนตัว.

แล้ว schemas ล่ะ?

บางสิ่งยังขาดหายไปที่นี่: ระบบฐานข้อมูลและการแมปสคีมา <-> บน TypeScript definition.

ทั่วไป v.s. เจาะจงตามความคิดเห็น

ในตอนนั้น ฉันได้ลองวิธีที่นิยมสองแนวทาง:

  • ใช้ ORM กับตัวตกแต่งมากมาย.
  • ใช้ query builder อย่าง Knex.js.

แต่ทั้งคู่ให้ความรู้สึกแปลก ๆ ในระหว่างการพัฒนาก่อนหน้านี้:

  • สำหรับ ORM: ฉันไม่ใช่แฟนของตัวตกแต่งและชั้นที่เป็นนามธรรมอีกชั้นของฐานข้อมูลทำให้ต้องเรียนรู้เพิ่มเติมและความไม่แน่นอนสำหรับทีม.
  • สำหรับ query builder: มันเหมือนเขียน SQL พร้อมกับข้อจำกัดบางอย่าง (ในทางที่ดี) แต่มันไม่ใช่ SQL จริง ดังนั้นเราต้องใช้ .raw() สำหรับ queries ต้นแบบในหลายสถานการณ์.

แล้วฉันเห็นบทความนี้: “หยุดใช้ Knex.js: การใช้ SQL query builder เป็นรูปแบบที่ยึดติดกับทางที่ผิด”. ชื่อดูน่ารุกราน แต่เนื้อหายอดเยี่ยมมาก มันเตือนฉันอย่างแรงว่า “SQL 是一种编程语言” และฉันได้ตระหนักว่าฉันสามารถเขียน SQL โดยตรง (เหมือนกับ CSS, ฉันจะพลาดสิ่งนี้ได้อย่างไร!) เพื่อใช้ประโยชน์จากคุณลักษณะของภาษาพื้นเมืองและฐานข้อมูลแทนที่จะเพิ่มอีกหนึ่งชั้นและลดพลังลง

โดยสรุป ฉันตัดสินใจที่จะติดกับ Postgres และ Slonik (ตัว Progres client ที่โอเพนซอร์ส) ตามที่บทความระบุ:

…ประโยชน์ของการอนุญาตให้ผู้ใช้เลือก ระหว่าง dialects ของฐานข้อมูลต่าง ๆ นั้นมีน้อยและการพัฒนาเหนือหัวของการพัฒนาสำหรับฐานข้อมูลหลายๆ ฐานพร้อมกัน เป็นที่มีนัยสำคัญ.

SQL <-> TypeScript

ข้อดีอีกประการหนึ่งของการเขียน SQL คือเราสามารถใช้งานมันได้ง่าย ๆ เป็นแหล่งข้อมูลเดียวของ definitions ของ TypeScript ฉันได้เขียน ตัวสร้างโค้ด เพื่อ transcompile สคีมา SQL เป็นโค้ด TypeScript ที่เราจะใช้ใน backend ของเราและผลลัพธ์ดูไม่เลว:

เราสามารถเชื่อม jsonb กับ type TypeScript และดำเนินการตรวจสอบตาม type validation ในบริการ backend ถ้าจำเป็น

ผลลัพธ์

โครงสร้าง related dependency สุดท้ายดูเหมือน:

คุณอาจสังเกตว่ามันเป็นแผนภาพแบบทิศทางเดียว ซึ่งช่วยเรามากในการรักษาสถาปัตยกรรมที่ชัดเจนและความสามารถในการขยายเมื่อโปรเจ็กต์เติบโตขึ้น บวกกับโค้ดทั้งหมด (พื้นฐาน) ในภาษา TypeScript

ประสบการณ์การพัฒนา

การแชร์แพ็คเกจและการตั้งค่า

ภายใน dependencies

pnpm และ lerna ทำงานได้ยอดเยี่ยมในการพึ่งพาหน่วยงานของ workspace ภายใน เราใช้คำสั่งด้านล่างใน root โปรเจ็กต์เพื่อเพิ่มแพ็คเกจพี่น้อง:

มันจะเพิ่ม @logto/schemas เป็น dependency ใน @logto/core ขณะที่รักษารุ่นเชิงความหมายใน package.json ของ dependencies ภายในของคุณ pnpm ยังสามารถเชื่อมโยงพวกเขาอย่างถูกต้องใน pnpm-lock.yaml ผลลัพธ์จะดูประมาณนี้:

การแชร์การตั้งค่า

เราปฏิบัติต่อแพ็คเกจทุกชิ้นใน monorepo ว่าเป็น “อิสระ” ดังนั้นเราสามารถใช้วิธีการมาตรฐานสำหรับการแชร์การตั้งค่า ซึ่งครอบคลุม tsconfig, eslintConfig, prettier, stlyelint และ jest-config ดู โปรเจ็กต์นี้ เป็นตัวอย่าง

โค้ด, lint และคอมมิต

ฉันใช้ VSCode สำหรับการพัฒนาประจำวันที่กำหนดอย่างสั้น ๆ ว่าไม่มีอะไรแตกต่างเมื่อโปรเจ็กต์ถูกตั้งค่าอย่างถูกต้อง:

  • ESLint และ Stylelint ทำงานปกติ.
    • ถ้าคุณใช้ปลั๊กอิน ESLint ของ VSCode เพิ่มการตั้งค่า VSCode ด้านล่างเพื่อให้มันเคารพการตั้งค่า ESLint ต่อแพ็คเกจ (แทนค่าของ pattern ด้วยของคุณเอง):
  • husky, commitlint และ lint-staged ทำงานตามที่คาดหวัง

คอมไพเลอร์และพร็อกซี

เรากำลังใช้คอมไพเลอร์ต่างกันสำหรับ frontend และ backend: parceljs สำหรับ UI (React) และ tsc สำหรับแพ็คเกจ TypeScript ล้วน ๆ อื่น ๆ ฉันแนะนำอย่างยิ่งให้คุณลอง parceljs หากคุณยังไม่ได้ มันคือคอมไพเลอร์ “ไม่มีการตั้งค่า” จริง ๆ ที่จัดการไฟล์ประเภทต่าง ๆ อย่างยอดเยี่ยม

Parcel โฮสต์เซิร์ฟเวอร์ dev frontend ของตัวเอง และผลลัพธ์ผลิตภัณฑ์คือไฟล์สถิติเพียงอย่างเดียว เนื่องจากเราต้องการเมาท์ APIs และ SPA ใต้ต้นกำเนิดเดียวกันเพื่อหลีกเลี่ยง problem CORS กลยุทธ์ด้านล่างทำงาน:

  • ในสิ่งแวดล้อม dev ใช้พร็อกซี HTTP ธรรมดาเพื่อเปลี่ยนเส้นทางการจราจรไปยังเซิร์ฟเวอร์ dev ของ Parcel.
  • ในการผลิต ให้ prompt ไฟล์สถิติและ

คุณสามารถหาการดำเนินฟังก์ชั่น middleware frontend ที่นี่.

โหมดเฝ้าดู

เรามีสคริปต์ dev ใน package.json สำหรับแต่ละแพ็คเกจที่เฝ้าดูการเปลี่ยนแปลงของไฟล์และคอมไพล์ใหม่เมื่อจำเป็น ขอบคุณ lerna ที่ทำให้สิ่งต่าง ๆ กลายเป็นเรื่องง่ายโดยใช้ lerna exec เพื่อเรียกใช้สคริปต์แพ็คเกจอย่างขนานกัน สคริปต์ root จะดูแบบนี้:

สรุป

ในอุดมคติมีเพียงสองขั้นตอนสำหรับวิศวกร/ผู้มีส่วนร่วมรายใหม่ในการเริ่มต้น:

  1. โคลน repo
  2. pnpm i && pnpm dev

บันทึกปิดท้าย

ทีมของเราได้พัฒนาภายใต้แนวทางนี้เป็นเวลาหนึ่งปีแล้วและเรารู้สึกพอใจกับมัน ไปที่ GitHub repo ของเรา เพื่อดูรูปร่างล่าสุดของโปรเจ็กต์ หากจะสรุป:

ปัญหา

  • ต้องคุ้นเคยกับระบบนิเวศของ JS/TS
  • ต้องเลือกตัวจัดการแพ็คเกจที่ถูกต้อง
  • ต้องตั้งค่าเพียงครั้งเดียวเพิ่มเติม

ประโยชน์

  • พัฒนาและบำรุงรักษาโปรเจ็กต์ทั้งหมดใน repo เดียว
  • ความต้องการทักษะการเขียนโค้ดที่ง่ายขึ้น
  • โค้ดสไตล์, สคีมา, phrases และ utilities ที่แชร์กัน
  • ประสิทธิภาพในการสื่อสารที่ดีขึ้น
    • ไม่มีคำถามเช่น: การกำหนด API คืออะไร?
    • วิศวกรทุกคนพูดในภาษาการเขียนโปรแกรมเดียวกัน
  • CI/CD อย่างง่าย
    • ใช้เครื่องมือเดียวกันสำหรับการสร้าง, ทดสอบ และเผยแพร่

บทความนี้ยังคงมีหัวข้อหลายเรื่องที่ไม่ได้กล่าวถึง: การตั้งค่า repo จากศูนย์, การเพิ่มแพ็คเกจใหม่, และการใช้ GitHub Actions สำหรับ CI/CD เป็นต้น มันจะยาวเกินไปสำหรับบทความนี้ถ้าฉันจะขยายแต่ละอัน ไม่ต้องลังเลที่จะ comment และบอกฉันว่าหัวข้อใดที่คุณอยากเห็นในอนาคต