# สร้างแอปธนาคาร ตอนที่ 1: เทมเพลต HTML และเส้นทางในเว็บแอป
เมื่อคอมพิวเตอร์นำทางของ Apollo 11 นำทางไปยังดวงจันทร์ในปี 1969 มันต้องสลับระหว่างโปรแกรมต่าง ๆ โดยไม่ต้องรีสตาร์ทระบบทั้งหมด แอปพลิเคชันเว็บสมัยใหม่ทำงานในลักษณะเดียวกัน – เปลี่ยนสิ่งที่คุณเห็นโดยไม่ต้องโหลดทุกอย่างใหม่ตั้งแต่ต้น ซึ่งสร้างประสบการณ์ที่ราบรื่นและตอบสนองที่ผู้ใช้คาดหวังในปัจจุบัน
ต่างจากเว็บไซต์แบบดั้งเดิมที่โหลดหน้าเว็บทั้งหมดใหม่ทุกครั้งที่มีการโต้ตอบ แอปเว็บสมัยใหม่จะอัปเดตเฉพาะส่วนที่ต้องการเปลี่ยนแปลง วิธีการนี้คล้ายกับการที่ศูนย์ควบคุมภารกิจสลับระหว่างหน้าจอแสดงผลต่าง ๆ ในขณะที่ยังคงการสื่อสารอย่างต่อเนื่อง สร้างประสบการณ์ที่ราบรื่นที่เราเคยชิน
นี่คือสิ่งที่ทำให้ความแตกต่างชัดเจน:
| แอปหลายหน้าแบบดั้งเดิม | แอปหน้าเดียวสมัยใหม่ |
|----------------------------|-------------------------|
| **การนำทาง** | โหลดหน้าเว็บใหม่ทั้งหมดสำหรับแต่ละหน้าจอ | สลับเนื้อหาได้ทันที |
| **ประสิทธิภาพ** | ช้ากว่าเนื่องจากต้องดาวน์โหลด HTML ทั้งหมด | เร็วกว่าเพราะอัปเดตเฉพาะส่วนที่จำเป็น |
| **ประสบการณ์ผู้ใช้** | หน้าเว็บกระพริบ | การเปลี่ยนแปลงที่ราบรื่นเหมือนแอป |
| **การแชร์ข้อมูล** | ยากระหว่างหน้า | การจัดการสถานะง่าย |
| **การพัฒนา** | ต้องดูแลไฟล์ HTML หลายไฟล์ | ใช้ HTML เดียวกับเทมเพลตแบบไดนามิก |
**ทำความเข้าใจวิวัฒนาการ:**
- **แอปแบบดั้งเดิม** ต้องการคำขอเซิร์ฟเวอร์สำหรับทุกการกระทำการนำทาง
- **SPA สมัยใหม่** โหลดครั้งเดียวและอัปเดตเนื้อหาแบบไดนามิกโดยใช้ JavaScript
- **ความคาดหวังของผู้ใช้** ตอนนี้ชอบการโต้ตอบที่ทันทีและราบรื่น
- **ประโยชน์ด้านประสิทธิภาพ** รวมถึงการลดแบนด์วิดท์และการตอบสนองที่เร็วขึ้น
ในบทเรียนนี้ เราจะสร้างแอปธนาคารที่มีหลายหน้าจอที่ไหลลื่นไปด้วยกันอย่างราบรื่น เช่นเดียวกับที่นักวิทยาศาสตร์ใช้เครื่องมือแบบโมดูลาร์ที่สามารถปรับเปลี่ยนได้สำหรับการทดลองต่าง ๆ เราจะใช้เทมเพลต HTML เป็นส่วนประกอบที่สามารถนำมาใช้ซ้ำได้ตามต้องการ
คุณจะได้ทำงานกับเทมเพลต HTML (พิมพ์เขียวที่นำมาใช้ซ้ำสำหรับหน้าจอต่าง ๆ), การกำหนดเส้นทางด้วย JavaScript (ระบบที่สลับระหว่างหน้าจอ), และ API ประวัติของเบราว์เซอร์ (ซึ่งทำให้ปุ่มย้อนกลับทำงานตามที่คาดหวัง) สิ่งเหล่านี้เป็นเทคนิคพื้นฐานเดียวกันที่ใช้ในเฟรมเวิร์กอย่าง React, Vue และ Angular
เมื่อจบบทเรียนนี้ คุณจะมีแอปธนาคารที่แสดงหลักการของแอปพลิเคชันหน้าเดียวแบบมืออาชีพ
## แบบทดสอบก่อนเรียน
[แบบทดสอบก่อนเรียน](https://ff-quizzes.netlify.app/web/quiz/41)
### สิ่งที่คุณต้องมี
เราจะต้องมีเซิร์ฟเวอร์เว็บในเครื่องเพื่อทดสอบแอปธนาคารของเรา – ไม่ต้องกังวล มันง่ายกว่าที่คิด! หากคุณยังไม่มีเซิร์ฟเวอร์ในเครื่อง เพียงติดตั้ง [Node.js](https://nodejs.org) และรันคำสั่ง `npx lite-server` จากโฟลเดอร์โปรเจกต์ของคุณ คำสั่งนี้จะช่วยเปิดเซิร์ฟเวอร์ในเครื่องและเปิดแอปของคุณในเบราว์เซอร์โดยอัตโนมัติ
### การเตรียมตัว
บนคอมพิวเตอร์ของคุณ สร้างโฟลเดอร์ชื่อ `bank` พร้อมไฟล์ชื่อ `index.html` ภายใน เราจะเริ่มต้นจาก [boilerplate HTML](https://en.wikipedia.org/wiki/Boilerplate_code):
```html
Bank App
```
**สิ่งที่ boilerplate นี้ให้:**
- **กำหนด** โครงสร้างเอกสาร HTML5 ด้วยการประกาศ DOCTYPE ที่เหมาะสม
- **ตั้งค่า** การเข้ารหัสตัวอักษรเป็น UTF-8 เพื่อรองรับข้อความระหว่างประเทศ
- **เปิดใช้งาน** การออกแบบที่ตอบสนองด้วยแท็ก meta viewport เพื่อความเข้ากันได้กับมือถือ
- **ตั้งค่า** ชื่อที่อธิบายซึ่งปรากฏในแท็บเบราว์เซอร์
- **สร้าง** ส่วน body ที่สะอาดซึ่งเราจะสร้างแอปพลิเคชันของเรา
> 📁 **ตัวอย่างโครงสร้างโปรเจกต์**
>
> **เมื่อจบบทเรียนนี้ โปรเจกต์ของคุณจะมี:**
> ```
> bank/
> ├── index.html
> ├── app.js
> └── style.css
> ```
>
> **หน้าที่ของไฟล์:**
> - **index.html**: มีเทมเพลตทั้งหมดและให้โครงสร้างแอป
> - **app.js**: จัดการการกำหนดเส้นทาง การนำทาง และการจัดการเทมเพลต
> - **Templates**: กำหนด UI สำหรับหน้าจอเข้าสู่ระบบ แดชบอร์ด และหน้าจออื่น ๆ
---
## เทมเพลต HTML
เทมเพลตแก้ปัญหาพื้นฐานในพัฒนาเว็บ เมื่อกูเทนเบิร์กคิดค้นการพิมพ์ตัวอักษรแบบเคลื่อนที่ในปี 1440 เขาตระหนักว่าแทนที่จะแกะสลักหน้าทั้งหมด เขาสามารถสร้างบล็อกตัวอักษรที่นำมาใช้ซ้ำได้และจัดเรียงตามต้องการ เทมเพลต HTML ทำงานบนหลักการเดียวกัน – แทนที่จะสร้างไฟล์ HTML แยกสำหรับแต่ละหน้าจอ คุณกำหนดโครงสร้างที่นำมาใช้ซ้ำได้ซึ่งสามารถแสดงผลเมื่อจำเป็น
คิดว่าเทมเพลตเป็นพิมพ์เขียวสำหรับส่วนต่าง ๆ ของแอปของคุณ เช่นเดียวกับที่สถาปนิกสร้างพิมพ์เขียวครั้งเดียวและใช้มันหลายครั้งแทนที่จะวาดห้องที่เหมือนกันใหม่ เราสร้างเทมเพลตครั้งเดียวและนำมาใช้เมื่อจำเป็น เบราว์เซอร์จะเก็บเทมเพลตเหล่านี้ไว้จนกว่า JavaScript จะเปิดใช้งาน
หากคุณต้องการสร้างหน้าจอหลายหน้าสำหรับหน้าเว็บ วิธีหนึ่งคือการสร้างไฟล์ HTML หนึ่งไฟล์สำหรับทุกหน้าจอที่คุณต้องการแสดง อย่างไรก็ตาม วิธีนี้มาพร้อมกับความไม่สะดวก:
- คุณต้องโหลด HTML ทั้งหมดใหม่เมื่อสลับหน้าจอ ซึ่งอาจช้า
- การแชร์ข้อมูลระหว่างหน้าจอต่าง ๆ เป็นเรื่องยาก
อีกวิธีหนึ่งคือการมีไฟล์ HTML เพียงไฟล์เดียว และกำหนด [HTML templates](https://developer.mozilla.org/docs/Web/HTML/Element/template) หลายตัวโดยใช้ `` element เทมเพลตเป็นบล็อก HTML ที่นำมาใช้ซ้ำได้ซึ่งเบราว์เซอร์ไม่แสดง และต้องถูกสร้างขึ้นใน runtime โดยใช้ JavaScript
### มาสร้างกันเถอะ
เรากำลังจะสร้างแอปธนาคารที่มีหน้าจอหลักสองหน้าจอ: หน้าล็อกอินและแดชบอร์ด ก่อนอื่น ให้เพิ่มองค์ประกอบ placeholder ลงใน body ของ HTML ของเรา – นี่คือที่ที่หน้าจอต่าง ๆ ของเราจะปรากฏ:
```html
Loading...
```
**ทำความเข้าใจ placeholder นี้:**
- **สร้าง** container ที่มี ID "app" ซึ่งหน้าจอทั้งหมดจะถูกแสดง
- **แสดง**ข้อความโหลดจนกว่า JavaScript จะเริ่มต้นหน้าจอแรก
- **ให้**จุดติดตั้งเดียวสำหรับเนื้อหาแบบไดนามิกของเรา
- **ช่วยให้**การกำหนดเป้าหมายจาก JavaScript ง่ายขึ้นโดยใช้ `document.getElementById()`
> 💡 **เคล็ดลับ**: เนื่องจากเนื้อหาขององค์ประกอบนี้จะถูกแทนที่ เราสามารถใส่ข้อความหรือตัวบ่งชี้การโหลดที่จะแสดงในขณะที่แอปกำลังโหลด
ต่อไป ให้เพิ่มเทมเพลต HTML สำหรับหน้าล็อกอินด้านล่าง สำหรับตอนนี้เราจะใส่แค่หัวข้อและส่วนที่มีลิงก์ที่เราจะใช้ในการนำทาง
```html
Bank App
```
**การวิเคราะห์เทมเพลตล็อกอินนี้:**
- **กำหนด**เทมเพลตด้วยตัวระบุเฉพาะ "login" เพื่อการกำหนดเป้าหมายด้วย JavaScript
- **รวม**หัวข้อหลักที่สร้างแบรนด์ของแอป
- **มี**องค์ประกอบ `` ที่มีความหมายเพื่อจัดกลุ่มเนื้อหาที่เกี่ยวข้อง
- **ให้**ลิงก์นำทางที่จะนำผู้ใช้ไปยังแดชบอร์ด
จากนั้นเราจะเพิ่มเทมเพลต HTML อีกตัวสำหรับหน้าแดชบอร์ด หน้านี้จะมีส่วนต่าง ๆ ดังนี้:
- ส่วนหัวที่มีชื่อเรื่องและลิงก์ออกจากระบบ
- ยอดเงินปัจจุบันในบัญชีธนาคาร
- รายการธุรกรรมที่แสดงในตาราง
```html
```
**มาทำความเข้าใจแต่ละส่วนของแดชบอร์ดนี้:**
- **จัดโครงสร้าง**หน้าเว็บด้วยองค์ประกอบ `` ที่มีความหมายซึ่งมีการนำทาง
- **แสดง**ชื่อแอปอย่างสม่ำเสมอในหน้าจอเพื่อสร้างแบรนด์
- **ให้**ลิงก์ออกจากระบบที่นำกลับไปยังหน้าล็อกอิน
- **แสดง**ยอดเงินบัญชีปัจจุบันในส่วนที่กำหนด
- **จัดระเบียบ**ข้อมูลธุรกรรมโดยใช้ตาราง HTML ที่มีโครงสร้างอย่างเหมาะสม
- **กำหนด**หัวตารางสำหรับคอลัมน์วันที่ รายการ และจำนวนเงิน
- **เว้นว่าง**ส่วนเนื้อหาตารางสำหรับการฉีดเนื้อหาแบบไดนามิกในภายหลัง
> 💡 **เคล็ดลับ**: เมื่อสร้างเทมเพลต HTML หากคุณต้องการดูว่ามันจะมีลักษณะอย่างไร คุณสามารถแสดงความคิดเห็นบรรทัด `` และ `` โดยใส่ไว้ใน ``
✅ ทำไมคุณคิดว่าเราต้องใช้ `id` ในเทมเพลต? เราสามารถใช้สิ่งอื่นเช่น class ได้หรือไม่?
## ทำให้เทมเพลตมีชีวิตด้วย JavaScript
ตอนนี้เราต้องทำให้เทมเพลตของเราทำงานได้ เหมือนกับที่เครื่องพิมพ์ 3D ใช้พิมพ์เขียวดิจิทัลและสร้างวัตถุจริง JavaScript ใช้เทมเพลตที่ซ่อนอยู่ของเราและสร้างองค์ประกอบที่มองเห็นได้และโต้ตอบได้ที่ผู้ใช้สามารถเห็นและใช้งานได้
กระบวนการนี้มี 3 ขั้นตอนที่สม่ำเสมอซึ่งเป็นพื้นฐานของการพัฒนาเว็บสมัยใหม่ เมื่อคุณเข้าใจรูปแบบนี้ คุณจะสามารถรับรู้ได้ในเฟรมเวิร์กและไลบรารีหลายตัว
หากคุณลองไฟล์ HTML ปัจจุบันในเบราว์เซอร์ คุณจะเห็นว่ามันติดอยู่ที่การแสดง `Loading...` นั่นเป็นเพราะเราต้องเพิ่มโค้ด JavaScript เพื่อสร้างและแสดงเทมเพลต HTML
การสร้างเทมเพลตมักทำใน 3 ขั้นตอน:
1. ดึงองค์ประกอบเทมเพลตใน DOM เช่นโดยใช้ [`document.getElementById`](https://developer.mozilla.org/docs/Web/API/Document/getElementById)
2. โคลนองค์ประกอบเทมเพลต โดยใช้ [`cloneNode`](https://developer.mozilla.org/docs/Web/API/Node/cloneNode)
3. แนบมันเข้ากับ DOM ใต้องค์ประกอบที่มองเห็นได้ เช่นโดยใช้ [`appendChild`](https://developer.mozilla.org/docs/Web/API/Node/appendChild)
```mermaid
flowchart TD
A[🔍 Step 1: Find Template] --> B[📋 Step 2: Clone Template]
B --> C[🔗 Step 3: Attach to DOM]
A1["document.getElementById('login')"] --> A
B1["template.content.cloneNode(true)"] --> B
C1["app.appendChild(view)"] --> C
C --> D[👁️ Template Visible to User]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
```
**การวิเคราะห์ภาพของกระบวนการ:**
- **ขั้นตอนที่ 1** ค้นหาเทมเพลตที่ซ่อนอยู่ในโครงสร้าง DOM
- **ขั้นตอนที่ 2** สร้างสำเนาที่สามารถแก้ไขได้อย่างปลอดภัย
- **ขั้นตอนที่ 3** แทรกสำเนาเข้าไปในพื้นที่หน้าเว็บที่มองเห็นได้
- **ผลลัพธ์** คือหน้าจอที่ใช้งานได้ซึ่งผู้ใช้สามารถโต้ตอบได้
✅ ทำไมเราต้องโคลนเทมเพลตก่อนแนบเข้ากับ DOM? คุณคิดว่าจะเกิดอะไรขึ้นหากเราข้ามขั้นตอนนี้?
### งาน
สร้างไฟล์ใหม่ชื่อ `app.js` ในโฟลเดอร์โปรเจกต์ของคุณและนำเข้าไฟล์นั้นในส่วน `` ของ HTML:
```html
```
**การทำความเข้าใจการนำเข้า script นี้:**
- **เชื่อมโยง**ไฟล์ JavaScript กับเอกสาร HTML ของเรา
- **ใช้**แอตทริบิวต์ `defer` เพื่อให้สคริปต์ทำงานหลังจากการวิเคราะห์ HTML เสร็จสิ้น
- **เปิดใช้งาน**การเข้าถึงองค์ประกอบ DOM ทั้งหมดเนื่องจากโหลดเสร็จสมบูรณ์ก่อนการดำเนินการสคริปต์
- **ปฏิบัติตาม**แนวทางปฏิบัติที่ดีที่สุดในปัจจุบันสำหรับการโหลดสคริปต์และประสิทธิภาพ
ตอนนี้ใน `app.js` เราจะสร้างฟังก์ชันใหม่ชื่อ `updateRoute`:
```js
function updateRoute(templateId) {
const template = document.getElementById(templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
}
```
**ทีละขั้นตอน นี่คือสิ่งที่เกิดขึ้น:**
- **ค้นหา**องค์ประกอบเทมเพลตโดยใช้ ID เฉพาะของมัน
- **สร้าง**สำเนาลึกของเนื้อหาเทมเพลตโดยใช้ `cloneNode(true)`
- **ค้นหา** container ของแอปที่เนื้อหาจะถูกแสดง
- **ล้าง**เนื้อหาที่มีอยู่ใน container ของแอป
- **แทรก**เนื้อหาเทมเพลตที่โคลนเข้าไปใน DOM ที่มองเห็นได้
ตอนนี้เรียกฟังก์ชันนี้ด้วยหนึ่งในเทมเพลตและดูผลลัพธ์
```js
updateRoute('login');
```
**สิ่งที่การเรียกฟังก์ชันนี้ทำสำเร็จ:**
- **เปิดใช้งาน**เทมเพลตล็อกอินโดยส่ง ID เป็นพารามิเตอร์
- **แสดงให้เห็น**วิธีการสลับระหว่างหน้าจอแอปต่าง ๆ ด้วยโปรแกรม
- **แสดง**หน้าจอล็อกอินแทนข้อความ "Loading..."
✅ วัตถุประสงค์ของโค้ดนี้ `app.innerHTML = '';` คืออะไร? จะเกิดอะไรขึ้นหากไม่มีมัน?
## การสร้างเส้นทาง
การกำหนดเส้นทางเป็นเรื่องของการเชื่อมโยง URL กับเนื้อหาที่ถูกต้อง ลองนึกถึงการที่ผู้ให้บริการโทรศัพท์ในยุคแรกใช้ switchboards เพื่อเชื่อมต่อสาย – พวกเขาจะรับคำขอที่เข้ามาและกำหนดเส้นทางไปยังปลายทางที่ถูกต้อง การกำหนดเส้นทางเว็บทำงานในลักษณะเดียวกัน โดยรับคำขอ URL และกำหนดว่าเนื้อหาใดควรแสดง
โดยปกติ เซิร์ฟเวอร์เว็บจะจัดการเรื่องนี้โดยการให้บริการไฟล์ HTML ต่าง ๆ สำหรับ URL ต่าง ๆ เนื่องจากเรากำลังสร้างแอปหน้าเดียว เราจำเป็นต้องจัดการการกำหนดเส้นทางนี้ด้วยตัวเองโดยใช้ JavaScript วิธีนี้ทำให้เราสามารถควบคุมประสบการณ์ผู้ใช้และประสิทธิภาพได้มากขึ้น
```mermaid
flowchart LR
A["🌐 URL Path
/dashboard"] --> B["🗺️ Routes Object
Lookup"]
B --> C["🎯 Template ID
'dashboard'"]
C --> D["📄 Find Template
getElementById"]
D --> E["👁️ Display Screen
Clone & Append"]
F["📍 /login"] --> G["🎯 'login'"]
H["📍 /unknown"] --> I["❌ Not Found"]
I --> J["🔄 Redirect to /login"]
style B fill:#e3f2fd
style E fill:#e8f5e8
style I fill:#ffebee
style J fill:#fff3e0
```
**การทำความเข้าใจการไหลของการกำหนดเส้นทาง:**
- **การเปลี่ยนแปลง URL** กระตุ้นการค้นหาในการกำหนดค่าการกำหนดเส้นทางของเรา
- **เส้นทางที่ถูกต้อง** เชื่อมโยงกับ ID เทมเพลตเฉพาะสำหรับการแสดงผล
- **เส้นทางที่ไม่ถูกต้อง** กระตุ้นพฤติกรรมสำรองเพื่อป้องกันสถานะที่เสียหาย
- **การแสดงผลเทมเพลต** ปฏิบัติตามกระบวนการสามขั้นตอนที่เราเรียนรู้มาก่อนหน้านี้
เมื่อพูดถึงเว็บแอป เราเรียก *การกำหนดเส้นทาง* ว่าความตั้งใจที่จะเชื่อมโยง **URL** กับหน้าจอเฉพาะที่ควรแสดง ในเว็บไซต์ที่มีไฟล์ HTML หลายไฟล์ สิ่งนี้จะทำโดยอัตโนมัติเนื่องจากเส้นทางไฟล์สะท้อนอยู่ใน URL ตัวอย่างเช่น ด้วยไฟล์เหล่านี้ในโฟลเดอร์โปรเจกต์ของคุณ:
```
mywebsite/index.html
mywebsite/login.html
mywebsite/admin/index.html
```
หากคุณสร้างเซิร์ฟเวอร์เว็บโดยมี `mywebsite` เป็น root การแมป URL จะเป็น:
```
https://site.com --> mywebsite/index.html
https://site.com/login.html --> mywebsite/login.html
https://site.com/admin/ --> mywebsite/admin/index.html
```
อย่างไรก็ตาม สำหรับเว็บแอปของเรา เรากำลังใช้ไฟล์ HTML เดียวที่มีหน้าจอทั้งหมด ดังนั้นพฤติกรรมเริ่มต้นนี้
✅ จะเกิดอะไรขึ้นถ้าคุณใส่เส้นทางที่ไม่รู้จักใน URL? เราจะแก้ไขปัญหานี้ได้อย่างไร?
## เพิ่มระบบนำทาง
เมื่อมีการตั้งค่าการกำหนดเส้นทางแล้ว ผู้ใช้จำเป็นต้องมีวิธีการนำทางผ่านแอป เว็บไซต์แบบดั้งเดิมจะโหลดหน้าเว็บใหม่ทั้งหมดเมื่อคลิกลิงก์ แต่เราต้องการอัปเดตทั้ง URL และเนื้อหาโดยไม่ต้องรีเฟรชหน้าเว็บ วิธีนี้จะช่วยสร้างประสบการณ์ที่ราบรื่นเหมือนกับแอปพลิเคชันบนเดสก์ท็อปที่เปลี่ยนมุมมองต่างๆ
เราต้องประสานสองสิ่งนี้: การอัปเดต URL ของเบราว์เซอร์เพื่อให้ผู้ใช้สามารถบันทึกหน้าและแชร์ลิงก์ และการแสดงเนื้อหาที่เหมาะสม เมื่อดำเนินการอย่างถูกต้อง จะสร้างการนำทางที่ราบรื่นตามที่ผู้ใช้คาดหวังจากแอปพลิเคชันสมัยใหม่
> 🏗️ **ข้อมูลเชิงลึกเกี่ยวกับสถาปัตยกรรม**: ส่วนประกอบของระบบนำทาง
>
> **สิ่งที่คุณกำลังสร้าง:**
> - **🔄 การจัดการ URL**: อัปเดตแถบที่อยู่ของเบราว์เซอร์โดยไม่ต้องโหลดหน้าใหม่
> - **📋 ระบบเทมเพลต**: สลับเนื้อหาแบบไดนามิกตามเส้นทางปัจจุบัน
> - **📚 การรวมประวัติ**: รักษาฟังก์ชันการทำงานของปุ่มย้อนกลับ/ไปข้างหน้าของเบราว์เซอร์
> - **🛡️ การจัดการข้อผิดพลาด**: การแก้ไขปัญหาอย่างราบรื่นสำหรับเส้นทางที่ไม่ถูกต้องหรือขาดหายไป
>
> **การทำงานร่วมกันของส่วนประกอบ:**
> - **ฟัง** เหตุการณ์การนำทาง (การคลิก การเปลี่ยนแปลงประวัติ)
> - **อัปเดต** URL โดยใช้ History API
> - **แสดงผล** เทมเพลตที่เหมาะสมสำหรับเส้นทางใหม่
> - **รักษา** ประสบการณ์ผู้ใช้ที่ราบรื่นตลอดเวลา
ขั้นตอนต่อไปสำหรับแอปของเราคือการเพิ่มความสามารถในการนำทางระหว่างหน้าโดยไม่ต้องเปลี่ยน URL ด้วยตนเอง ซึ่งหมายถึงสองสิ่ง:
1. อัปเดต URL ปัจจุบัน
2. อัปเดตเทมเพลตที่แสดงผลตาม URL ใหม่
เราได้ดูแลส่วนที่สองด้วยฟังก์ชัน `updateRoute` แล้ว ดังนั้นเราต้องหาวิธีอัปเดต URL ปัจจุบัน
เราจะต้องใช้ JavaScript และโดยเฉพาะอย่างยิ่ง [`history.pushState`](https://developer.mozilla.org/docs/Web/API/History/pushState) ซึ่งช่วยให้อัปเดต URL และสร้างรายการใหม่ในประวัติการท่องเว็บโดยไม่ต้องโหลด HTML ใหม่
> ⚠️ **หมายเหตุสำคัญ**: แม้ว่าองค์ประกอบ HTML anchor [``](https://developer.mozilla.org/docs/Web/HTML/Element/a) สามารถใช้สร้างไฮเปอร์ลิงก์ไปยัง URL ต่างๆ ได้ด้วยตัวเอง แต่โดยค่าเริ่มต้นมันจะทำให้เบราว์เซอร์โหลด HTML ใหม่ จำเป็นต้องป้องกันพฤติกรรมนี้เมื่อจัดการการกำหนดเส้นทางด้วย JavaScript แบบกำหนดเอง โดยใช้ฟังก์ชัน `preventDefault()` บนเหตุการณ์คลิก
### งาน
มาสร้างฟังก์ชันใหม่ที่เราสามารถใช้ในการนำทางในแอปของเรา:
```js
function navigate(path) {
window.history.pushState({}, path, path);
updateRoute();
}
```
**การทำความเข้าใจฟังก์ชันการนำทางนี้:**
- **อัปเดต** URL ของเบราว์เซอร์ไปยังเส้นทางใหม่โดยใช้ `history.pushState`
- **เพิ่ม** รายการใหม่ในสแต็กประวัติของเบราว์เซอร์เพื่อรองรับปุ่มย้อนกลับ/ไปข้างหน้า
- **เรียกใช้** ฟังก์ชัน `updateRoute()` เพื่อแสดงเทมเพลตที่เกี่ยวข้อง
- **รักษา** ประสบการณ์แอปหน้าเดียวโดยไม่ต้องโหลดหน้าใหม่
วิธีนี้จะอัปเดต URL ปัจจุบันตามเส้นทางที่กำหนดก่อน แล้วจึงอัปเดตเทมเพลต คุณสมบัติ `window.location.origin` จะคืนค่ารากของ URL ซึ่งช่วยให้เราสร้าง URL ที่สมบูรณ์จากเส้นทางที่กำหนดได้
ตอนนี้เรามีฟังก์ชันนี้แล้ว เราสามารถจัดการปัญหาที่เกิดขึ้นเมื่อเส้นทางไม่ตรงกับเส้นทางที่กำหนดไว้ เราจะปรับเปลี่ยนฟังก์ชัน `updateRoute` โดยเพิ่มการแก้ไขปัญหาไปยังหนึ่งในเส้นทางที่มีอยู่หากเราไม่พบการจับคู่
```js
function updateRoute() {
const path = window.location.pathname;
const route = routes[path];
if (!route) {
return navigate('/login');
}
const template = document.getElementById(route.templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
}
```
**จุดสำคัญที่ต้องจำ:**
- **ตรวจสอบ** ว่ามีเส้นทางสำหรับเส้นทางปัจจุบันหรือไม่
- **เปลี่ยนเส้นทาง** ไปยังหน้าเข้าสู่ระบบเมื่อเข้าถึงเส้นทางที่ไม่ถูกต้อง
- **ให้** กลไกการแก้ไขปัญหาที่ป้องกันการนำทางที่เสียหาย
- **รับรอง** ว่าผู้ใช้จะเห็นหน้าจอที่ถูกต้องเสมอ แม้ในกรณีที่ URL ไม่ถูกต้อง
หากไม่พบเส้นทาง เราจะเปลี่ยนเส้นทางไปยังหน้า `login`
ตอนนี้มาสร้างฟังก์ชันเพื่อรับ URL เมื่อคลิกลิงก์ และป้องกันพฤติกรรมลิงก์เริ่มต้นของเบราว์เซอร์:
```js
function onLinkClick(event) {
event.preventDefault();
navigate(event.target.href);
}
```
**การแยกย่อยตัวจัดการคลิกนี้:**
- **ป้องกัน** พฤติกรรมลิงก์เริ่มต้นของเบราว์เซอร์โดยใช้ `preventDefault()`
- **ดึง** URL ปลายทางจากองค์ประกอบลิงก์ที่คลิก
- **เรียกใช้** ฟังก์ชันการนำทางแบบกำหนดเองแทนการโหลดหน้าใหม่
- **รักษา** ประสบการณ์แอปหน้าเดียวที่ราบรื่น
```html
Login
...
Logout
```
**สิ่งที่การผูก onclick นี้ทำได้:**
- **เชื่อมโยง** ลิงก์แต่ละลิงก์กับระบบนำทางแบบกำหนดเองของเรา
- **ส่งผ่าน** เหตุการณ์คลิกไปยังฟังก์ชัน `onLinkClick` ของเราเพื่อประมวลผล
- **เปิดใช้งาน** การนำทางที่ราบรื่นโดยไม่ต้องโหลดหน้าใหม่
- **รักษา** โครงสร้าง URL ที่เหมาะสมที่ผู้ใช้สามารถบันทึกหรือแชร์ได้
แอตทริบิวต์ [`onclick`](https://developer.mozilla.org/docs/Web/API/GlobalEventHandlers/onclick) จะผูกเหตุการณ์ `click` กับโค้ด JavaScript ที่นี่คือการเรียกฟังก์ชัน `navigate()`
ลองคลิกที่ลิงก์เหล่านี้ คุณควรจะสามารถนำทางระหว่างหน้าจอต่างๆ ของแอปของคุณได้แล้ว
✅ เมธอด `history.pushState` เป็นส่วนหนึ่งของมาตรฐาน HTML5 และถูกนำไปใช้ใน [เบราว์เซอร์สมัยใหม่ทั้งหมด](https://caniuse.com/?search=pushState) หากคุณกำลังสร้างเว็บแอปสำหรับเบราว์เซอร์รุ่นเก่า มีเคล็ดลับที่คุณสามารถใช้แทน API นี้ได้: การใช้ [hash (`#`)](https://en.wikipedia.org/wiki/URI_fragment) ก่อนเส้นทาง คุณสามารถใช้การกำหนดเส้นทางที่ทำงานร่วมกับการนำทางลิงก์ปกติและไม่โหลดหน้าใหม่ เนื่องจากมีวัตถุประสงค์เพื่อสร้างลิงก์ภายในหน้า
## ทำให้ปุ่มย้อนกลับและไปข้างหน้าทำงาน
ปุ่มย้อนกลับและไปข้างหน้าเป็นสิ่งสำคัญสำหรับการท่องเว็บ เช่นเดียวกับที่ผู้ควบคุมภารกิจของ NASA สามารถตรวจสอบสถานะระบบก่อนหน้าได้ในระหว่างภารกิจอวกาศ ผู้ใช้คาดหวังว่าปุ่มเหล่านี้จะทำงาน และเมื่อไม่ทำงาน มันจะทำลายประสบการณ์การท่องเว็บที่คาดหวัง
แอปหน้าเดียวของเราต้องการการกำหนดค่าเพิ่มเติมเพื่อรองรับสิ่งนี้ เบราว์เซอร์รักษาสแต็กประวัติ (ซึ่งเราได้เพิ่มด้วย `history.pushState`) แต่เมื่อผู้ใช้นำทางผ่านประวัติ แอปของเราต้องตอบสนองโดยการอัปเดตเนื้อหาที่แสดงผลตามนั้น
```mermaid
sequenceDiagram
participant User
participant Browser
participant App
participant Template
User->>Browser: Clicks "Login" link
Browser->>App: onclick event triggered
App->>App: preventDefault() & navigate('/dashboard')
App->>Browser: history.pushState('/dashboard')
Browser->>Browser: URL updates to /dashboard
App->>App: updateRoute() called
App->>Template: Find & clone dashboard template
Template->>App: Return cloned content
App->>Browser: Replace app content with template
Browser->>User: Display dashboard screen
Note over User,Template: User clicks browser back button
User->>Browser: Clicks back button
Browser->>Browser: History moves back to /login
Browser->>App: popstate event fired
App->>App: updateRoute() called automatically
App->>Template: Find & clone login template
Template->>App: Return cloned content
App->>Browser: Replace app content with template
Browser->>User: Display login screen
```
**จุดปฏิสัมพันธ์สำคัญ:**
- **การกระทำของผู้ใช้** กระตุ้นการนำทางผ่านการคลิกหรือปุ่มเบราว์เซอร์
- **แอปสกัดกั้น** การคลิกลิงก์เพื่อป้องกันการโหลดหน้าใหม่
- **History API** จัดการการเปลี่ยนแปลง URL และสแต็กประวัติของเบราว์เซอร์
- **เทมเพลต** ให้โครงสร้างเนื้อหาสำหรับแต่ละหน้าจอ
- **ตัวฟังเหตุการณ์** รับรองว่าแอปตอบสนองต่อการนำทางทุกประเภท
การใช้ `history.pushState` สร้างรายการใหม่ในประวัติการนำทางของเบราว์เซอร์ คุณสามารถตรวจสอบได้โดยการกดปุ่ม *ย้อนกลับ* ของเบราว์เซอร์ มันควรจะแสดงบางอย่างเช่นนี้:

หากคุณลองคลิกปุ่มย้อนกลับหลายครั้ง คุณจะเห็นว่า URL ปัจจุบันเปลี่ยนไปและประวัติได้รับการอัปเดต แต่เทมเพลตเดียวกันยังคงแสดงผลอยู่
นั่นเป็นเพราะแอปพลิเคชันไม่ทราบว่าเราต้องเรียก `updateRoute()` ทุกครั้งที่ประวัติเปลี่ยนแปลง หากคุณดูเอกสาร [`history.pushState`](https://developer.mozilla.org/docs/Web/API/History/pushState) คุณจะเห็นว่าหากสถานะเปลี่ยนแปลง - หมายความว่าเราได้ย้ายไปยัง URL ที่แตกต่างกัน - เหตุการณ์ [`popstate`](https://developer.mozilla.org/docs/Web/API/Window/popstate_event) จะถูกกระตุ้น เราจะใช้สิ่งนั้นเพื่อแก้ไขปัญหานี้
### งาน
เพื่อให้แน่ใจว่าเทมเพลตที่แสดงผลได้รับการอัปเดตเมื่อประวัติของเบราว์เซอร์เปลี่ยนแปลง เราจะผูกฟังก์ชันใหม่ที่เรียก `updateRoute()` เราจะทำสิ่งนี้ที่ด้านล่างของไฟล์ `app.js` ของเรา:
```js
window.onpopstate = () => updateRoute();
updateRoute();
```
**การทำความเข้าใจการรวมประวัตินี้:**
- **ฟัง** เหตุการณ์ `popstate` ที่เกิดขึ้นเมื่อผู้ใช้นำทางด้วยปุ่มเบราว์เซอร์
- **ใช้** ฟังก์ชันลูกศรสำหรับไวยากรณ์ตัวจัดการเหตุการณ์ที่กระชับ
- **เรียกใช้** `updateRoute()` โดยอัตโนมัติทุกครั้งที่สถานะประวัติเปลี่ยนแปลง
- **เริ่มต้น** แอปโดยการเรียก `updateRoute()` เมื่อหน้าโหลดครั้งแรก
- **รับรอง** ว่าเทมเพลตที่ถูกต้องจะแสดงผลไม่ว่าผู้ใช้จะนำทางอย่างไร
> 💡 **เคล็ดลับมือโปร**: เราใช้ [ฟังก์ชันลูกศร](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/Arrow_functions) ที่นี่เพื่อประกาศตัวจัดการเหตุการณ์ `popstate` ของเราเพื่อความกระชับ แต่ฟังก์ชันปกติก็สามารถทำงานได้เหมือนกัน
นี่คือวิดีโอรีเฟรชเกี่ยวกับฟังก์ชันลูกศร:
[](https://youtube.com/watch?v=OP6eEbOj2sc "ฟังก์ชันลูกศร")
> 🎥 คลิกที่ภาพด้านบนเพื่อดูวิดีโอเกี่ยวกับฟังก์ชันลูกศร
ตอนนี้ลองใช้ปุ่มย้อนกลับและไปข้างหน้าของเบราว์เซอร์ของคุณ และตรวจสอบว่าเส้นทางที่แสดงผลได้รับการอัปเดตอย่างถูกต้องในครั้งนี้
---
## ความท้าทาย GitHub Copilot Agent 🚀
ใช้โหมด Agent เพื่อทำความท้าทายต่อไปนี้:
**คำอธิบาย:** ปรับปรุงแอปธนาคารโดยการเพิ่มการจัดการข้อผิดพลาดและเทมเพลตหน้า 404 สำหรับเส้นทางที่ไม่ถูกต้อง เพื่อปรับปรุงประสบการณ์ผู้ใช้เมื่อไปยังหน้าที่ไม่มีอยู่
**คำสั่ง:** สร้างเทมเพลต HTML ใหม่ที่มี id "not-found" ซึ่งแสดงหน้า 404 ที่เป็นมิตรต่อผู้ใช้พร้อมการจัดสไตล์ จากนั้นปรับเปลี่ยนตรรกะการกำหนดเส้นทาง JavaScript เพื่อแสดงเทมเพลตนี้เมื่อผู้ใช้นำทางไปยัง URL ที่ไม่ถูกต้อง และเพิ่มปุ่ม "Go Home" ที่นำทางกลับไปยังหน้าเข้าสู่ระบบ
เรียนรู้เพิ่มเติมเกี่ยวกับ [โหมด Agent](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) ที่นี่
## 🚀 ความท้าทาย
เพิ่มเทมเพลตและเส้นทางใหม่สำหรับหน้าที่สามที่แสดงเครดิตสำหรับแอปนี้
**เป้าหมายของความท้าทาย:**
- **สร้าง** เทมเพลต HTML ใหม่ที่มีโครงสร้างเนื้อหาที่เหมาะสม
- **เพิ่ม** เส้นทางใหม่ในวัตถุการกำหนดค่าของเส้นทางของคุณ
- **รวม** ลิงก์นำทางไปและกลับจากหน้าครดิต
- **ทดสอบ** ว่าการนำทางทั้งหมดทำงานอย่างถูกต้องกับประวัติของเบราว์เซอร์
## แบบทดสอบหลังการบรรยาย
[แบบทดสอบหลังการบรรยาย](https://ff-quizzes.netlify.app/web/quiz/42)
## ทบทวนและศึกษาด้วยตนเอง
การกำหนดเส้นทางเป็นหนึ่งในส่วนที่ยุ่งยากอย่างน่าประหลาดใจของการพัฒนาเว็บ โดยเฉพาะอย่างยิ่งเมื่อเว็บเปลี่ยนจากพฤติกรรมการรีเฟรชหน้าไปเป็นการรีเฟรชหน้าในแอปพลิเคชันหน้าเดียว ลองอ่านเกี่ยวกับ [วิธีที่บริการ Azure Static Web App](https://docs.microsoft.com/azure/static-web-apps/routes/?WT.mc_id=academic-77807-sagibbon) จัดการการกำหนดเส้นทาง คุณสามารถอธิบายได้ไหมว่าทำไมการตัดสินใจบางอย่างที่อธิบายไว้ในเอกสารนั้นจึงจำเป็น?
**แหล่งเรียนรู้เพิ่มเติม:**
- **สำรวจ** วิธีที่เฟรมเวิร์กยอดนิยมอย่าง React Router และ Vue Router ใช้การกำหนดเส้นทางฝั่งไคลเอนต์
- **ค้นคว้า** ความแตกต่างระหว่างการกำหนดเส้นทางแบบ hash-based และ history API
- **เรียนรู้** เกี่ยวกับการเรนเดอร์ฝั่งเซิร์ฟเวอร์ (SSR) และวิธีที่มันส่งผลต่อกลยุทธ์การกำหนดเส้นทาง
- **ตรวจสอบ** วิธีที่ Progressive Web Apps (PWAs) จัดการการกำหนดเส้นทางและการนำทาง
## งานที่ได้รับมอบหมาย
[ปรับปรุงการกำหนดเส้นทาง](assignment.md)
---
**ข้อจำกัดความรับผิดชอบ**:
เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI [Co-op Translator](https://github.com/Azure/co-op-translator) แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่ถูกต้อง เอกสารต้นฉบับในภาษาดั้งเดิมควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลสำคัญ ขอแนะนำให้ใช้บริการแปลภาษามืออาชีพ เราจะไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความผิดที่เกิดจากการใช้การแปลนี้