Now.js Framework Documentation
RouterManager - ตัวจัดการเส้นทาง
RouterManager - ตัวจัดการเส้นทาง
Now.js Router Manager สำหรับจัดการการกำหนดเส้นทาง (routing) และการนำทาง (navigation) ในแอปพลิเคชันแบบ Single Page Application (SPA)
ภาพรวม
RouterManager เป็น core component ของ Now.js ที่จัดการการ routing และ navigation โดยรองรับทั้ง hash-based routing (#/path) และ history API (/path) พร้อมด้วยระบบ authentication guards, middleware, และ route parameter extraction
กรณีการใช้งาน:
- การกำหนดเส้นทางสำหรับ Single Page Application (SPA)
- โหลดคอนเทนต์แบบไดนามิกโดยไม่ต้องรีเฟรชหน้า
- จัดการพารามิเตอร์ใน URL และ query string
- ตั้งค่า route guard และการยืนยันตัวตน
- รองรับเส้นทางซ้อน (Nested routes) และกลุ่มเส้นทาง
- จัดการหน้า 404 แบบกำหนดเองได้
การรองรับเบราว์เซอร์:
- Chrome, Firefox, Safari, Edge (เวอร์ชันสมัยใหม่)
- รองรับ History API สำหรับบราวเซอร์รุ่นใหม่
- ถอยกลับไปใช้โหมด hash สำหรับบราวเซอร์รุ่นเก่า
การติดตั้งและการเริ่มต้นใช้งาน
การโหลดสคริปต์
RouterManager ถูกรวมอยู่ใน Now.js core แล้ว:
<script src="/Now/Now.js"></script>หรือโหลดแยกต่างหาก:
<script src="/Now/js/RouterManager.js"></script>การเริ่มต้นใช้งานพื้นฐาน
// เริ่มต้น Router แบบง่าย
await RouterManager.init({
enabled: true,
mode: 'history', // 'hash' หรือ 'history'
base: '/'
});
// ลงทะเบียน routes
RouterManager.register('/home', {
template: 'home',
title: 'Home Page'
});
RouterManager.register('/about', {
template: 'about',
title: 'About Us'
});การเริ่มต้นผ่าน Now.js
await Now.init({
router: {
enabled: true,
mode: 'history',
base: '/',
notFound: {
path: '/404',
template: '404',
title: 'Page Not Found'
}
}
});ตัวเลือกการตั้งค่า
ตัวเลือกหลัก
| ตัวเลือก | ประเภท | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
enabled |
boolean | false | เปิดใช้งาน router |
mode |
string | 'history' | โหมด routing: 'hash' หรือ 'history' |
base |
string | '/' | เส้นทางฐานของแอปพลิเคชัน |
autoDetectBase |
boolean | true | ตรวจจับเส้นทางฐานอัตโนมัติจากตำแหน่งสคริปต์ |
fallback |
string | 'index.html' | หน้า fallback สำหรับ 404 |
showLoadingNotification |
boolean | true | แสดงการแจ้งเตือนขณะโหลด |
การตั้งค่าการยืนยันตัวตน
auth: {
enabled: false, // เปิดใช้ auth guards
autoGuard: true, // ป้องกันเส้นทางตามคอนเวนชันโดยอัตโนมัติ
defaultRequireAuth: false, // กำหนดให้ทุกเส้นทางต้องยืนยันตัวตนเป็นค่าเริ่มต้น
publicPaths: ['/login', '/register'],
guestOnlyPaths: ['/login', '/register'],
conventions: {
'/auth/*': { public: true },
'/admin/*': { roles: ['admin'] }
},
redirects: {
unauthorized: '/login',
forbidden: '/403',
afterLogin: '/',
afterLogout: '/login'
}
}การตั้งค่า Not Found
notFound: {
path: '/404', // เส้นทางสำหรับ 404
template: '404', // เทมเพลตสำหรับหน้า 404
title: 'Page Not Found',
behavior: 'render', // 'render', 'redirect', 'silent'
preserveUrl: false, // รักษา URL เดิมไว้
preserveQuery: true, // รักษา query string
customHandler: null // ฟังก์ชันจัดการแบบกำหนดเอง
}การตั้งค่าการโหลดเริ่มต้น
initialLoad: {
enabled: true, // จัดการ initial page load
preserveContent: false, // รักษา content เดิมไว้
skipIfContent: true, // ข้ามถ้ามีคอนเทนต์อยู่แล้ว
contentSelector: '#main', // ตัวเลือกสำหรับตรวจสอบคอนเทนต์
forceRoute: false // บังคับนำทางแม้มีคอนเทนต์
}การตั้งค่า Trailing Slash
trailingSlash: {
mode: 'remove', // 'remove', 'add', 'preserve'
redirect: false, // รีไดเรกต์เมื่อจัดการ trailing slash
ignorePaths: ['/'] // เส้นทางที่ไม่ต้องจัดการ trailing slash
}เมธอดและพร็อพเพอร์ตี
init(options)
เริ่มต้น RouterManager พร้อม configuration
พารามิเตอร์:
options(Object, optional) - อ็อบเจ็กต์การตั้งค่าเพิ่มเติม
ค่าที่ส่งกลับ: (Promise) - สำเร็จเมื่อการเริ่มต้นเสร็จสิ้น
ตัวอย่างการใช้งาน:
// เริ่มต้นแบบพื้นฐาน
await RouterManager.init({
enabled: true,
mode: 'history'
});
// เปิดใช้งานระบบยืนยันตัวตน
await RouterManager.init({
enabled: true,
mode: 'history',
auth: {
enabled: true,
defaultRequireAuth: true,
publicPaths: ['/login', '/register', '/']
}
});
// ใช้งาน handler 404 แบบกำหนดเอง
await RouterManager.init({
enabled: true,
notFound: {
behavior: 'redirect',
path: '/404',
customHandler: (path) => {
console.log('ไม่พบหน้า:', path);
}
}
});register(path, config)
ลงทะเบียน route เข้าสู่ router
พารามิเตอร์:
path(string) - เส้นทางที่รองรับพารามิเตอร์ เช่น/user/:idconfig(Object|string) - การตั้งค่าเส้นทางหรือชื่อเทมเพลต
วัตถุตั้งค่า (Configuration Object):
template(string) - พาธของเทมเพลตหรือ HTML stringtitle(string, optional) - ชื่อหน้าที่ต้องการแสดงguards(Array, optional) - ฟังก์ชัน guard สำหรับเส้นทางrequireAuth(boolean, optional) - ต้องการการยืนยันตัวตนหรือไม่roles(Array, optional) - กลุ่มบทบาทที่ต้องมีpublic(boolean, optional) - ระบุว่าเป็นเส้นทางสาธารณะchildren(Object, optional) - เส้นทางย่อยแบบซ้อน
ค่าที่ส่งกลับ: void
ตัวอย่างการใช้งาน:
// เส้นทางพื้นฐาน
RouterManager.register('/home', 'home');
// เส้นทางพร้อมการตั้งค่า
RouterManager.register('/about', {
template: 'about',
title: 'เกี่ยวกับเรา'
});
// เส้นทางที่มีพารามิเตอร์
RouterManager.register('/user/:id', {
template: 'user-profile',
title: 'โปรไฟล์ผู้ใช้'
});
// เส้นทางที่ต้องยืนยันตัวตน
RouterManager.register('/dashboard', {
template: 'dashboard',
title: 'แดชบอร์ด',
requireAuth: true
});
// เส้นทางที่จำกัดบทบาท
RouterManager.register('/admin', {
template: 'admin',
title: 'ผู้ดูแลระบบ',
requireAuth: true,
roles: ['admin']
});
// เส้นทางที่ใช้เทมเพลตแบบ inline
RouterManager.register('/info', {
template: '<div><h1>หน้าข้อมูล</h1><p>รายละเอียดเพิ่มเติม</p></div>',
title: 'ข้อมูล'
});
// เส้นทางที่มีเส้นทางย่อย
RouterManager.register('/docs', {
template: 'docs-layout',
title: 'เอกสารประกอบ',
children: {
'/getting-started': {
template: 'docs-getting-started',
title: 'เริ่มต้นใช้งาน'
},
'/api': {
template: 'docs-api',
title: 'เอกสาร API'
}
}
});navigate(path, params, options)
นำทางไปยังเส้นทางที่ระบุ
พารามิเตอร์:
path(string) - เส้นทางปลายทางparams(Object, optional) - พารามิเตอร์ของเส้นทางoptions(Object, optional) - ตัวเลือกเพิ่มเติมสำหรับการนำทางreplace(boolean) - ใช้ replaceState แทน pushStateskipGuards(boolean) - ข้ามการทำงานของ guardpreserveQuery(boolean) - รักษา query string เดิมไว้
ค่าที่ส่งกลับ: (Promise) - สำเร็จเมื่อการนำทางเสร็จสิ้น
ตัวอย่างการใช้งาน:
// นำทางแบบพื้นฐาน
await RouterManager.navigate('/home');
// นำทางพร้อมพารามิเตอร์
await RouterManager.navigate('/user/:id', { id: 123 });
// นำทางพร้อม query string
await RouterManager.navigate('/search?q=keyword');
// แทนที่รายการ history ปัจจุบัน
await RouterManager.navigate('/about', {}, { replace: true });
// นำทางจากโค้ดโดยตรง
document.getElementById('btn').addEventListener('click', () => {
RouterManager.navigate('/contact');
});
// นำทางพร้อมอ็อบเจ็กต์พารามิเตอร์
await RouterManager.navigate('/product/:category/:id', {
category: 'electronics',
id: 'laptop-001'
});
// ผลลัพธ์: /product/electronics/laptop-001beforeEach(guard)
เพิ่ม global guard ที่ทำงานก่อนทุก route navigation
พารามิเตอร์:
guard(Function) - ฟังก์ชัน guard รูปแบบ(to, from, next) => {}to(Object) - ข้อมูลเส้นทางปลายทางfrom(Object) - ข้อมูลเส้นทางปัจจุบันnext(Function) - เรียกเพื่อดำเนินการต่อ หรือส่งเส้นทางเพื่อ redirect
ค่าที่ส่งกลับ: (Function) - ฟังก์ชันสำหรับยกเลิกการลงทะเบียน guard
ตัวอย่างการใช้งาน:
// บันทึกการนำทาง
RouterManager.beforeEach((to, from, next) => {
console.log(`กำลังนำทางจาก ${from.path} ไป ${to.path}`);
next();
});
// การตรวจสอบสิทธิ์ก่อนเข้าเส้นทาง
RouterManager.beforeEach(async (to, from, next) => {
const requireAuth = to.route?.requireAuth;
const isAuthenticated = await checkAuth();
if (requireAuth && !isAuthenticated) {
next('/login');
} else {
next();
}
});
// การตรวจสอบบทบาทผู้ใช้
RouterManager.beforeEach(async (to, from, next) => {
const requiredRoles = to.route?.roles;
if (requiredRoles) {
const userRoles = await getUserRoles();
const hasRole = requiredRoles.some(role => userRoles.includes(role));
if (!hasRole) {
next('/forbidden');
return;
}
}
next();
});
// ยกเลิกการใช้งาน guard
const unregister = RouterManager.beforeEach((to, from, next) => {
// โค้ดสำหรับ guard
next();
});
// ภายหลัง...
unregister(); // ลบ guard นี้ออกafterEach(guard)
เพิ่ม global guard ที่ทำงานหลังทุก route navigation
พารามิเตอร์:
guard(Function) - ฟังก์ชัน guard รูปแบบ(to, from) => {}
ค่าที่ส่งกลับ: (Function) - ฟังก์ชันสำหรับยกเลิกการลงทะเบียน guard
ตัวอย่างการใช้งาน:
// ติดตามการเข้าชมหน้า
RouterManager.afterEach((to, from) => {
if (window.gtag) {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.path
});
}
});
// เลื่อนกลับไปด้านบนสุด
RouterManager.afterEach(() => {
window.scrollTo(0, 0);
});
// อัปเดต breadcrumb
RouterManager.afterEach((to) => {
updateBreadcrumb(to.path);
});
// ปิดเมนูบนมือถือ
RouterManager.afterEach(() => {
const mobileMenu = document.querySelector('.mobile-menu');
if (mobileMenu) {
mobileMenu.classList.remove('open');
}
});getPath(includeQuery)
รับ current path
พารามิเตอร์:
includeQuery(boolean, optional) - ระบุว่าจะรวม query string หรือไม่
ค่าที่ส่งกลับ: (string) - เส้นทางปัจจุบัน
ตัวอย่างการใช้งาน:
// รับเส้นทางโดยไม่รวม query
const path = RouterManager.getPath();
console.log(path); // ผลลัพธ์: /user/123
// รับเส้นทางพร้อม query
const fullPath = RouterManager.getPath(true);
console.log(fullPath); // ผลลัพธ์: /user/123?tab=profile
// ตรวจสอบเส้นทางปัจจุบัน
if (RouterManager.getPath() === '/admin') {
console.log('อยู่ในหน้าแอดมิน');
}getQuery()
รับ query string ปัจจุบัน
ค่าที่ส่งกลับ: (string) - Query string ที่รวมสัญลักษณ์ ?
ตัวอย่างการใช้งาน:
// ที่อยู่ URL: /search?q=laptop&category=electronics
const query = RouterManager.getQuery();
console.log(query); // ผลลัพธ์: ?q=laptop&category=electronics
// แปลง query string
const params = new URLSearchParams(RouterManager.getQuery());
console.log(params.get('q')); // ผลลัพธ์: laptop
console.log(params.get('category')); // ผลลัพธ์: electronicsgetParams()
รับ route parameters ทั้งหมด
Return: (Object) - Parameters object
ตัวอย่างการใช้งาน:
// ที่อยู่ URL: /user/123
// เส้นทาง: /user/:id
const params = RouterManager.getParams();
console.log(params.id); // ผลลัพธ์: 123
// ที่อยู่ URL: /product/electronics/laptop-001
// เส้นทาง: /product/:category/:id
const params = RouterManager.getParams();
console.log(params.category); // ผลลัพธ์: electronics
console.log(params.id); // ผลลัพธ์: laptop-001getParam(name)
รับ route parameter ตามชื่อ
พารามิเตอร์:
name(string) - ชื่อของพารามิเตอร์
ค่าที่ส่งกลับ: (string|undefined) - ค่าพารามิเตอร์ที่พบ
ตัวอย่างการใช้งาน:
// ที่อยู่ URL: /user/123
// เส้นทาง: /user/:id
const userId = RouterManager.getParam('id');
console.log(userId); // ผลลัพธ์: 123
// ตรวจสอบว่ามีพารามิเตอร์หรือไม่
const categoryId = RouterManager.getParam('categoryId');
if (categoryId) {
loadCategory(categoryId);
}hasRoute(path)
ตรวจสอบว่ามี route ที่ระบุหรือไม่
พารามิเตอร์:
path(string) - เส้นทางที่ต้องการตรวจสอบ
ค่าที่ส่งกลับ: (boolean)
ตัวอย่างการใช้งาน:
if (RouterManager.hasRoute('/admin')) {
console.log('มีเส้นทางสำหรับผู้ดูแลระบบ');
}
// ตัดสินใจนำทางตามเงื่อนไข
const targetPath = '/dashboard';
if (RouterManager.hasRoute(targetPath)) {
RouterManager.navigate(targetPath);
} else {
RouterManager.navigate('/404');
}getRoute(path)
รับ route configuration
พารามิเตอร์:
path(string) - เส้นทางที่ต้องการค้นหา
ค่าที่ส่งกลับ: (Object|undefined) - การตั้งค่าของเส้นทางนั้น
ตัวอย่างการใช้งาน:
const route = RouterManager.getRoute('/dashboard');
console.log(route.title); // ผลลัพธ์: แดชบอร์ด
console.log(route.requireAuth); // ผลลัพธ์: true
// ตรวจสอบคุณสมบัติของเส้นทาง
const aboutRoute = RouterManager.getRoute('/about');
if (aboutRoute) {
console.log('เทมเพลต:', aboutRoute.template);
console.log('ชื่อหน้า:', aboutRoute.title);
}getRoutes()
รับ routes ทั้งหมด
ค่าที่ส่งกลับ: (Array) - รายการอ็อบเจ็กต์เส้นทาง
ตัวอย่างการใช้งาน:
const allRoutes = RouterManager.getRoutes();
allRoutes.forEach(route => {
console.log(`${route.path} - ${route.title}`);
});
// สร้างเมนูนำทาง
const routes = RouterManager.getRoutes();
const menuItems = routes
.filter(route => !route.hidden)
.map(route => ({
path: route.path,
title: route.title
}));คุณสมบัติของสถานะ
RouterManager มี state object ที่เก็บ state ปัจจุบัน:
RouterManager.state = {
current: null, // อ็อบเจ็กต์เส้นทางปัจจุบัน
previous: null, // อ็อบเจ็กต์เส้นทางก่อนหน้า
initialized: false, // สถานะการเริ่มต้น
loading: false, // สถานะการโหลด
error: null, // อ็อบเจ็กต์ข้อผิดพลาด (ถ้ามี)
disabled: false // สถานะปิดการทำงาน
}ตัวอย่างการใช้งาน:
// ตรวจสอบว่าถูกเริ่มต้นหรือยัง
if (RouterManager.state.initialized) {
console.log('Router พร้อมทำงาน');
}
// ตรวจสอบสถานะการโหลด
if (RouterManager.state.loading) {
console.log('กำลังนำทาง...');
}
// ตรวจสอบเส้นทางปัจจุบัน
console.log('เส้นทางปัจจุบัน:', RouterManager.state.current?.path);
console.log('เส้นทางก่อนหน้า:', RouterManager.state.previous?.path);
// ตรวจสอบข้อผิดพลาด
if (RouterManager.state.error) {
console.error('เกิดข้อผิดพลาดใน Router:', RouterManager.state.error);
}ตัวอย่างการใช้งานจริง
ตัวอย่างที่ 1: ตั้งค่า SPA พื้นฐาน
// เริ่มต้น router
await RouterManager.init({
enabled: true,
mode: 'history',
base: '/'
});
// ลงทะเบียนเส้นทาง
RouterManager.register('/', {
template: 'home',
title: 'หน้าแรก'
});
RouterManager.register('/about', {
template: 'about',
title: 'เกี่ยวกับเรา'
});
RouterManager.register('/contact', {
template: 'contact',
title: 'ติดต่อเรา'
});
// ตัวอย่างลิงก์ HTML
// <a href="/about">เกี่ยวกับเรา</a>
// <a href="/contact">ติดต่อเรา</a>ตัวอย่างที่ 2: เส้นทางที่ต้องยืนยันตัวตน
// เริ่มต้นโดยเปิดใช้ระบบยืนยันตัวตน
await RouterManager.init({
enabled: true,
auth: {
enabled: true,
defaultRequireAuth: false,
publicPaths: ['/login', '/register', '/'],
redirects: {
unauthorized: '/login'
}
}
});
// เส้นทางสาธารณะ
RouterManager.register('/', {
template: 'home',
title: 'หน้าแรก',
public: true
});
RouterManager.register('/login', {
template: 'login',
title: 'เข้าสู่ระบบ',
public: true
});
// เส้นทางที่ต้องยืนยันตัวตน
RouterManager.register('/dashboard', {
template: 'dashboard',
title: 'แดชบอร์ด',
requireAuth: true
});
RouterManager.register('/profile', {
template: 'profile',
title: 'โปรไฟล์ของฉัน',
requireAuth: true
});
// เส้นทางเฉพาะผู้ดูแลระบบ
RouterManager.register('/admin', {
template: 'admin',
title: 'พื้นที่ผู้ดูแลระบบ',
requireAuth: true,
roles: ['admin']
});ตัวอย่างที่ 3: เส้นทางไดนามิกพร้อมพารามิเตอร์
// เส้นทางโปรไฟล์ผู้ใช้
RouterManager.register('/user/:id', {
template: 'user-profile',
title: 'โปรไฟล์ผู้ใช้'
});
// เส้นทางรายละเอียดสินค้า
RouterManager.register('/product/:category/:id', {
template: 'product-detail',
title: 'รายละเอียดสินค้า'
});
// เส้นทางบทความบล็อก
RouterManager.register('/blog/:slug', {
template: 'blog-post',
title: 'บทความบล็อก'
});
// นำทางพร้อมพารามิเตอร์
RouterManager.navigate('/user/:id', { id: 123 });
RouterManager.navigate('/product/:category/:id', {
category: 'electronics',
id: 'laptop-001'
});
RouterManager.navigate('/blog/:slug', { slug: 'hello-world' });
// ดึงค่าพารามิเตอร์ภายในเทมเพลต
const userId = RouterManager.getParam('id');
const category = RouterManager.getParam('category');
const productId = RouterManager.getParam('id');ตัวอย่างที่ 4: Guard สำหรับการนำทางแบบกำหนดเอง
// Guard แสดงสถานะกำลังโหลด
RouterManager.beforeEach((to, from, next) => {
// แสดงสถานะกำลังโหลด
document.body.classList.add('loading');
next();
});
RouterManager.afterEach(() => {
// ซ่อนสถานะกำลังโหลด
document.body.classList.remove('loading');
});
// Guard ตรวจสอบสิทธิ์
RouterManager.beforeEach(async (to, from, next) => {
if (to.route?.requireAuth) {
const authManager = Now.getManager('auth');
if (!authManager.isAuthenticated()) {
next('/login');
return;
}
if (to.route.roles) {
const hasPermission = await authManager.hasAnyRole(to.route.roles);
if (!hasPermission) {
next('/forbidden');
return;
}
}
}
next();
});
// ติดตามข้อมูลการใช้งาน
RouterManager.afterEach((to, from) => {
// บันทึกข้อมูล Google Analytics
if (window.gtag) {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.path,
page_title: to.route?.title
});
}
// ระบบวิเคราะห์แบบกำหนดเอง
trackPageView(to.path);
});ตัวอย่างที่ 5: ตั้งค่าโหมด Hash
// เริ่มต้นในโหมด hash
await RouterManager.init({
enabled: true,
mode: 'hash', // URL จะอยู่ในรูปแบบ /#/path
base: '/'
});
// ลงทะเบียนเส้นทาง (เหมือนโหมด history)
RouterManager.register('/home', 'home');
RouterManager.register('/about', 'about');
// ตัวอย่าง URL:
// ตัวอย่าง: http://example.com/#/home
// ตัวอย่าง: http://example.com/#/about
// การนำทางทำงานเหมือนกัน
RouterManager.navigate('/home');ตัวอย่างที่ 6: Handler 404 แบบกำหนดเอง
await RouterManager.init({
enabled: true,
notFound: {
behavior: 'render',
template: 'custom-404',
title: '404 - ไม่พบหน้า',
preserveUrl: true,
customHandler: (path) => {
console.log('404 - ไม่พบเส้นทาง:', path);
// บันทึกสถิติไปยังระบบวิเคราะห์
if (window.gtag) {
gtag('event', 'page_not_found', {
page_path: path
});
}
// แนะนำหน้าที่ใกล้เคียง
suggestSimilarPages(path);
}
}
});Events และ Callbacks
RouterManager ทำงานร่วมกับ EventManager โดย emit events ต่างๆ:
router:before-navigate
เกิดก่อน navigation เริ่มต้น
Now.getManager('event').on('router:before-navigate', (data) => {
console.log('Navigating to:', data.path);
console.log('Parameters:', data.params);
});router:after-navigate
เกิดหลัง navigation สำเร็จ
Now.getManager('event').on('router:after-navigate', (data) => {
console.log('นำทางเสร็จสิ้น');
console.log('เส้นทางปัจจุบัน:', data.to);
console.log('เส้นทางก่อนหน้า:', data.from);
});router:error
เกิดเมื่อมี error ระหว่าง navigation
Now.getManager('event').on('router:error', (error) => {
console.error('เกิดข้อผิดพลาดระหว่างการนำทาง:', error);
});router:not-found
เกิดเมื่อไม่พบ route
Now.getManager('event').on('router:not-found', (data) => {
console.log('ไม่พบเส้นทาง:', data.path);
});router:auth-redirect
เกิดเมื่อถูก redirect เนื่องจาก authentication
Now.getManager('event').on('router:auth-redirect', (data) => {
console.log('มีการ redirect เนื่องจากสิทธิ์การใช้งาน:', data.reason);
console.log('จาก:', data.from);
console.log('ไปยัง:', data.to);
});แนวทางปฏิบัติที่ดี
✓ ควรทำ (Do's)
- ✓ ใช้ history mode สำหรับ production (URLs สวยกว่า)
- ✓ ใช้ hash mode สำหรับ development หรือเมื่อไม่สามารถตั้งค่า server ได้
- ✓ กำหนด base path ให้ถูกต้องเมื่อแอปไม่ได้อยู่ที่ root
- ✓ ใช้ route parameters สำหรับ dynamic content
- ✓ ตั้งค่า 404 handler ที่เหมาะสม
- ✓ ใช้ guards สำหรับ authentication และ authorization
- ✓ แยก public routes และ protected routes ให้ชัดเจน
- ✓ ตั้งชื่อ template ให้สื่อความหมาย
- ✓ ใช้ async/await สำหรับ navigation และ guards
- ✓ จัดการข้อผิดพลาดอย่างเหมาะสม
- ✓ ใช้ Events สำหรับ tracking และ analytics
✗ ไม่ควรทำ (Don'ts)
- ✗ อย่า hard-code URLs ในโค้ด ใช้
navigate()แทน - ✗ อย่าลืมตั้งค่า server rewrite สำหรับ history mode
- ✗ อย่าใช้
window.location.hrefสำหรับ navigation - ✗ อย่าลืม call
next()ใน beforeEach guards - ✗ อย่า block navigation นานเกินไปใน guards
- ✗ อย่าใช้ query string สำหรับ sensitive data
- ✗ อย่าสร้าง circular redirects
- ✗ อย่าลืม handle async operations ใน guards
- ✗ อย่าลืมจัดการงาน async ภายใน guards ให้เรียบร้อย
เคล็ดลับและข้อแนะนำ
-
Server Configuration สำหรับ History Mode
- Apache: ใช้
.htaccessredirect ทุก requests ไปที่index.html - Nginx: ใช้
try_files $uri $uri/ /index.html
- Apache: ใช้
-
Route Organization
- จัดกลุ่ม routes ตาม feature
- ใช้ nested routes สำหรับ layout ที่ซับซ้อน
- แยก public และ protected routes
-
Performance
- ใช้ lazy loading สำหรับ templates ขนาดใหญ่
- Cache templates ที่ใช้บ่อย
- Optimize guards ให้ทำงานเร็ว
-
Security
- ใช้ auth guards สำหรับ protected routes
- Validate parameters ใน guards
- ไม่ trust client-side routing เพียงอย่างเดียว
ข้อผิดพลาดที่พบบ่อย
❌ ผิด: ใช้ window.location.href สำหรับ navigation
// ❌ ผิด - ทำให้ page reload
document.querySelector('#link').addEventListener('click', () => {
window.location.href = '/about';
});✅ ถูก: ใช้ RouterManager.navigate()
// ✅ ถูก - SPA navigation
document.querySelector('#link').addEventListener('click', () => {
RouterManager.navigate('/about');
});❌ ผิด: ลืม call next() ใน guard
// ❌ ผิด - Navigation จะหยุดทำงาน
RouterManager.beforeEach((to, from, next) => {
console.log('Navigating...');
// ลืม call next()
});✅ ถูก: Call next() เสมอ
// ✅ ถูก
RouterManager.beforeEach((to, from, next) => {
console.log('Navigating...');
next(); // ต้อง call
});❌ ผิด: History mode โดยไม่ตั้งค่า server
// ❌ ผิด - Direct URL จะเกิด 404 error
await RouterManager.init({
mode: 'history'
});
// แต่ไม่มี server rewrite rule✅ ถูก: ตั้งค่า server rewrite
# .htaccess for Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>// ✅ ถูก - ตอนนี้ direct URLs จะทำงาน
await RouterManager.init({
mode: 'history'
});❌ ผิด: Blocking guard นานเกินไป
// ❌ ผิด - Guard ใช้เวลานานมาก
RouterManager.beforeEach(async (to, from, next) => {
// เรียก API ที่ช้า
await fetch('/api/slow-endpoint');
await new Promise(resolve => setTimeout(resolve, 5000));
next();
});✅ ถูก: ปรับปรุงประสิทธิภาพของ guard
// ✅ ถูก - ทำเฉพาะงานที่จำเป็น
RouterManager.beforeEach(async (to, from, next) => {
// ตรวจสอบแคชก่อน
if (isCached()) {
next();
return;
}
// ตรวจสอบแบบรวดเร็วเท่านั้น
const isValid = await quickCheck();
next(isValid ? undefined : '/error');
});❌ ผิด: ไม่ handle parameters ที่ missing
// ❌ ผิด - อาจเกิด undefined error
RouterManager.register('/user/:id', {
template: 'user-profile'
});
// ใน template
const userId = RouterManager.getParam('id');
loadUser(userId); // userId อาจเป็น undefined✅ ถูก: Validate parameters
// ✅ ถูก - ตรวจสอบก่อนใช้งาน
RouterManager.register('/user/:id', {
template: 'user-profile'
});
const userId = RouterManager.getParam('id');
if (!userId) {
RouterManager.navigate('/404');
} else {
loadUser(userId);
}
// หรือใช้ guard
RouterManager.beforeEach((to, from, next) => {
if (to.path.startsWith('/user/') && !to.params.id) {
next('/404');
} else {
next();
}
});การพิจารณาด้านประสิทธิภาพ
การเพิ่มประสิทธิภาพ
-
Template Caching
// เทมเพลตจะถูกแคชอัตโนมัติ // แต่สามารถ preload เพิ่มเติมได้ RouterManager.register('/dashboard', { template: 'dashboard', preload: true }); -
Lazy Loading
// โหลดเทมเพลตเฉพาะเมื่อจำเป็นต้องใช้เท่านั้น RouterManager.register('/heavy-page', { template: async () => { const module = await import('./templates/heavy-page.js'); return module.template; } }); -
Route Guards Optimization
// แคชผลการตรวจสอบสิทธิ์ let authCache = null; let cacheTime = 0; RouterManager.beforeEach(async (to, from, next) => { const now = Date.now(); // ใช้ข้อมูลในแคชถ้ายังไม่เกิน 5 วินาที if (authCache && (now - cacheTime < 5000)) { next(authCache ? undefined : '/login'); return; } // ตรวจสอบสิทธิ์ authCache = await checkAuth(); cacheTime = now; next(authCache ? undefined : '/login'); });
ข้อควรระวัง
- ความถี่ในการนำทาง: หลีกเลี่ยงการเรียก navigate บ่อยเกินไป (ใช้ debounce หากจำเป็น)
- ความซับซ้อนของ guard: guard ควรทำงานรวดเร็ว (< 100 มิลลิวินาที)
- ขนาดเทมเพลต: ใช้ lazy loading สำหรับเทมเพลตขนาดใหญ่
- การรั่วไหลของหน่วยความจำ: ยกเลิกการลงทะเบียน event listener เมื่อไม่ใช้งาน
การพิจารณาด้านความปลอดภัย
ข้อควรพิจารณาด้านความปลอดภัย
-
ความปลอดภัยฝั่งไคลเอนต์
// ❌ อย่าพึ่งพา client-side protection เพียงอย่างเดียว RouterManager.register('/admin', { requireAuth: true, roles: ['admin'] }); // ✅ ต้องมี server-side validation ด้วย // ฝั่งเซิร์ฟเวอร์ต้องตรวจสอบการยืนยันและการอนุญาตสิทธิ์ -
การป้องกัน XSS
// ✅ RouterManager escape HTML โดยอัตโนมัติ // แต่ระวังเมื่อใช้ innerHTML โดยตรง // ✓ ปลอดภัย const params = RouterManager.getParams(); element.textContent = params.name; // ✗ อันตราย element.innerHTML = params.name; // อาจมี XSS -
การตรวจสอบพารามิเตอร์
// ✅ ตรวจสอบความถูกต้องของพารามิเตอร์ RouterManager.beforeEach((to, from, next) => { if (to.params.id) { // ตรวจสอบรูปแบบของ ID if (!/^\d+$/.test(to.params.id)) { next('/404'); return; } } next(); }); -
การป้องกัน CSRF
// สำหรับฟอร์มที่ส่งผ่าน router // ใช้ CSRF token RouterManager.beforeEach(async (to, from, next) => { if (to.route?.requiresCSRF) { const token = await getCSRFToken(); to.params._csrf = token; } next(); });
ความเข้ากันได้กับเบราว์เซอร์
เบราว์เซอร์ที่รองรับ
| เบราว์เซอร์ | เวอร์ชัน | โหมดที่รองรับ |
|---|---|---|
| Chrome | 2 เวอร์ชันล่าสุด | Hash, History |
| Firefox | 2 เวอร์ชันล่าสุด | Hash, History |
| Safari | 2 เวอร์ชันล่าสุด | Hash, History |
| Edge | 2 เวอร์ชันล่าสุด | Hash, History |
| iOS Safari | iOS 12 ขึ้นไป | Hash, History |
| Chrome Android | เวอร์ชันล่าสุด | Hash, History |
Polyfill ที่ควรใช้
สำหรับเบราว์เซอร์รุ่นเก่า:
<!-- Polyfill สำหรับ History API -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=URL,history"></script>ปัญหาที่ทราบ
-
iOS Safari Back Button: อาจมีปัญหากับ history.back() ใน iOS Safari รุ่นเก่า
- วิธีแก้: ใช้ hash mode หรืออัพเดท iOS
-
IE11: ไม่รองรับ History API บางส่วน
- วิธีแก้: ใช้ hash mode หรือ polyfill
คลาสและเมธอดที่เกี่ยวข้อง
TemplateManager
- ใช้ร่วมกับ RouterManager ในการโหลดและแสดง templates
- เอกสาร TemplateManager
AuthManager
- ใช้สำหรับ authentication guards
- ผสานการทำงานกับ protected routes
- เอกสาร AuthManager
EventManager
- RouterManager ส่งอีเวนต์ผ่าน EventManager
- เอกสาร EventManager
HistoryManager
- จัดการ browser history
- ดูแลการนำทางย้อนกลับ/ไปข้างหน้า
บันทึกเพิ่มเติม
เวอร์ชันที่รองรับ
- Now.js v1.0+
- ES6+ JavaScript
การเปลี่ยนแปลงที่มีผลกระทบ
v1.0:
- เปลี่ยน API จาก callback-based เป็น Promise-based
init()ต้อง await- Guards รองรับ async/await
ฟีเจอร์ที่เลิกใช้
RouterManager.route()- ใช้register()แทนRouterManager.go()- ใช้navigate()แทน
ข้อจำกัด
- Router ทำงานฝั่ง client-side เท่านั้น
- ต้องตั้งค่า server สำหรับ history mode
- SEO: ใช้ SSR หรือ prerendering สำหรับ SEO ที่ดีขึ้น
- การใช้ query parameters มี length limit ตาม browser
อ้างอิง API
รายการเมธอดทั้งหมดแบบย่อ:
| เมธอด | พารามิเตอร์ | ค่าที่ส่งกลับ | คำอธิบาย |
|---|---|---|---|
init(options) |
Object | Promise | เริ่มต้น router |
register(path, config) |
String, Object | void | ลงทะเบียน route |
navigate(path, params, options) |
String, Object, Object | Promise | นำทางไปยังเส้นทาง |
beforeEach(guard) |
Function | Function | เพิ่ม before guard |
afterEach(guard) |
Function | Function | เพิ่ม after guard |
getPath(includeQuery) |
Boolean | String | รับเส้นทางปัจจุบัน |
getQuery() |
- | String | รับ query string |
getParams() |
- | Object | รับพารามิเตอร์ทั้งหมด |
getParam(name) |
String | String | รับพารามิเตอร์ตามชื่อ |
hasRoute(path) |
String | Boolean | ตรวจสอบว่ามีเส้นทาง |
getRoute(path) |
String | Object | รับการตั้งค่าเส้นทาง |
getRoutes() |
- | Array | รับเส้นทางทั้งหมด |
ตัวอย่างโปรเจคเต็มรูปแบบ
// main.js - SPA พร้อมการยืนยันตัวตน
(async () => {
// เริ่มต้น Now.js พร้อม Router
await Now.init({
router: {
enabled: true,
mode: 'history',
base: '/',
auth: {
enabled: true,
defaultRequireAuth: false,
publicPaths: ['/login', '/register', '/'],
redirects: {
unauthorized: '/login',
afterLogin: '/dashboard'
}
},
notFound: {
template: '404',
title: 'ไม่พบหน้า'
}
}
});
const router = Now.getManager('router');
// เส้นทางสาธารณะ
router.register('/', {
template: 'home',
title: 'หน้าแรก',
public: true
});
router.register('/login', {
template: 'login',
title: 'เข้าสู่ระบบ',
public: true
});
router.register('/register', {
template: 'register',
title: 'สมัครสมาชิก',
public: true
});
// เส้นทางที่ต้องยืนยันตัวตน
router.register('/dashboard', {
template: 'dashboard',
title: 'แดชบอร์ด',
requireAuth: true
});
router.register('/profile', {
template: 'profile',
title: 'โปรไฟล์ของฉัน',
requireAuth: true
});
router.register('/settings', {
template: 'settings',
title: 'การตั้งค่า',
requireAuth: true
});
// เส้นทางสำหรับผู้ดูแลระบบ
router.register('/admin', {
template: 'admin',
title: 'แผงควบคุมผู้ดูแล',
requireAuth: true,
roles: ['admin']
});
// เส้นทางแบบไดนามิก
router.register('/user/:id', {
template: 'user-profile',
title: 'โปรไฟล์ผู้ใช้',
requireAuth: true
});
// Guard ระดับ global
router.beforeEach((to, from, next) => {
console.log(`กำลังนำทาง: ${from.path} -> ${to.path}`);
next();
});
router.afterEach((to, from) => {
// การวิเคราะห์ข้อมูล
if (window.gtag) {
gtag('config', 'GA_ID', {
page_path: to.path
});
}
// เลื่อนกลับไปด้านบนสุด
window.scrollTo(0, 0);
});
})();