Now.js Framework Documentation

Now.js Framework Documentation

RouterManager - ตัวจัดการเส้นทาง

TH 29 Nov 2025 03:12

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/:id
  • config (Object|string) - การตั้งค่าเส้นทางหรือชื่อเทมเพลต

วัตถุตั้งค่า (Configuration Object):

  • template (string) - พาธของเทมเพลตหรือ HTML string
  • title (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'
        }
    }
});

นำทางไปยังเส้นทางที่ระบุ

พารามิเตอร์:

  • path (string) - เส้นทางปลายทาง
  • params (Object, optional) - พารามิเตอร์ของเส้นทาง
  • options (Object, optional) - ตัวเลือกเพิ่มเติมสำหรับการนำทาง
    • replace (boolean) - ใช้ replaceState แทน pushState
    • skipGuards (boolean) - ข้ามการทำงานของ guard
    • preserveQuery (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-001

beforeEach(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')); // ผลลัพธ์: electronics

getParams()

รับ 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-001

getParam(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 ให้เรียบร้อย

เคล็ดลับและข้อแนะนำ

  1. Server Configuration สำหรับ History Mode

    • Apache: ใช้ .htaccess redirect ทุก requests ไปที่ index.html
    • Nginx: ใช้ try_files $uri $uri/ /index.html
  2. Route Organization

    • จัดกลุ่ม routes ตาม feature
    • ใช้ nested routes สำหรับ layout ที่ซับซ้อน
    • แยก public และ protected routes
  3. Performance

    • ใช้ lazy loading สำหรับ templates ขนาดใหญ่
    • Cache templates ที่ใช้บ่อย
    • Optimize guards ให้ทำงานเร็ว
  4. 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();
    }
});

การพิจารณาด้านประสิทธิภาพ

การเพิ่มประสิทธิภาพ

  1. Template Caching

    // เทมเพลตจะถูกแคชอัตโนมัติ
    // แต่สามารถ preload เพิ่มเติมได้
    RouterManager.register('/dashboard', {
       template: 'dashboard',
       preload: true
    });
  2. Lazy Loading

    // โหลดเทมเพลตเฉพาะเมื่อจำเป็นต้องใช้เท่านั้น
    RouterManager.register('/heavy-page', {
       template: async () => {
           const module = await import('./templates/heavy-page.js');
           return module.template;
       }
    });
  3. 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 เมื่อไม่ใช้งาน

การพิจารณาด้านความปลอดภัย

ข้อควรพิจารณาด้านความปลอดภัย

  1. ความปลอดภัยฝั่งไคลเอนต์

    // ❌ อย่าพึ่งพา client-side protection เพียงอย่างเดียว
    RouterManager.register('/admin', {
       requireAuth: true,
       roles: ['admin']
    });
    
    // ✅ ต้องมี server-side validation ด้วย
    // ฝั่งเซิร์ฟเวอร์ต้องตรวจสอบการยืนยันและการอนุญาตสิทธิ์
  2. การป้องกัน XSS

    // ✅ RouterManager escape HTML โดยอัตโนมัติ
    // แต่ระวังเมื่อใช้ innerHTML โดยตรง
    
    // ✓ ปลอดภัย
    const params = RouterManager.getParams();
    element.textContent = params.name;
    
    // ✗ อันตราย
    element.innerHTML = params.name; // อาจมี XSS
  3. การตรวจสอบพารามิเตอร์

    // ✅ ตรวจสอบความถูกต้องของพารามิเตอร์
    RouterManager.beforeEach((to, from, next) => {
       if (to.params.id) {
           // ตรวจสอบรูปแบบของ ID
           if (!/^\d+$/.test(to.params.id)) {
               next('/404');
               return;
           }
       }
       next();
    });
  4. การป้องกัน 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>

ปัญหาที่ทราบ

  1. iOS Safari Back Button: อาจมีปัญหากับ history.back() ใน iOS Safari รุ่นเก่า

    • วิธีแก้: ใช้ hash mode หรืออัพเดท iOS
  2. IE11: ไม่รองรับ History API บางส่วน

    • วิธีแก้: ใช้ hash mode หรือ polyfill

คลาสและเมธอดที่เกี่ยวข้อง

TemplateManager

AuthManager

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() แทน

ข้อจำกัด

  1. Router ทำงานฝั่ง client-side เท่านั้น
  2. ต้องตั้งค่า server สำหรับ history mode
  3. SEO: ใช้ SSR หรือ prerendering สำหรับ SEO ที่ดีขึ้น
  4. การใช้ 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);
    });
})();