Tech Blog

การเปรียบเทียบ UI เว็บธุรกิจเดียวกันที่ใช้งานโดยใช้ Vanilla HTML / Vue / React / Thymeleaf — ความแตกต่างระหว่าง 4 สแต็กและแนวทางการเลือก

Vanilla HTML Vue3 React Thymeleaf Spring Boot TypeScript Java フロントエンド 比較 ライブラリ選定

สิ่งที่คุณสามารถเรียนรู้ได้จากบทความนี้

  • อะไรคือความแตกต่างในโค้ดเมื่อใช้ Business Web UI ที่มีข้อกำหนดเดียวกันโดยใช้สี่สแต็ก: Vanilla HTML / Vue 3 / React / Thymeleaf (พร้อมตัวอย่างโค้ด)
  • ความแตกต่างในการกำหนดค่า/บิลด์โปรเจ็กต์ (package.json / Vite / pom.xml / HTML ธรรมดา)
  • การเปรียบเทียบตามมุมมอง ของไคลเอนต์ API การจัดการสถานะ การประมวลผลแบบฟอร์ม การแสดงข้อผิดพลาด และการปรับใช้
  • แกนตัดสินว่า “จะเลือกกองไหนในสถานการณ์ไหน”

กลุ่มเป้าหมาย

  • ผู้ที่สงสัยว่าควรใช้ Framework ใดระหว่าง Vanilla / Vue / React / Thymeleaf
  • แม้ว่าคุณจะใช้ผลิตภัณฑ์แต่ละชนิดแยกกัน แต่คุณก็อยากทราบว่าเมื่อเลือกใช้ผลิตภัณฑ์ที่มีข้อมูลจำเพาะเหมือนกันจะมีความแตกต่างกันอย่างไร
  • ผู้ที่ต้องการเปรียบเทียบสแต็กบนสถานที่ตั้งของการสร้าง UI ของเว็บที่เกี่ยวข้องกับธุรกิจ (แบบฟอร์ม + การเรียก API + การแสดงผลลัพธ์)

สภาพแวดล้อมการทำงาน

รายการเวอร์ชั่น
วานิลลา HTMLไม่จำเป็นต้องสร้าง (สคริปต์โมดูล)
วิว3.x + Vite 5.x + TypeScript 5.x + เราเตอร์ Vue 4.x
ตอบสนอง18.x + Vite 5.x + TypeScript 5.x + ตอบสนองเราเตอร์ 6.x
ไธม์ลีฟSpring Boot 3.x + Java 21 + Thymeleaf 3.x

1. บทนำ

พูดตามตรง เดิมทีฉันไม่ได้ตั้งใจจะเขียนบทความเปรียบเทียบ

ในขณะที่พัฒนาแบ็กเอนด์สำหรับ banklink-service ฉันคิดว่า ฉันต้องการหน้าจอที่สามารถตรวจสอบได้ว่า API ทำงานหรือไม่'' ดังนั้นฉันจึงสุ่มรวบรวมมันโดยใช้ Vanilla HTML ซึ่งเป็นวิธีที่รวดเร็วที่สุดในการสร้างมันขึ้นมาทันที **มันเป็น UI แบบใช้แล้วทิ้งที่ดูเหมือนรูปภาพ "แค่ย้ายไปตอนนี้"**เมื่อดำเนินการพัฒนาต่อ ฉันคิดว่า ในที่สุดฉันก็จะแทนที่มันด้วย Vue หรือ React และทำให้มันเป็นเทมเพลตการพัฒนาที่ครบถ้วน” ฉันวางแผนที่จะเริ่มการใช้งานหลักของส่วนหน้าจากที่นั่น

**แต่ในขณะที่ฉันพยายามจะขยับมือ ฉันก็สังเกตเห็นอะไรบางอย่าง **

ด้วยการประมวลผลจำนวนนี้ (แบบฟอร์ม + การเรียก API + การแสดงผลลัพธ์) จะใช้เวลาไม่นานในการปรับใช้โดยใช้ทั้ง 4 สแต็ก รวมถึง Thymeleaf ด้วย ยิ่งกว่านั้นถ้าพวกมันมีฟังก์ชั่นเหมือนกัน จะเป็นเรื่องดีหรือไม่ที่จะเปรียบเทียบความแตกต่างในแต่ละเทคโนโลยีไปพร้อมๆ กัน? ** และ.

เท่าที่งานพัฒนาดำเนินไป นี่ถือเป็น เส้นทางที่ผิดทาง โดยสิ้นเชิง ก่อนที่ฉันจะรู้ แผนการของฉันในการ “สร้างเทมเพลตเดียว” กลายเป็น “การใช้งานแบบขนานกับ 4 สแต็กแล้วเปรียบเทียบกัน”

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


ดังนั้นหัวข้อหลัก

เมื่อสร้าง UI ของเว็บที่เกี่ยวข้องกับธุรกิจ การตัดสินใจแรกที่คุณต้องทำคือ “Vanilla HTML / Vue / React / Thymeleaf ใดเป็นตัวเลือกที่ดีที่สุด” แต่ละคนมักถูกพูดถึงกันคนละเรื่อง แต่มีบทความไม่กี่บทความที่น่าประหลาดใจที่อธิบายว่า “อะไรคือความแตกต่างเมื่อคุณจัดเรียงสี่บทความที่มีสเปคเหมือนกัน? ด้วยรหัสจริง

ดังนั้นเราจึงใช้ UI ธุรกิจที่มีข้อกำหนดเดียวกันทุกประการโดยใช้ 4 สแต็กขนานกันโดยใช้บริการ Banking API Wrapper banklink-service (โครงการฝึกหัดส่วนบุคคล) บทความนี้เป็นบันทึกการเปรียบเทียบนั้น> 🔗 บทความที่เกี่ยวข้อง: การออกแบบแบ็กเอนด์ ของ banklink-service (คำจำกัดความข้อกำหนด → การออกแบบพื้นฐาน → การออกแบบโดยละเอียด → PoC, การตัดสินใจออกแบบโดเมน 5 รายการ, การใช้งาน DDD / ArchUnit / คุณภาพทางการเงิน) มีการสรุปไว้ในบทความแยกต่างหาก บทความนี้เป็น การเปรียบเทียบ 4 สแต็กที่ส่วนหน้า ที่เชื่อมต่อกับแบ็คเอนด์นั้น

การสร้าง Banking API จากการออกแบบ: จากคำจำกัดความข้อกำหนดของ 5 โดเมนไปจนถึงการตรวจสอบ PoC และแผนงานไปจนถึงความทนทานของธุรกิจ

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


2. ข้อกำหนดทั่วไป (สมมติว่า 4 กองอยู่ในแนวเดียวกัน)

การใช้งานทั้ง 4 รายการตรงตามข้อกำหนดต่อไปนี้:

คุณสมบัติ

  • หน้า 6: หน้าแรก / บัญชี / สินเชื่อ / สกุลเงินต่างประเทศ / การลงทุน / KYC
  • ส่วนการทำงานของ API หลายส่วนในแต่ละหน้า (ตัวอย่าง: หน้าบัญชีมี 5 ส่วน: “การได้มาของรายการ, การได้มาซึ่งยอดคงเหลือ, การฝาก, การถอนเงิน, ประวัติการทำธุรกรรม”)
  • ตั้งค่าโทเค็นการตรวจสอบสิทธิ์ API จากช่องป้อนข้อมูล โทเค็นผู้ถือ ที่ด้านบนของหน้าจอ
  • การนำทางช่วยให้คุณสามารถกลับไปกลับมาระหว่างหน้าต่างๆ
  • กดปุ่มเพื่อเข้าสู่ API และแสดงผลบนหน้าจอ

แบ็กเอนด์ที่จะเชื่อมต่อ

  • Spring Boot API เดียวกัน (/api/v1/accounts, /api/v1/loans, …)
  • การตอบสนองคือ JSON ข้อผิดพลาดจะรวมเป็นหนึ่งเดียวกับสถานะ HTTP + โครงสร้างเนื้อหา

จุดที่ไม่ตรงกัน (จุดแตกต่าง)

  • รูปลักษณ์ CSS (ตั้งใจเปลี่ยนใน UI ภายนอก/UI ภายใน)
  • การใช้งานภายใน (ตามลักษณะกรอบงาน)

กล่าวอีกนัยหนึ่ง เราได้สร้างฐานการเปรียบเทียบ “หน้าจอและฟังก์ชันเดียวกัน โดยมีเนื้อหาต่างกันเพียง 4 รายการเท่านั้นหน้าจอ “บัญชี” ที่จะเจาะลึกในบทความนี้ — เวอร์ชัน UI ภายนอกของ Vanilla HTML ทั้ง 4 สแต็กมีฟังก์ชันการทำงานเหมือนกัน

หน้าจอบัญชี (ด้านบน) มี 5 ส่วน: “รับรายการบัญชี รับยอด ฝาก ถอน และประวัติการทำธุรกรรม” หน้าจอเดียวกันและฟังก์ชันเดียวกันนี้ใช้งานแยกกันใน 4 สแต็ค เป็นเรื่องของการเปรียบเทียบในบทความนี้


3. ภาพรวมการกำหนดค่าสแต็ก 4

ก่อนอื่น มาดู “การกำหนดค่าขั้นต่ำ” แต่ละรายการกันก่อน

วานิลลา HTML

banklink-web-vanilla-html/
├─ index.html ← หน้าแรก
├─ บัญชี.html ← หน้าบัญชี
├─กู้ยืมเงิน.html ← หน้าเงินกู้
├─ ... (อีก 4 หน้า)
├─ common-external.js ← การจัดการโทเค็น + การรวมทั่วไป
└─ แบ่งปัน/
    ├─ api/client.js ← ดึงข้อมูล wrapper
    └─ common.js ← bindAction / renderResponse

ไม่มีเครื่องมือสร้าง .html สามารถเปิดได้โดยตรงในเบราว์เซอร์ (หรือแจกจ่ายด้วย nginx) นำเข้า JS ด้วย <script type="module">

วิว````

banklink-เว็บ-vue/ ├─vite.config.ts ├─ index.html ← รายการสปา └─ src/ ├─ main.ts ← createApp + ภูเขา ├─ App.vue ← เค้าโครง + RouterView ├─ เราเตอร์/index.ts ← การตั้งค่าเราเตอร์ Vue ├─ api/client.ts ← ดึงข้อมูล wrapper (TypeScript) └─ จำนวนการดู/ ├─ HomeView.vue ├─ AccountsView.vue ← หน้าบัญชี ├─ … (อีก 4 หน้า)


สร้างไฟล์คงที่ใน `dist/` ด้วย `npm run build` → แจกจ่ายด้วย nginx

### โต้ตอบ

banklink-เว็บตอบสนอง/ ├─vite.config.ts ├─ index.html └─ src/ ├─ main.tsx ← createRoot + เรนเดอร์ ├─ App.tsx ← รวมทั้ง 6 หน้าเป็นไฟล์เดียว (เพราะมันเล็ก) └─ … (shared/api/client.ts)


คล้ายกับ Vue แต่ทุกหน้าเขียนด้วย `App.tsx` (ลดจำนวนส่วนประกอบให้เหลือน้อยที่สุด)

### ไธม์ลีฟ````
banklink-web-thymeleaf/
├─ pom.xml
└─ banklink-external-web-thymeleaf/
    └─ src/หลัก/
        ├─ java/com/y104autumn/banklink/ภายนอก/
        │ ├─ BanklinkExternalApplication.java
        │ ├─ ตัวควบคุม/
        │ │ ├─ ExternalTopPageController.java
        │ │ ├─ AccountsController.java
        │ │ └─ ... (อีก 4 หน้า)
        │ ├─ บริการ/
        │ │ ├─ AccountsService.java ← การเรียก API ด้วย RestClient
        │ │ └─ ...
        │ └─ แบบฟอร์ม/
        │ ├─ AccountsForm.java ← สำหรับ @ModelAttribute
        │ └─ ...
        └─ ทรัพยากร/เทมเพลต/
            ├─ index.html
            ├─ account.html ← ด้วย th:field
            └─ ...

สร้าง jar ที่สามารถดำเนินการได้ด้วย mvn package → เริ่มต้นด้วย java -jar หรือ Docker

การเปรียบเทียบจำนวนไฟล์การกำหนดค่า| | โครงการทั้งหมด | ไฟล์ที่จำเป็นสำหรับการใช้งานหน้าเดียว |

|---|---|---| | วานิลลา HTML | ประมาณ 10 ไฟล์ | 1 (accounts.html เท่านั้น) | | วิว | ประมาณ 15 ไฟล์ | 1 (AccountsView.vue) | | ตอบสนอง | ประมาณ 5 ไฟล์ | 1 (ฟังก์ชั่นใน App.tsx) | | ไธม์ลีฟ | ประมาณ 25 ไฟล์ | 3 (ตัวควบคุม + บริการ + แบบฟอร์ม + เทมเพลต) |

Thymeleaf มีโค้ดมาตรฐานมากมายสำหรับแบ่ง MVC ออกเป็นสามชั้น แม้ว่าจะมีจำนวนไฟล์มากที่สุด แต่บทบาทของแต่ละไฟล์ก็มีความชัดเจน

6 หน้าที่ใช้งาน (ภาพหน้าจออ้างอิง)

ทั้ง 5 หน้านอกเหนือจากหน้าจอบัญชีจะมีฟังก์ชันเหมือนกันในทั้ง 4 สแต็ค เพื่อเป็นข้อมูลอ้างอิง หน้าจอของเวอร์ชัน Vanilla HTML มีดังต่อไปนี้

หน้าจอด้านบน (UI ภายนอก) — อินพุตโทเค็น + การนำทาง + นำไปสู่แต่ละหน้า

หน้าจอสินเชื่อ — ส่วนการทำงานของ API สำหรับการสมัคร การสอบถามยอดคงเหลือ การจำลองการชำระเงิน ฯลฯ

หน้าจอสกุลเงินต่างประเทศ — การดำเนินการ เช่น การรับอัตราแลกเปลี่ยนและการดำเนินการแลกเปลี่ยนสกุลเงิน

หน้าจอการลงทุน — พอร์ตโฟลิโอ คำสั่งซื้อ/ขาย ฯลฯ การดำเนินการ

หน้าจอ KYC — การดำเนินการ เช่น สถานะการยืนยันตัวตนและการส่งเอกสาร

การสร้างความแตกต่างด้วยภาพของ UI ภายนอกและ UI ภายใน

เราได้ใช้นโยบาย “ฟังก์ชันเหมือนกัน แต่รูปลักษณ์ภายนอกและ UI ภายในมีความแตกต่างกันโดยเจตนา” และ UI ภายใน (ใกล้กับเทอร์มินัลธุรกิจมากขึ้น) จะมีลักษณะเช่นนี้หน้าจอด้านบนของ UI ภายใน (เวอร์ชัน Vanilla HTML) — การออกแบบที่สร้างความแตกต่างโดยตั้งใจใกล้กับเทอร์มินัลธุรกิจมากขึ้น

การเปรียบเทียบในบทความนี้อิงตาม UI ภายนอก แต่แต่ละสแต็กใช้สองชุด: ภายนอก/ภายใน และโครงสร้างสามารถรองรับทั้งสองอย่างได้โดยแทนที่เฉพาะ CSS เท่านั้น


4. ความแตกต่างในการกำหนดค่า/การสร้างโปรเจ็กต์

Vanilla HTML — ไม่มีการตั้งค่าบิลด์

ทั้ง package.json หรือ tsconfig.json เพียงเปิดไฟล์โดยตรงในเบราว์เซอร์ของคุณหรือให้บริการแบบคงที่ด้วย nginx

# การพัฒนา: เปิดโดยตรงในเบราว์เซอร์
เปิดบัญชี.html

# การผลิต: วางในรูทเอกสาร nginx
nginx -c nginx-external.conf

ไม่มีแพ็คเกจที่ต้องพึ่งพา ไม่มีกระบวนการสร้าง ไม่มี node_modules

Vue — Vite + TypeScript

// package.json (ส่วนหลัก)
{
  "สคริปต์": {
    "dev": "vite",
    "build": "vite build",
    "ดูตัวอย่าง": "ดูตัวอย่าง"
  },
  "การอ้างอิง": {
    "วิว": "^3.4.0",
    "vue-เราเตอร์": "^4.3.0"
  },
  "devDependency": {
    "@vitejs/plugin-vue": "^5.0.0",
    "typescript": "^5.5.0",
    "เยี่ยม": "^5.3.0",
    "vue-tsc": "^2.0.0"
  }
}
``````` ทุบตี
ติดตั้ง npm # สร้าง node_modules
การพัฒนารีโหลดร้อนแรงด้วย npm run dev # http://localhost:5173
รัน npm build # สร้างไฟล์คงที่ใน dist/

React — Vite + TypeScript (เหมือนกับ Vue, Vite)

{
  "สคริปต์": {
    "dev": "vite",
    "build": "tsc && vite build"
  },
  "การอ้างอิง": {
    "ตอบสนอง": "^18.3.0",
    "react-dom": "^18.3.0",
    "react-เราเตอร์-dom": "^6.24.0"
  },
  "devDependency": {
    "@vitejs/plugin-react": "^4.3.0",
    "typescript": "^5.5.0",
    "เยี่ยม": "^5.3.0"
  }
}

ความรู้สึกในการใช้งานเกือบจะเหมือนกับ Vue ข้อแตกต่างเพียงอย่างเดียวคือ @vitejs/plugin-vue กับ @vitejs/plugin-react

Thymeleaf — Maven + Spring Boot

<!-- pom.xml (ส่วนหลัก) -->
<ผู้ปกครอง>
    <groupId>org.springframework.boot</groupId>
    <artifactId>สปริงบูตสตาร์ทเตอร์พาเรนต์</artifactId>
    <เวอร์ชั่น>3.4.5</เวอร์ชั่น>
</ผู้ปกครอง><การพึ่งพา>
    <การพึ่งพา>
        <groupId>org.springframework.boot</groupId>
        <artifactId>สปริงบูตสตาร์ทเว็บ</artifactId>
    </การพึ่งพา>
    <การพึ่งพา>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </การพึ่งพา>
</การพึ่งพา>
mvn clean package # สร้างเป้าหมาย/*.jar
java -jar target/banklink-external-web-thymeleaf.jar # เริ่มต้น

ต้องใช้ JVM jar ที่สร้างโดย target/ มี Tomcat ฝังอยู่ด้วย ดังนั้นจึงไม่จำเป็นต้องใช้เซิร์ฟเวอร์แอปพลิเคชันเพิ่มเติม

เปรียบเทียบประสบการณ์เวลาสร้าง

สแต็คเริ่มต้น install ฯลฯสร้างการผลิตเวลาเริ่มต้น
วานิลลา HTML0 วินาที0 วินาทีทันที
วิว30-60 วินาที (npm install)5-15 วินาทีนาทีเริ่มต้น nginx
ตอบสนอง30-60 วินาที5-15 วินาทีนาทีเริ่มต้น nginx
ไธม์ลีฟ30 ถึง 120 วินาที (DL ขึ้นอยู่กับ Maven)20 ถึง 40 วินาที (mvn package)นาทีเริ่มต้น JVM (5 ถึง 15 วินาที)

5. ใช้งานหน้าจอเดียวกัน (หน้าบัญชี) มี 4 สแต็ค

นี่คือจุดเน้นของบทความนี้ ลองดูโค้ดที่เขียนด้วย 4 วิธีที่แตกต่างกันสำหรับ 5 ส่วนที่เหมือนกัน: “การได้มาของรายการบัญชี, การได้มาของยอดคงเหลือ, การฝาก, การถอน, ประวัติการทำธุรกรรม” ตามลำดับ### เวอร์ชัน HTML วานิลลา

HTML และ JS อยู่ร่วมกันใน accounts.html เดียวกัน (โมดูลใน <script type="module">)

<!-- Accounts.html (ข้อความที่ตัดตอนมาจากรายการบัญชีและส่วนที่ฝาก) -->
<ส่วนชั้น="ส่วนบัตร">
  <h2>รับรายการบัญชี</h2>
  <button id="accounts-list-btn">รับ /api/v1/accounts</button>
  <div id="บัญชีรายการตอบกลับ"></div>
</ส่วน>

<ส่วนชั้น="ส่วนบัตร">
  <h2>ฝากเงิน</h2>
  <label>accountId<input id="deposit-account-id" value="ACC-0001" /></label>
  <label>จำนวน<input id="deposit-amount" type="number" value="10000" /></label>
  <label>Idempotency-Key<input id="deposit-key" value="dep-key-001" /></label>
  <button id="accounts-deposit-btn">โพสต์ /api/v1/accounts/{id}/deposit</button>
  <div id="accounts-deposit-response"></div>
</ส่วน>

<ประเภทสคริปต์ = "โมดูล">
  นำเข้า { bindAction, numberValue, ค่า } จาก "./common-external.js";// รหัสปุ่มการแมปและรหัสปลายทางแสดงการตอบสนอง
  bindAction("accounts-list-btn", "accounts-list-response", () => ({
    วิธีการ: "รับ"
    เส้นทาง: "/api/v1/accounts",
  }));

  bindAction("accounts-deposit-btn", "accounts-deposit-response", () => ({
    วิธีการ: "โพสต์"
    เส้นทาง: `/api/v1/accounts/${value("deposit-account-id")}/deposit`,
    idempotencyKey: value("รหัสเงินฝาก"),
    เนื้อความ: {
      จำนวน: numberValue("เงินฝาก-จำนวน"),
      สกุลเงิน: value("เงินฝาก-สกุลเงิน"),
      การอ้างอิง: value("เงินฝาก-อ้างอิง"),
    },
  }));
</สคริปต์>

คุณสมบัติ:

  • ระบุองค์ประกอบ HTML ด้วย id → อ้างอิงด้วย document.getElementById จาก JS (ทำในฟังก์ชัน bindAction)
  • ค่าอินพุตจะได้รับในแต่ละครั้งโดยใช้ตัวช่วย value("input-id") (อ่าน “ค่า ณ เวลาที่คลิก” แทนที่จะอ่านแบบโต้ตอบ)
  • ผลลัพธ์จะถูกวาดเป็นสตริงที่มี innerHTML

เวอร์ชั่นวิว

<!-- AccountsView.vue (การตั้งค่าสคริปต์ + เทมเพลต) -->
<การตั้งค่าสคริปต์ lang="ts">
นำเข้า { ฉีด อ้างอิง } จาก "vue";
ประเภทการนำเข้า { อ้างอิง } จาก "vue";
นำเข้า { requestApi } จาก "../api/client";const token = inject<Ref<string>>("token"!;
ฟังก์ชั่น fmt (r: ไม่ทราบ) { กลับ JSON.stringify (r, null, 2); }

// รายการบัญชี
const listRes = ref<string>("");
const listStatus = อ้างอิง <หมายเลข | โมฆะ>(โมฆะ);
ฟังก์ชัน async getAccounts() {
  const r = รอ requestApi ({ วิธีการ: "GET", เส้นทาง: "/api/v1/accounts", โทเค็น: token.value });
  listStatus.value = r.สถานะ; listRes.value = fmt(r.body);
}//ฝาก
const depId = อ้างอิง ("ACC-0001"), depAmount = อ้างอิง (10,000)
      depKey = ref("dep-key-001"), depCurrency = ref("JPY"), depRef = ref("TEST-DEP-001");
const depRes = อ้างอิง (""); const depStatus = อ้างอิง <หมายเลข | โมฆะ>(โมฆะ);
ฟังก์ชั่น async ฝาก () {
  const r = รอ requestApi ({
    วิธีการ: "โพสต์"
    เส้นทาง: `/api/v1/accounts/${depId.value}/deposit`,
    โทเค็น: token.value,
    idempotencyKey: depKey.value,
    body: { จำนวน: depAmount.value, สกุลเงิน: depCurrency.value, การอ้างอิง: depRef.value },
  });
  depStatus.value = r.สถานะ; depRes.value = fmt(r.body);
}
</สคริปต์>

<แม่แบบ>
  <ส่วนชั้น="ส่วนบัตร">
    <h2>รับรายการบัญชี</h2>
    <button @click="getAccounts">รับ /api/v1/accounts</button>
    <pre v-if="listStatus !== null">HTTP {{ listStatus }}\n{{ listRes }}</pre>
  </ส่วน><ส่วนชั้น="ส่วนบัตร">
    <h2>ฝากเงิน</h2>
    <label>accountId<input v-model="depId" /></label>
    <label>จำนวน<input v-model.number="depAmount" type="number" /></label>
    <label>Idempotency-Key<input v-model="depKey" /></label>
    <button @click="deposit">โพสต์ /api/v1/accounts/{id}/deposit</button>
    <pre v-if="depStatus !== null">HTTP {{ depStatus }}\n{{ depRes }}</pre>
  </ส่วน>
</แม่แบบ>

คุณสมบัติ:

  • ประกาศ ตัวแปรปฏิกิริยา ด้วย ref() การเชื่อมโยงสองทางด้วย v-model
  • รับโทเค็นที่แชร์จากผู้ปกครองด้วย inject<Ref<string>>("token")
  • แสดงการตอบสนองสาขาแบบมีเงื่อนไขด้วย v-if ฝังตัวแปรด้วย {{ }}

เวอร์ชั่นตอบโต้

// ฟังก์ชั่น AccountsPage ของ App.tsx (ข้อความที่ตัดตอนมา)
ฟังก์ชั่น AccountsPage ({ โทเค็น }: PageProps) {
  const [listResponse, setListResponse] = useState<ApiResult | โมฆะ>(โมฆะ);
  const [depositResponse, setDepositResponse] = useState<ApiResult | โมฆะ>(โมฆะ);const [depositAccountId, setDepositAccountId] = useState("ACC-0001");
  const [depositAmount, setDepositAmount] = useState("10,000");
  const [depositKey, setDepositKey] = useState("dep-key-001");
  const [depositCurrency, setDepositCurrency] = useState("JPY");
  const [depositReference, setDepositReference] = useState("TEST-DEP-001");

  กลับ (
    <div className="หน้าตาราง">
      <Section title="การได้มาของรายการบัญชี">
        <ปุ่ม
          onClick={async () =>
            setListResponse(รอ requestApi ({ วิธีการ: "GET", เส้นทาง: "/api/v1/accounts", โทเค็น }))
          }
        >
          รับ /api/v1/accounts.php
        </ปุ่ม>
        <ResponsePanel การตอบสนอง={listResponse} />
      </ส่วน><ชื่อหัวข้อ = "เงินฝาก">
        <label>รหัสบัญชี
          <input value={depositAccountId} onChange={e => setDepositAccountId(e.target.value)} />
        </ฉลาก>
        <ฉลาก>จำนวน
          <input type="number" value={depositAmount} onChange={e => setDepositAmount(e.target.value)} />
        </ฉลาก>
        <label>Idempotency-Key
          <input value={depositKey} onChange={e => setDepositKey(e.target.value)} />
        </ฉลาก>
        <ปุ่ม
          onClick={async () =>
            setDepositResponse (รอคำขอ API ({
              วิธีการ: "โพสต์"
              เส้นทาง: `/api/v1/accounts/${depositAccountId}/deposit`,
              โทเค็น,
              idempotencyKey: เงินฝากคีย์,
              เนื้อความ: {
                จำนวน: หมายเลข (จำนวนเงินฝาก)
                สกุลเงิน: เงินฝากสกุลเงิน,
                อ้างอิง: อ้างอิงเงินฝาก,
              },
            }))
          }
        >
          POST /api/v1/accounts/{"{id}"}/deposit
        </ปุ่ม>
        <ResponsePanel การตอบสนอง={depositResponse} />
      </ส่วน>
    </div>
  );
}
````**คุณสมบัติ**:
- ประกาศสถานะด้วย `useState` สำหรับแต่ละช่องอินพุตและแต่ละคำตอบ (**ประกาศมากกว่า `ref` ของ Vue**)
- คุณต้องเขียนตัวจัดการเหตุการณ์สำหรับ `onChange={e => setX(e.target.value)}` ทุกครั้ง (ไม่มีไวยากรณ์น้ำตาลเช่น `v-model` ใน Vue)
- คุณสามารถเขียนตัวจัดการ async สำหรับปุ่มได้โดยตรงภายใน `JSX`

### เวอร์ชั่นไทม์ลีฟ

โครงสร้าง 3 ชั้น: เทมเพลต + ตัวควบคุม + บริการ + วัตถุแบบฟอร์ม

```html
<!-- Accounts.html (ข้อความที่ตัดตอนมาจากรายการบัญชีและส่วนที่ฝาก) -->
<ส่วนชั้น="ส่วนบัตร">
  <h2>การได้มาของรายการบัญชี (แบบฟอร์มฝั่งเซิร์ฟเวอร์)</h2>
  <form id="accounts-form" th:action="@{/api/v1/accounts}" th:object="${accountsForm}" method="โพสต์">
    <input type="hidden" th:field="*{authorization}" />
    <button type="submit" class="action-button">โพสต์ /api/v1/accounts</button>
  </แบบฟอร์ม>
  <div th:if="${accountsForm != null และ accountForm.apiResponse != null}" class="response-box">
    <p><strong>สถานะ:</strong> <span th:text="${accountsForm.apiResponse.statusCode}">0</span></p>
    <pre th:text="${accountsForm.apiResponse.body}"></pre>
  </div>
</ส่วน><ส่วนชั้น="ส่วนบัตร">
  <h2>ฝากเงิน</h2>
  <form th:action="@{/api/v1/accounts/deposit}" th:object="${accountsForm}" method="โพสต์">
    <input type="hidden" th:field="*{authorization}" />
    <label>accountId<input th:field="*{depositAccountId}" /></label>
    <label>จำนวน<input th:field="*{depositAmount}" type="number" /></label>
    <label>Idempotency-Key<input th:field="*{depositIdempotencyKey}" /></label>
    <button type="submit" class="action-button">โพสต์ /api/v1/accounts/deposit</button>
  </แบบฟอร์ม>
</ส่วน>
// AccountsController.java
@คอนโทรลเลอร์
@RequiredArgsConstructor
AccountsController ระดับสาธารณะ {
    บัญชีส่วนตัวขั้นสุดท้ายบัญชีบริการบริการ;@PostMapping("/api/v1/accounts")
    รายการสตริงสาธารณะบัญชี (
            @ModelAttribute("accountsForm") แบบฟอร์มบัญชี
            รุ่น รุ่น) {
        ApplyToken(form.getAuthorization());
        form.setApiResponse(accountsService.listAccounts());
        model.addAttribute("รูปแบบบัญชี", แบบฟอร์ม);
        ส่งคืน "บัญชี"; // แสดงผล account.html อีกครั้ง
    }@PostMapping("/api/v1/accounts/deposit")
    ฝากสตริงสาธารณะ(
            @ModelAttribute("accountsForm") แบบฟอร์มบัญชี
            รุ่น รุ่น) {
        ApplyToken(form.getAuthorization());
        แผนที่ <String, Object> body = Map.of(
            "จำนวน", form.getDepositAmount(),
            "สกุลเงิน", form.getDepositCurrency(),
            "ข้อมูลอ้างอิง", form.getDepositReference()
        );
        form.setApiResponse(accountsService.deposit(
            form.getDepositAccountId(), เนื้อความ, form.getDepositIdempotencyKey()));
        model.addAttribute("รูปแบบบัญชี", แบบฟอร์ม);
        ส่งคืน "บัญชี";
    }
}
```````จาวา
// AccountsForm.java (ข้อความที่ตัดตอนมา)
@ข้อมูล
แบบฟอร์มบัญชีคลาสสาธารณะ {
    การอนุญาตสตริงส่วนตัว
    สตริงส่วนตัวDepositAccountId;
    จำนวนเงินฝากส่วนตัวจำนวนเต็ม;
    เงินฝากสตริงส่วนตัวสกุลเงิน;
    การอ้างอิงการฝากสตริงส่วนตัว;
    สตริงส่วนตัวฝากIdempotencyKey;
    apiResponse ภายนอกส่วนตัว apiResponse;
    // ... (ช่องอื่นๆ)
}

คุณสมบัติ:

  • โฟลว์เว็บแบบดั้งเดิมของ HTTP POST → การประมวลผลของเซิร์ฟเวอร์ → การวาดหน้าใหม่ สำหรับการดำเนินการแต่ละครั้ง
  • การเชื่อมโยงสองทางระหว่างฟิลด์ Form Object และอินพุตโดยใช้ th:field="*{depositAmount}" (Spring ส่งค่าโดยอัตโนมัติ)
  • ผลลัพธ์ยังแสดงผลเป็น HTML บนฝั่งเซิร์ฟเวอร์ → JS บนฝั่งไคลเอ็นต์นั้นมีน้อยมาก

เปรียบเทียบจำนวนบรรทัดโค้ด (ทั้งหน้าบัญชี)

บรรทัดของรหัสภาษา/ไฟล์
วานิลลา HTMLประมาณ 130 บรรทัดHTML + JS (1 ไฟล์)
วิวประมาณ 130 บรรทัดTypeScript + เทมเพลต (1 ไฟล์)
ตอบสนองประมาณ 350 บรรทัดTypeScript JSX (1 ฟังก์ชัน)
ไธม์ลีฟประมาณ 250 บรรทัดHTML + Java (กระจายมากกว่า 5 ไฟล์)

6. ความแตกต่างระหว่างไคลเอนต์ API

พวกเขาทั้งหมดเข้าถึง API แบ็กเอนด์เดียวกัน (/api/v1/accounts ฯลฯ) แต่การใช้งานไคลเอนต์แตกต่างกันเล็กน้อย

วานิลลา HTML / Vue / React — JavaScript fetch

// Vue เวอร์ชัน client.ts
ส่งออกอินเทอร์เฟซ ApiResult {
  สถานะ: หมายเลข;
  ร่างกาย: ไม่ทราบ;
}

ส่งออกฟังก์ชัน async requestApi (ตัวเลือก: {
  วิธีการ: สตริง;
  เส้นทาง: สตริง;
  โทเค็น: สตริง;
  idempotencyKey?: สตริง;
  ร่างกาย?: ไม่ทราบ;
}): สัญญา<ApiResult> {
  ส่วนหัว const: บันทึก<string, string> = {
    "Content-Type": "application/json",
    การอนุญาต: `Bearer ${opts.token}`,
  };
  ถ้า (opts.idempotencyKey) {
    ส่วนหัว ["Idempotency-Key"] = opts.idempotencyKey;
  }ลอง {
    const res = รอการดึงข้อมูล (opts.path, {
      วิธีการ: opts.method,
      ส่วนหัว,
      เนื้อความ: opts.body !== ไม่ได้กำหนด ? JSON.stringify(opts.body) : ไม่ได้กำหนด,
    });
    const text = รอ res.text();
    const responseBody = ข้อความ ? JSON.parse(ข้อความ) : null;
    กลับ { สถานะ: res.status ร่างกาย: responseBody };
  } จับ (e) {
    กลับ { สถานะ: 0, เนื้อความ: สตริง (e) };
  }
}

เกือบจะเหมือนกัน ใน Vanilla/Vue/React ข้อแตกต่างเพียงอย่างเดียวคือการมีหรือไม่มีคำอธิบายประกอบประเภท (วานิลลาคือ .js, Vue / React คือ .ts)

Thymeleaf — รองเท้าบูทสปริง RestClient

// AccountsService.java
@บริการ
@RequiredArgsConstructor
AccountsService ระดับสาธารณะ {
    RestClient สุดท้ายส่วนตัว restClient;
    โทเค็นสตริงส่วนตัวรายการบัญชี ExternalApiResponse สาธารณะ () {
        ResponseEntity<String> การตอบสนอง = restClient
            .get()
            .uri("/api/v1/accounts")
            .header("การอนุญาต", "ผู้ถือ" + โทเค็น)
            .ดึง()
            .toEntity(String.คลาส);

        ส่งคืน ExternalApiResponse ใหม่ (
            response.getStatusCode().value()
            จริง,
            การตอบสนองgetBody()
        );
    }
}

สัญกรณ์ตัวสร้างใน Java RestClient (Spring 6.1+) เมื่อเทียบกับ fetch ประเภทจะเข้มงวดกว่าและการทำให้ IDE สมบูรณ์มีประสิทธิภาพมากกว่า

ความเหมือนและความแตกต่าง

มุมมองวานิลลา/วิว/รีแอคไธม์ลีฟ
ภาษาจาวาสคริปต์ / TypeScriptชวา
ไคลเอ็นต์ HTTPfetch (มาตรฐาน)RestClient (สปริง สแตนดาร์ด)
การจัดการข้อผิดพลาดลอง/จับ + รหัสสถานะลอง/จับ + HttpClientErrorException
พิมพ์ความปลอดภัยพิมพ์คำอธิบายประกอบใน TypeScriptพิมพ์ ความปลอดภัย (ค่าเริ่มต้น) ใน Java
ตำแหน่งที่เกิดเครือข่ายเบราว์เซอร์ → APIเซิร์ฟเวอร์ → API
  • สำหรับระบบ SPA (Vanilla/Vue/React) เข้าถึง API ได้โดยตรงจากเบราว์เซอร์ → จำเป็นต้องมีการตั้งค่า CORS / ข้อมูลการตรวจสอบสิทธิ์ API ถูกส่งไปยังไคลเอนต์
  • Thymeleaf อนุญาตให้เซิร์ฟเวอร์เข้าถึง API → ไม่จำเป็นต้องใช้ CORS / ข้อมูลการรับรองความถูกต้องถูกเก็บไว้ภายในเซิร์ฟเวอร์

7. ความแตกต่างระหว่างการจัดการสถานะและการผูกข้อมูล

ความเป็นเอกเทศของแต่ละสแต็กจะถูกกำหนดโดยวิธีที่จะรักษา “ค่าอินพุตบนหน้าจอ การตอบสนองที่ได้รับ และโทเค็น”

Vanilla HTML — DOM เป็นแหล่งที่มาของสถานะ

// ค่าอินพุต: ดึงข้อมูลจาก DOM ได้ตลอดเวลา
const accountId = document.getElementById("balance-account-id").value;

// แสดงการตอบสนอง: เขียนสตริงโดยตรงใน innerHTML
document.getElementById("สมดุลตอบสนอง").innerHTML = formatResponse(ผลลัพธ์);

// โทเค็น: บันทึกลงใน localStorage
localStorage.setItem("banklink_token", โทเค็น);

“รัฐ” ไม่มีแนวคิด อ่านค่า DOM หรือ localStorage เสมอ มันง่าย แต่เมื่อแอปเติบโตขึ้น การซิงโครไนซ์ก็จะกลายเป็นเรื่องยาก

Vue — การประกาศเชิงโต้ตอบด้วย ref

const balanceId = อ้างอิง ("ACC-0001"); // ตัวแปรปฏิกิริยาพร้อมค่าสตริง
const balanceRes = อ้างอิง (""); // เขียน {{ balanceRes }} ในเทมเพลตเพื่ออัปเดตอัตโนมัติ// หากคุณเขียน <input v-model="balanceId" /> ที่ด้านเทมเพลต
// การป้อนข้อมูลของผู้ใช้จะสะท้อนให้เห็นโดยอัตโนมัติใน balanceId.value (การผูกสองทาง)

โมเดล “การประกาศตัวแปรรีแอกทีฟ → เทมเพลตจะตามมาโดยอัตโนมัติ” ผู้เขียนไม่จำเป็นต้องตระหนักถึงการซิงโครไนซ์สถานะ

React — ประกาศสถานะ hooks ด้วย useState

const [balanceId, setBalanceId] = useState("ACC-0001");
const [balanceRes, setBalanceRes] = useState<ApiResult | โมฆะ>(โมฆะ);

// ด้านเทมเพลต: ส่งค่าและตัวจัดการแยกกัน
<input value={balanceId} onChange={e => setBalanceId(e.target.value)} />

“การประกาศคู่ตัวตั้งค่าตัวแปร → อัปเดตผ่านตัวตั้งค่า → แสดงผลซ้ำ” โมเดล ไม่มีไวยากรณ์เหมือน v-model ใน Vue ดังนั้นคุณต้องเขียน onChange สำหรับแต่ละฟิลด์อินพุต ซึ่งจะเพิ่มจำนวนโค้ด

Thymeleaf — สถานะรวมบนฝั่งเซิร์ฟเวอร์ด้วย Form Object

@ข้อมูล
แบบฟอร์มบัญชีคลาสสาธารณะ {
    การอนุญาตสตริงส่วนตัว
    สตริงส่วนตัว balanceAccountId;
    apiResponse ภายนอกส่วนตัว apiResponse;
    // ...สาขาอื่นๆ
}
<input th:field="*{balanceAccountId}" />
````**Form Object บนฝั่งเซิร์ฟเวอร์คือ "สำเนาต้นฉบับของสถานะ"** ทุกครั้งที่มี POST ค่าอินพุตจะถูกบรรจุลงใน Form Object และผู้ควบคุมจะประมวลผล → ผลลัพธ์จะถูกบรรจุลงใน Form Object และวาดใหม่ โมเดลเว็บแบบคลาสสิกที่ไม่มีสถานะในฝั่งไคลเอ็นต์

### ไม่เหมาะกับแต่ละฉาก

| ลักษณะของแอป | สแต็คโดยตรง |
|---|---|
| จอเล็ก 1 จอ แทบไม่มีสภาพ | **วานิลลา HTML** |
| การใช้งาน Reactive UI อย่างหนัก หลายรูปแบบ | **วิว** |
| การใช้ส่วนประกอบซ้ำสูง การมุ่งเน้นระบบนิเวศ | **โต้ตอบ** |
| ฉันต้องการเก็บสถานะไว้ที่ฝั่งเซิร์ฟเวอร์ (ระบบธุรกิจ) | **ไธม์ลีฟ** |

---

## 8. ความแตกต่างในการประมวลผลแบบฟอร์ม

วิธีใช้แบบฟอร์มการฝากเงิน (5 ช่อง: accountId / amount / currency / Reference / Idempotency-Key)

### วานิลลา HTML

```จาวาสคริปต์
// ระบุ ID ให้กับแต่ละอินพุต
<input id="deposit-account-id" value="ACC-0001" />

// อ่านค่าทั้งหมดพร้อมกันเมื่อคลิก
ร่างกาย const = {
  accountId: document.getElementById("deposit-account-id").value,
  จำนวน: Number(document.getElementById("deposit-amount").value)
  // ...
};

คุณสามารถลดการซ้ำซ้อนนี้ได้โดยการเขียนฟังก์ชันตัวช่วย value() แต่แนวคิดของ “รูปแบบ” นั้นมีอยู่ในฝั่ง HTML/JS เท่านั้น

Vue — v-model รับเพียง 5 บรรทัดเท่านั้น```วิว


แปลงเป็นประเภทตัวเลขอัตโนมัติด้วย `v-model.number` **จำนวนการเขียนน้อยที่สุด**

### React — useState + onChange สำหรับแต่ละฟิลด์

``` ซ
const [depositAccountId, setDepositAccountId] = useState("ACC-0001");
const [depositAmount, setDepositAmount] = useState("10,000");
const [depositCurrency, setDepositCurrency] = useState("JPY");
// ... สำหรับแต่ละฟิลด์

<input value={depositAccountId} onChange={e => setDepositAccountId(e.target.value)} />
<input type="number" value={depositAmount} onChange={e => setDepositAmount(e.target.value)} />
// ...

5 ช่อง = useState 5 ครั้ง + onChange 5 ครั้ง สิ่งนี้สามารถละเว้นได้ด้วยไลบรารีภายนอก เช่น react-hook-form แต่ในบทความนี้ เราจะใช้ “plain React” เพื่อเปรียบเทียบ### Thymeleaf — ผสมผสานอัตโนมัติกับ Form Object ด้วย th:field

<form th:action="@{/api/v1/accounts/deposit}" th:object="${accountsForm}" method="โพสต์">
  <input type="hidden" th:field="*{authorization}" />
  <label>accountId<input th:field="*{depositAccountId}" /></label>
  <label>จำนวน<input th:field="*{depositAmount}" type="number" /></label>
  <label>Idempotency-Key<input th:field="*{depositIdempotencyKey}" /></label>
  <button type="submit">ส่ง</button>
</แบบฟอร์ม>

th:field ตั้งค่า “แอตทริบิวต์ชื่อ คุณลักษณะ id และแอตทริบิวต์ค่า” โดยอัตโนมัติด้วยแอตทริบิวต์เดียว และเชื่อมต่อ Form Object และฟิลด์บนฝั่งเซิร์ฟเวอร์ ประสบการณ์การเขียนคล้ายกับ v-model ของ Vue


9. ความแตกต่างในการแสดงข้อผิดพลาดในการโหลด

แสดงรูปแบบเมื่อ API ล้มเหลว

วานิลลา HTML```จาวาสคริปต์

ฟังก์ชั่น renderResponse (targetId, การตอบสนอง) { เป้าหมาย const = document.getElementById(targetId); const badgeClass = response.status >= 200 && response.status < 300 ? “ตกลง” : “ผิดพลาด”; target.innerHTML = <div class="response-panel"> <span class="badge ${badgeClass}">HTTP ${response.status}</span> <pre>${escapeHtml(JSON.stringify(response.body, null, 2))}</pre> </div> ; }


**ต้องใช้การหลบหนีด้วยตนเอง** `escapeHtml` เตรียมฟังก์ชั่นของคุณเองหรือใช้ `textContent`

### Vue / React - การหลบหนีอัตโนมัติ + การเรนเดอร์แบบมีเงื่อนไข

```วิว
<!-- วิว -->
<div v-if="สถานะ !== null" class="response-panel">
  <span :class="status < 400 ? 'badge-ok' : 'badge-err'">HTTP {{ สถานะ }}</span>
  <pre>{{ ตอบกลับ }}</pre>
</div>
``````` ซ
//โต้ตอบ
{ตอบกลับ && (
  <div className="แผงตอบกลับ">
    <span className={response.status < 400 ? "badge-ok" : "badge-err"}>HTTP {response.status}</span>
    <pre>{JSON.stringify(response.body, null, 2)}</pre>
  </div>
)}

หากคุณฝังค่าด้วย {{ }} หรือ {} ค่านั้นจะถูก ใช้ Escape อัตโนมัติ ไม่จำเป็นต้องตระหนักถึงมาตรการรับมือ XSS

Thymeleaf — หลบหนีอัตโนมัติด้วย th:text

<div th:if="${accountsForm.apiResponse != null}" class="response-box">
  <p>สถานะ: <span th:text="${accountsForm.apiResponse.statusCode}"></span></p>
  <pre th:text="${accountsForm.apiResponse.body}"></pre>
</div>

th:text จะถูก Escape โดยอัตโนมัติเช่นกัน th:utext ทำให้เกิด unescape (ความเสี่ยง XSS)


10. ความแตกต่างในการกำหนดค่าการเริ่มต้น/ปรับใช้

Vanilla HTML — การจัดส่งแบบคงที่ด้วย nginx

# nginx-external.conf
เซิร์ฟเวอร์ {
  ฟัง 8080;
  รูท /app/banklink-external-web-vanilla-html;
  ดัชนี index.html;

  ตำแหน่ง /api/ {
    proxy_pass http://banklink-api:8080; # API เฮลิบาโปร
  }
}
```````นักเทียบท่าไฟล์
จาก nginx:alpine
คัดลอก banklink-web-vanilla-html /app/
คัดลอก nginx-external.conf /etc/nginx/conf.d/default.conf

การกำหนดค่าขั้นต่ำ ส่ง HTML/JS/CSS ตามที่เป็นอยู่

Vue / React — Vite build → การกระจาย nginx

#ขั้นตอนการสร้าง
จากโหนด: ตัวสร้าง AS 20-อัลไพน์
WORKDIR /แอป
คัดลอกแพ็คเกจ*.json ./
วิ่ง npm ci
คัดลอก
RUN npm run build # สร้าง dist/

#เวทีเสิร์ฟ
จาก nginx:alpine
คัดลอก --from=builder /app/dist /usr/share/nginx/html
คัดลอก nginx-external.conf /etc/nginx/conf.d/default.conf

ปรับใช้บิวด์อาร์ติแฟกต์กับ nginx ข้อแตกต่างเพียงอย่างเดียวจาก Vanilla คือการเพิ่มขั้นตอนการสร้าง

Thymeleaf - โถปฏิบัติการ Spring Boot

จาก eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /แอป
คัดลอก pom.xml
คัดลอก src ./src
เรียกใช้แพ็คเกจ mvn ใหม่ -DskipTests

จากคราส-เทมูริน:21-jre-อัลไพน์
คัดลอก --from=builder /app/target/*.jar app.jar
จุดเข้า ["java", "-jar", "/app.jar"]

จำเป็นต้องมี JVM ขนาดคอนเทนเนอร์คือหลายสิบ MB + JVM (ประมาณ 200 MB)

เปรียบเทียบขนาดภาพ| | ขนาดอิมเมจคอนเทนเนอร์ | เวลาเริ่มต้น |

|---|---|---| | วานิลลา HTML (nginx) | ~25 เมกะไบต์ | < 1 วินาที | | Vue (การส่ง nginx) | ~30 เมกะไบต์ | < 1 วินาที | | ตอบสนอง (การส่ง nginx) | ~30 เมกะไบต์ | < 1 วินาที | | Thymeleaf (สปริงบูท + JVM) | ~200 เมกะไบต์ | 5-15 วินาที |

หากคุณให้ความสำคัญกับการลดน้ำหนัก ระบบการกระจาย nginx (สามระบบก่อนหน้านี้) จะเป็นประโยชน์ แม้ว่า Spring Boot จะมีน้ำหนักมาก แต่ก็มีจุดแข็งคือสามารถจัดการตรรกะฝั่งเซิร์ฟเวอร์ พร็อกซี API การรวมการรับรองความถูกต้อง ฯลฯ ในกระบวนการเดียวกันได้


11. ตารางเปรียบเทียบแนวนอน| มุมมอง | วานิลลา HTML | วิว 3 | ตอบสนอง | ไธม์ลีฟ |

|---|---|---|---|---| | ภาษา | เจเอส | ทีเอส | TS (TSX) | ชวา | | สร้าง | ไม่จำเป็น | เยี่ยม | เยี่ยม | มาเวน | | ค่าเล่าเรียน | ต่ำ | ปานกลาง | ปานกลางถึงสูง | ปานกลาง (ต่ำหากคุณได้เรียนรู้ Java แล้ว) | | จำนวนบรรทัดรหัส (หน้าบัญชี) | ประมาณ 130 บรรทัด | ประมาณ 130 บรรทัด | ประมาณ 350 เส้น | ประมาณ 250 บรรทัด (กระจาย) | | รูปแบบการบริหารจัดการของรัฐ | โดม | การอ้างอิงเชิงโต้ตอบ | ใช้สถานะ | แบบฟอร์มวัตถุ (เซิร์ฟเวอร์) | | แบบฟอร์มเข้าร่วม | คู่มือ | v-model | onChange บุคคล | th:field | | XSS หลบหนีอัตโนมัติ | คู่มือ | อัตโนมัติ | อัตโนมัติ | อัตโนมัติ | | ตำแหน่งการเรียก API | เบราว์เซอร์ | เบราว์เซอร์ | เบราว์เซอร์ | เซิร์ฟเวอร์ | | จะค้นหาข้อมูลรับรองของคุณได้ที่ไหน | พื้นที่เก็บข้อมูลท้องถิ่น | พื้นที่เก็บข้อมูลท้องถิ่น | พื้นที่เก็บข้อมูลท้องถิ่น | เซสชันเซิร์ฟเวอร์ | | ต้องใช้ CORS | ใช่ | ใช่ | ใช่ | ไม่ | | จำนวนแพ็คเกจที่ต้องพึ่งพา | 0 | ~10 | ~10 | ~20 (มาเวน) | | ภาพคอนเทนเนอร์ | ~25 เมกะไบต์ | ~30 เมกะไบต์ | ~30 เมกะไบต์ | ~200 เมกะไบต์ | | เวลาเริ่มต้น | ทันที | ทันที | ทันที | 5-15 วินาที (JVM) | | ** UI ไดนามิก ** | อ่อนแอ | แข็งแกร่ง | แข็งแกร่ง | อ่อนแอ (ต้องโหลดซ้ำ) | | ระบบนิเวศ | ไม่มี | ปานกลาง | ใหญ่โต | ระบบนิเวศฤดูใบไม้ผลิ |


12. วิธีเลือก — จุดแข็งและจุดอ่อนของ 4 สแต็คและฐานการตัดสินใจ

วานิลลา HTMLจุดแข็ง:

  • การพึ่งพาเป็นศูนย์, การสร้างเป็นศูนย์, ค่าใช้จ่ายในการเรียนรู้เป็นศูนย์ (พื้นฐาน HTML/JS เท่านั้น)
  • ค่าจัดส่งขั้นต่ำ/อิมเมจคอนเทนเนอร์ขั้นต่ำ
  • ดีที่สุดสำหรับ “การสร้างการสาธิตการทำงานใน 30 นาที”

จุดอ่อน:

  • การจัดการสถานะพังเมื่อแอปเติบโตขึ้น (ข้อจำกัดของการจัดการ DOM โดยตรง)
  • ไม่มีความปลอดภัยประเภท TypeScript
  • ฉันไม่ชอบ UI แบบโต้ตอบ

เลือกเวลา:

  • เครื่องมือยืนยันการทำงานของ API, แบบฟอร์มการตรวจสอบภายใน, แดชบอร์ดที่เรียบง่าย
  • “ฉันไม่ต้องการเฟรมเวิร์ก ฉันแค่ต้องการหน้าจอ”
  • ต้นแบบจะถูกแทนที่ด้วย SPA ในภายหลัง

วิว

จุดแข็ง:

  • น้ำตาลไวยากรณ์เช่น v-model โค้ดน้อยกว่า React
  • ไวยากรณ์เทมเพลตคล้ายกับ HTML และอ่านง่ายแม้สำหรับผู้เริ่มต้น
  • ส่วนประกอบไฟล์เดี่ยว (.vue) ช่วยให้ตรรกะ/เทมเพลต/สไตล์พอดีกับไฟล์เดียว

จุดอ่อน:

  • ระบบนิเวศเล็กกว่าเมื่อเทียบกับ React
  • ด้อยกว่า React ทั้งในด้านจำนวนโครงการรับสมัครงานและทรัพยากรบุคคล

เลือกเวลา:

  • Business SPA มีหลายรูปแบบ/อินพุต UI
  • “หากคุณมีปัญหาในการเลือกเฟรมเวิร์ก ลองใช้ Vue”
  • โครงการที่ตลาดทรัพยากรบุคคลของวิศวกรเว็บส่วนใหญ่อยู่ในญี่ปุ่น

โต้ตอบ

จุดแข็ง:

  • ระบบนิเวศขนาดใหญ่ (ไลบรารี UI, การจัดการสถานะ, การทดสอบ, อุปกรณ์เคลื่อนที่)
  • เข้ากันได้ดีกับ TypeScript (คำจำกัดความประเภทที่หลากหลาย)
  • ตำแหน่งงานว่างส่วนใหญ่ในตลาดงาน

จุดอ่อน:

  • มีแนวโน้มที่จะต้องใช้บรรทัดมากกว่า Vue ในการเขียนฟังก์ชันเดียวกัน
  • ค่าใช้จ่ายในการเรียนรู้วิธีใช้ useState, useEffect, useMemo, useCallback
  • ต้องมีความเข้าใจเกี่ยวกับการเรนเดอร์องค์ประกอบของฟังก์ชันใหม่

เลือกเวลา:

  • กำลังมองหาการเชื่อมต่อกับ SPA ขนาดใหญ่, Next.js / React Native
  • องค์กรที่เน้นการจ้างงานวิศวกร
  • ฉันต้องการนำไลบรารี UI มาใช้ซ้ำ (MUI / Mantine / shadcn ฯลฯ )### ไธม์ลีฟ

จุดแข็ง:

  • JS ฝั่งไคลเอ็นต์สามารถย่อให้เล็กสุดได้ (ระบบธุรกิจ)
  • ข้อมูลการตรวจสอบความถูกต้อง/โทเค็น API ถูก ปิดภายในเซิร์ฟเวอร์ (มีประโยชน์ในสถานการณ์ที่มีข้อกำหนดด้านความปลอดภัยที่เข้มงวด)
  • ใช้ประโยชน์จากระบบนิเวศของ Spring Security / Spring Boot อย่างเต็มที่
  • วิศวกร Java สามารถสร้าง UI ของเว็บโดยใช้ทักษะที่มีอยู่

จุดอ่อน:

  • ต้องใช้เซิร์ฟเวอร์ไปกลับสำหรับการเปลี่ยนหน้าแต่ละครั้ง (เทียบกับ SPA)
  • ไม่เหมาะสำหรับ UI แบบโต้ตอบ
  • อิมเมจคอนเทนเนอร์ JVM มีน้ำหนักมาก

เลือกเวลา:

  • หน้าจอระบบธุรกิจ/การจัดการ (สำหรับใช้ภายใน)
  • ฉันไม่ต้องการเปิดเผยคีย์ API ให้กับลูกค้าเนื่องจากข้อกำหนดด้านความปลอดภัย
  • องค์กรที่มีวิศวกร Java จำนวนมากได้นำ Spring Boot มาใช้แล้ว
  • แทนที่จะเป็น UI ที่สมบูรณ์ “ส่งแบบฟอร์มและแสดงผลลัพธ์” ก็เพียงพอแล้ว

กระแสการตัดสินใจ

ไตรมาสที่ 1 ปฏิกิริยา UI จำเป็นหรือไม่?
   ├─ ไม่ (ส่งแบบฟอร์มและแสดงผลก็เพียงพอแล้ว)
   │ ├─ ฉันต้องการรวมทางฝั่งเซิร์ฟเวอร์ → Thymeleaf
   │ └─ ฉันต้องการทำให้มันมีน้ำหนักเบา → วานิลลา HTML
   └─ ใช่ (ต้องใช้ UI แบบโต้ตอบ)
       ├─ มุ่งเน้นไปที่ระบบนิเวศและการสรรหาบุคลากร → ตอบสนอง
       └─ ต้องการลดจำนวนโค้ด/ต้นทุนการเรียนรู้ต่ำ → Vue

13. สรุป/การเรียนรู้- 4 สแต็กไม่ใช่ “ถูกต้องและไม่ถูกต้อง” แต่ ข้อดีและข้อเสียจะเปลี่ยนแปลงขึ้นอยู่กับข้อกำหนดของโครงการ ความแตกต่างจะปรากฏชัดเจนเมื่อวางข้อมูลจำเพาะเดียวกันไว้เคียงข้างกัน

  • ถ้าคุณดูแค่จำนวนบรรทัดของโค้ด Vanilla/Vue มีน้อยกว่า และ React มีมากกว่า อย่างไรก็ตาม React มากกว่าชดเชยในตลาดการสรรหาบุคลากรและระบบนิเวศ
  • Thymeleaf ยังไม่ล้าสมัย — มักจะตรงตามข้อกำหนดด้านความปลอดภัยและการปฏิบัติงานของระบบธุรกิจ
  • “เลือก Framework โดยทำงานย้อนหลังจากข้อกำหนด” เป็นข้อสรุป หากคุณเลือกบางอย่างตามกระแส คุณจะพบกับช่วงเวลาที่ยากลำบากในอีกไม่กี่ปีต่อมา
  • หลังจากเรียกใช้การใช้งาน 4 แบบพร้อมกัน ฉันพบว่าความแตกต่างในเครื่องมือสร้าง (Vite กับ Maven เทียบกับไม่มี) สร้างความแตกต่างอย่างมากในประสบการณ์ในวันแรกของการเปิดตัวโครงการ ในขั้นตอนต้นแบบ ระบบ Vanilla/Vite มีความเร็วอย่างท่วมท้น

เราหวังว่าการเปรียบเทียบนี้จะเป็นประโยชน์เมื่อทำการเลือก

ส่งข้อความได้ตามสบาย

กรุณาส่งข้อความ หากมีคำปรึกษาด้านเทคนิค ความคิดเห็น หรือคำถาม