• בלוג
  • בואו נבנה Router עבור Single Page Application כדי להבין איך זה עובד

בואו נבנה Router עבור Single Page Application כדי להבין איך זה עובד

פוסט זה כולל טיפ קצר בנושא פיתוח Front End. אם אתם רוצים ללמוד יותר לעומק על פיתוח Front End מהבסיס ועד הנושאים המתקדמים תשמחו לשמוע שבניתי קורס וידאו מקיף בנושא זה הכולל מעל 50 שיעורי וידאו והמון תרגול מעשי.
למידע נוסף והצטרפות לקורס בקרו בדף קורס Front End באתר.
 

דרך טובה להבין איך מנגנון מסוים עובד היא לבנות אותו, ובמיוחד נכון הדבר כשמדובר במנגנון מאוד בסיסי בהרבה ספריות מודרניות היום. בפוסט היום נדבר על ניווט בין דפים ביישומי Single Page Application ונבנה מימוש פשוט למנגנון זה.

1. מה בונים

המטרה לבנות יישום עמוד יחיד שיכול לנווט בין דפים בצד הלקוח בלבד, ומשתמש ב Hash כדי לציין היכן נמצאים באפליקציה. אפשר לראות את הקוד המלא בקישור כאן:
https://codepen.io/ynonp/project/editor/XVvNYO/

ואת היישום בפעולה כאן (לחצו על הלינקים כדי לטעון את הדפים):

2. הנתב

הרכיב הראשון והמרכזי ביישום הוא הנתב. רכיב זה מחזיק רשימה של איזה דפים יש ביישום ואת הלוגיקה שמטפלת בשינוי עמוד. בכל פעם שמזהים שמשתנה ה hash מופעלת הפונקציה show כדי להציג את הדף החדש. זה קוד הפונקציה:

  async show(pageName) {
    const page = this.routes[pageName];
    await page.load();
    this.el.innerHTML = '';
    page.show(this.el);
  }

הפונקציה מקבלת את שם העמוד, דרכו מגיעה לאוביקט שמייצג את העמוד ואז מגיע החלק המעניין: הקריאה ל load. הפונקציה load של עמוד אחראית על טעינת תוכן העמוד, דרך Ajax או בכל דרך אחרת. הטעינה עשויה להיות אסינכרונית ולכן פונקציה זו מחזירה Promise. הפקודה await גורמת להמתנה עד שה Promise מתבצע, כך שהשורות הבאות יופעלו רק אחרי שהעמוד סיים להיטען. מיד נמשיך למבנה של עמוד ונראה איך load עצמה בנויה.

לאחר שהעמוד סיים להיטען מוחקים את התוכן הראשי ונותנים לעמוד לצייר את עצמו שם, והנה עברנו לעמוד החדש.

קוד הנתב המלא נראה כך:

class Router {
  constructor(routes, el) {
    this.routes = routes;
    this.el = el;
    window.onhashchange = this.hashChanged.bind(this);
    this.hashChanged();
  }

  async hashChanged(ev) {
    if (window.location.hash.length > 0) {
      const pageName = window.location.hash.substr(1);
      this.show(pageName);
    } else if (this.routes['#default']) {
      this.show('#default');
    }
  }

  async show(pageName) {
    const page = this.routes[pageName];
    await page.load();
    this.el.innerHTML = '';
    page.show(this.el);
  }
}

3. עמוד ביישום

עמוד הוא משהו שיודע לצייר את עצמו לתוך אלמנט. במקרה שלנו יש רק סוג עמוד אחד, המייצג תוכן HTML שנקרא בבקשת Ajax. ביישומים כלליים יותר אפשר לדמיין סוגים שונים של עמודים שמציירים את עצמם בצורות שונות או טוענים מידע ממקורות שונים.

קוד המחלקה המלא עבור Page מאוד פשוט וכולל בסך הכל 3 פונקציות קצרות:

class Page {
  constructor(url) {
    this.url = 'views/' + url;
  }

  load() {
    return $.get(this.url).then(res => this.html = res);
  }  

  show(el) {
    el.innerHTML = this.html;
  }
}

הבנאי מקבל url ושומר אותו כדי שאפשר יהיה לטעון משם את המידע בהמשך. הפונקציה load טוענת את המידע ב Ajax (באמצעות ספריית jQuery), ומחזירה Promise כך שהנתב יכול לחכות עד שהמידע מסיים להיטען. הפונקציה show מציגה את העמוד באמצעות כתיבת התוכן לתוך האלמנט.

החזרת ה Promise כאן היא המפתח לטעינה האסינכרונית ולעבודה המתואמת של Page ו Router. אוביקט Page מחזיר Promise, וה Router ממתין דרך ה Prmomise עד שקורא ל show.

4. בונוס: ארגון עמודים יחד ב Layout

לתוך המבנה הזה קל להכניס סוג נוסף של דף שנקרא Layout, דף המייצג קבוצה של דפים וטוען את כולם יחד. ההבדל בינו לבין Page יתבטא בעיקר בפונקציות הטעינה וההצגה. כדי לטעון Layout נרצה לחכות לטעינת כל הדפים שבתוכו:

  load() {
    return Promise.all(this.pages.map(page => page.load()));
  }

וכדי להציג Layout נצטרך להציג את כל הדפים שבתוכו אחד אחרי השני:

  show(el) {
    for (let page of this.pages) {
      const div = document.createElement('div');
      page.show(div);
      el.appendChild(div);
    }
  }

כעת בכל מקום שמישהו מצפה לקבל Page אפשר להעביר Layout במקום בלי לפגוע בהתנהגות היישום.

5. יצירת טבלת העמודים

הקובץ main.js יוצר את הנתב ואת טבלת העמודים והוא שמתחיל את כל היישום. שימו לב ששני הדפים הראשונים הם Layout והשלישי הוא Page, וזה בסדר גמור כי שתי המחלקות מתנהגות באותו אופן:

const r = new Router(
  {
    about: new Layout(new Page('menu.html'), new Page('about.html')),
    home: new Layout(new Page('menu.html'), new Page('home.html')),
    '#default': new Page('menu.html'),
  },
  document.querySelector('main')
);