TypeScript ครบทุกเรื่อง: Monorepo กับปัญหาและประโยชน์ของมัน
ในบทความนี้ ฉันจะไม่เปรียบเทียบ monorepo และ polyrepo เพราะมันเป็นเรื่องของปรัชญา แทนที่จะเปรียบเทียบ ฉันจะเน้นที่ประสบการณ์การสร้างและพัฒนาและถือว่าคุณคุ้นเคยกับระบบนิเวศของ JS/TS แล้ว
บทนำ
ฉันมักจะมีความฝันเกี่ยวกับ 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
ด้วยของคุณเอง):
- ถ้าคุณใช้ปลั๊กอิน ESLint ของ VSCode เพิ่มการตั้งค่า VSCode ด้านล่างเพื่อให้มันเคารพการตั้งค่า ESLint ต่อแพ็คเกจ (แทนค่าของ
- 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 จะดูแบบนี้:
สรุป
ในอุดมคติมีเพียงสองขั้นตอนสำหรับวิศวกร/ผู้มีส่วนร่วมรายใหม่ในการเริ่มต้น:
- โคลน repo
pnpm i && pnpm dev
บันทึกปิดท้าย
ทีมของเราได้พัฒนาภายใต้แนวทางนี้เป็นเวลาหนึ่ งปีแล้วและเรารู้สึกพอใจกับมัน ไปที่ GitHub repo ของเรา เพื่อดูรูปร่างล่าสุดของโปรเจ็กต์ หากจะสรุป:
ปัญหา
- ต้องคุ้นเคยกับระบบนิเวศของ JS/TS
- ต้องเลือกตัวจัดการแพ็คเกจที่ถูกต้อง
- ต้องตั้งค่าเพียงครั้งเดียวเพิ่มเติม
ประโยชน์
- พัฒนาและบำรุงรักษาโปรเจ็กต์ทั้งหมดใน repo เดียว
- ความต้องการทักษะการเขียนโค้ดที่ง่ายขึ้น
- โค้ดสไตล์, สคีมา, phrases และ utilities ที่แชร์กัน
- ประสิทธิภาพในการสื่อสารที่ดีขึ้น
- ไม่มีคำถามเช่น: การกำหนด API คืออะไร?
- วิศวกรทุกคนพูดในภาษาการเขียนโปรแกรมเดียวกัน
- CI/CD อย่างง่าย
- ใช้เครื่องมือเดียวกันสำหรับการสร้าง, ทดสอบ และเผยแพร่
บทความนี้ยังคงมีหัวข้อหลายเรื่องที่ไม่ได้กล่าวถึง: การตั้งค่า repo จากศูนย์, การเพิ่มแพ็คเกจใหม่, และการใช้ GitHub Actions สำหรับ CI/CD เป็นต้น มันจะยาวเกินไปสำหรับบทความนี้ถ้า ฉันจะขยายแต่ละอัน ไม่ต้องลังเลที่จะ comment และบอกฉันว่าหัวข้อใดที่คุณอยากเห็นในอนาคต