• בלוג
  • הוספת תקשורת זמן-אמת ליישום Angular

הוספת תקשורת זמן-אמת ליישום Angular

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

תקשורת דו-כיוונית באמצעות Web Sockets יכולה לשדרג כל יישום ולאפשר שיתוף מידע מיידי בין הגולשים. במדריך זה אדגים את השימוש ב Web Sockets כדי להוסיף תקשורת זמן-אמת לאפליקציית Angular. אנו נבנה לוח הצבעות המתעדכן בזמן אמת, בו כל משתמש יכול לבחור מבין מגוון אפשרויות ולראות במה בחרו המשתמשים האחרים. אנו נחבר את לוח ההצבעות ל Facebook API, ממנו נשלוף את תמונות המצביעים להצגה על המסך.

1. נקודת ההתחלה

במדריך זה אנו הולכים להמתקד בתקשורת זמן-אמת. כדי לחסוך זמן בחרתי להתחיל מלוח הצבעה פשוט למדי הבנוי ב Angular ללא צד-שרת. כך נראה היישום:

See the Pen vEjJLz by ynonp (@ynonp) on CodePen.

היישום מתחיל עם Controller אחד עבור ההצבעות ו Factory אחד עבור כל מועמד, כאשר המידע על כל מועמד להצבעה כולל את שמו ומערך של מצביעים. בינתיים כל הצבעה מוסיפה איבר קבוע (המספר 1) למערך המצביעים, ובהמשך נעדכן ערך זה להיות מזהה המשתמש שהצביע, ונחליף את הריבוע הכחול בתמונת פייסבוק. 
בתצוגה אנו מציירים כל אחת מהאפשרויות ועבור כל מערך מצביעים מציירים סימנים שיראו לנו כמה הצבעות כל אפשרות קיבלה.

היישום כולל אך ורק קוד צד-לקוח. ההצבעות אינן נשמרות ולכן בכל רענון של הדף כל ההצבעות מתאפסות.

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

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

מוכנים? יאללה, נצא לדרך.

2. Firebase על קצה המזלג

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

socket server architecture

כל גולש מחובר לשני שרתים שונים: גם לשרת ה Web Sockets וגם לשרת ה Application. שני שרתים אלו יכולים להיות מחוברים באותה הרשת ולדבר ביניהם דרך בסיס נתונים או Message Queue, או בכל פרוטוקול תקשורת אחר שיבחרו. שירות Firebase אתו נעבוד בפוסט זה יודע לתקשר עם שרת שלכם דרך REST API. 

Firebase הינו שירות צד-שרת המנהל עבורכם תקשורת דו-כיוונית עם הלקוחות שלכם. בנוסף, הם מספקים ספריית JavaScript שעוטפת את העבודה עם Web Sockets בממשק קצת יותר נוח, ועבור מפתחי אנגולר יש ספריה נוספת שעוטפת את הספריה הקודמת ומתאימה את הממשק לעבודה עם אנגולר. הדבר הגדול כאן הוא Data Binding תלת-כיווני, כלומר יהיה לנו אובייקט שקשור גם לשרת Firebase וגם ל DOM. כל שינוי יעדכן גם את העמוד וגם את השרת.

באפליקציית ההצבעות שלנו אין Application Server וכל המידע יישמר על השרת של Firebase. אופן העבודה של Firebase מורכב מעץ אובייקטים, כאשר אנו יכולים להוסיף או למחוק אובייקטים מהעץ או מענפים מסוימים שלו. כל נתיב בעץ הינו אובייקט אשר נגיש באמצעות URL מסוים. 
כך למשל בהנחה שיש לנו אובייקט חיבור ל Firebase בשם fb (בהמשך נראה כיצד ליצור אחד), נוכל להפעיל את הקוד הבא כדי לעדכן מידע על אחד המועמדים להצבעה:

var data = fb.child('/candidates/cat');
data.set({
    name: 'Evil Cat',
    goal: 'Take over the world',
    moto: 'JavaScript is not funny'
});

הפקודה child מחזירה נתיב בעץ, והפקודה set שומרת מידע עבור נתיב זה. כל המידע על שרת Firebase נשמר לפי נתיבים. 

אנחנו ניצור נתיב שנקרא votes עבור האפליקציה שלנו ובתוכו שלושה תתי נתיבים עבור כל אחת מאפשרויות ההצבעה. בכל נתיב כזה נחזיק רשימה של כל מי שבחר באפשרות זו. הרשימה תמומש כאובייקט קשור תלת-כיווני. כך, בכל פעם שמישהו ממחשב אחר יצביע, המערך אצלנו יקבל את עדכון ההצבעה מהשרת ויגרום לעדכון של ה DOM.

3. עדכון הקוד לצורך שמירת המידע ב Firebase

שירות Firebase עובד במודל פרימיום, עבור יישומים קטנים ומשחקים אין צורך לשלם וכשהיישום מתחיל לגדול כך גם החשבון. נתחיל בפתיחת חשבון בחינם באתר החברה בכתובת:
https://www.firebase.com/

נוסיף לקוד את הספריות הדרושות לעבודה עם Firebase. ספריית firebase.js וספריית angularfire שמספקת ממשק נוח לעבודה עם Firebase מתוך Angular:

https://cdn.firebase.com/js/client/2.2.0/firebase.js
https://cdn.firebase.com/libs/angularfire/0.9.2/angularfire.min.js

נעדכן את האפליקציה שלנו כך שתשתמש ב Firebase Provider באמצעות הוספת Firebase לרשימת המודולים בהם אנו תלויים בעת הגדרת המודול:

angular.module('firebaseVotingApp', ['firebase']);

לסיום נעדכן את הקוד של Candidate כך שישמור את המידע על שרת Firebase:

angular.module('firebaseVotingApp')
.factory('Candidate', ['$firebase', function ($firebase) {

	return function(name) {
		var self = this;
		var fb =  new Firebase('https://sizzling-inferno-9198.firebaseio.com/');
		var entry = fb.child('/votes/' + name);
		var sync = $firebase(entry);

		self.name = name;
		self.votes = sync.$asArray();

		self.vote = function() {
			self.votes.$add(1);				
		};
	};
}]);

כאן בוצע מספר השינויים הגדול ביותר: החיבור ל Firebase נעשה באמצעות יצירת אובייקט Firebase חדש ב JavaScript. נשים לב שאנו לא עובדים בחיבור Web Sockets ״נקי״ אלא משתמשים בממשק של Firebase שעוטף יכולת זו, מחבר אותה בקלות ל Angular ומוסיף עוד המון יכולות על בסיס התשתית. לאחר החיבור הראשוני לקחנו את הנתיב הרלוונטי באמצעות הפונקציה child ובנינו אובייקט סינכרון עבור אנגולר הנשמר במשתנה sync.

החיבור בין Firebase ל Angular נעשה באמצעות אובייקט סינכרון זה. לאובייקט שתי פונקציות מעניינות, הראשונה בה נשתמש בקוד היא $asArray, והיא מחזירה מערך המגובה בשרת, כלומר כל כתיבה למערך נשמרת אוטומטית על השרת ונשלחת לכל שאר הגולשים באתר המסונכרנים עם אותו המערך. הפונקציה השניה נקראת $asObject והיא מחזירה אובייקט המסונכרן עם השרת, כך שכל כתיבה לשדה של האובייקט נשמרת ומדווחת לגולשים האחרים. הדבר היפה כאן הוא שאין לנו צורך לשנות את קוד האיטרציה שמציג הצבעות. הפקודה ng-repeat המציגה את תוכן המערך תופעל ותתעדכן באופן מיידי ברגע שאלמנט חדש יתווסף למערך, בין אם כתוצאה מהצבעה מקומית או כתוצאה מהצבעה של גולש אחר.

הוספת איבר למערך מסונכרן מבוצעת עם הפונקציה $add של המערך המסונכרן, במקום push עבור מערך רגיל. פונקציה נוספת של המערך המסונכרן בה לא השתמשתי בדוגמא היא הפונקציה $loaded. פונקציה זו תחזיר לכם Promise שישתערך לאמת אחרי שהמידע הראשוני נטען מהשרת.

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

See the Pen ravzwm by ynonp (@ynonp) on CodePen.

4. חיבור הקוד ל Facebook API

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

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

לצורך החיבור לפייסבוק אנו נבנה אפליקציית פייסבוק חדשה שתחובר למערכת ההצבעות שלנו. לאחר התחברות לפייסבוק גלשו בבקשה לקישור:
https://developers.facebook.com/apps/

בצד ימין למעלה תראו כפתור ״יצירת אפליקציה חדשה״. לחיצה עליו פותחת דיאלוג לבחירת סוג האפליקציה. אנו נבחר באפשרות Website כמתואר באיור:

new facebook app dialog

בשלב הבא תזינו את שם האפליקציה ולאחר מכן תגיעו למסך Quick Start של פייסבוק. יש ללחוץ Skip Quick Start ולהגיע ישר למסך ההגדרות. שם יש להגדיר שהאפליקציה תוכל לעבוד עם שרת Firebase באופן הבא:

הכנסו לטאב Settings ובתוכו לטאב Basic. שם תחת הכותרת Website תמצאו שתי תיבות טקסט, האחת עבור Site url והשניה עבור Mobile site url. בתיבה הראשונה יש לרשום את הכתובת הבאה, עם החלפת המחרוזת [your-firebase-address] בכתובת שרת ה firebase שקיבלתם:

https://auth.firebase.com/v2/[your-firebase-address]/auth/facebook/callback

כך זה נראה עבור המשתמש שלי:

facebook settings to connect with firebase

נותר רק לקחת את פרטי האפליקציה ולהעביר אותם ל Firebase כדי שנוכל לשלב את חיבור הפייסבוק עם החיבור לשרת. עברו לטאב Dashboard (בצד שמאל) ורשמו לעצמכם את ה App ID שלכם ואת ה App Secret. אנו מיד נצטרך ערכים אלו.

עברו לחשבון ה Firebase שיצרתם בכתובת:
https://www.firebase.com/account/#/

בחרו את השרת שלכם ועברו לטאב Login & Auth ושם לטאב Facebook. כך זה נראה:

firebase settings to connect with facebook

הקלידו בתיבות את הפרטים שהעתקתם קודם מפייסבוק, ה App ID וה App Secret וסמנו את התיבה Enable Facebook Authentication. שימו לב שאם אתם מפעילים את היישום מ Codepen או משרת, עליכם להוסיף את הכתובת לתיבה העליונה, כפי שמוצג בצילום המסך.

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

5. עדכון קוד האפליקציה כך שתשתמש במשתמשי Facebook

תחילה נוסיף כפתורי ״התחבר״ ו״התנתק״ עבור חיבור לפייסבוק.
בעמוד ה HTML הוסיפו את הקוד הבא עבור הכפתורים, שיוצגו רק אם המשתמש מחובר או לא מחובר בהתאמה:

<div ng-show="!authData">
	<p>Please <button ng-click="login()">Login</button> To vote</p>
</div>
<div ng-show="!!authData">
	<p>Feel free to <button ng-click="logout()">Logout</button> any time</p>
</div>

נעדכן את כפתורי ההצבעה כך שרק משתמשים מחוברים יוכלו להצביע באמצעות הוספת מאפיין ng-show עבורם. כפתור הצבעה יראה כעת כך:

<button ng-show="!!authData" ng-click="party.vote()">Vote</button>

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

http://graph.facebook.com/{{fbid}}/picture

ולכן קוד הצגת התמונה ב View יראה כך:

<img ng-repeat="v in party.votes" ng-src="http://graph.facebook.com/{{v.$value}}/picture" width=40 /> 

נמשיך ליצירת Service עבור אימות המשתמש. אני אשתמש כאן ב FirebaseAuth כדי לבצע את האימות. רכיב זה נותן לנו ממשק אחיד לאימות משתמשים באמצעות פונקציית החיבור, המחזירה Promise. כך זה נראה:

angular.module('firebaseVotingApp')
.service('Auth', ['$firebaseAuth', function($firebaseAuth) {
  var self = this;
  // connect to firebase
  var ref = new Firebase('https://sizzling-inferno-9198.firebaseio.com/');
  
  // create an authentication object
  var auth = $firebaseAuth(ref);
  
  self.login = function() {
      // use authentication object to login to facebook
    return auth.$authWithOAuthPopup("facebook").then(function(authData) {
        // when login is done, print the user id
        // and save it on our Auth object so other parts of
        // the application will also be able to use the data
      console.log("Logged in as:", authData.facebook.id);
      self.authData = authData;
    }).catch(function(error) {
      console.error("Authentication failed: ", error);
    });    
  };
  
  self.logout = function() {
    auth.$unauth();
  };    
}]);

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

See the Pen xbjMRm by ynonp (@ynonp) on CodePen.

6. מניעת הצבעות כפולות

דרך קלה למניעת הצבעות כפולות היא מעבר ממבנה של מערך למבנה של אובייקט. ידוע הרי שב JavaScript אובייקט מתפקד כ Hash ולכן לא תתכן כפילות במפתחות. במעבר לאובייקט כל הצבעה תוסיף לאובייקט את מזהה הפייסבוק של המצביע בתור מפתח נוסף. במקרה של הצבעה חוזרת לא יהיה כל שינוי באובייקט.

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

נתחיל מאובייקט ההצבעות. שליפת המידע כאובייקט ולא כמערך מתבצעת באמצעות הפונקציה $asObject כמתואר:

// use an object instead of an array
self.votes = sync.$asObject();

והקוד עבור פעולת ההצבעה:

self.vote = function() {
    self.votes[auth.authData.facebook.id] = true;
    self.votes.$save();
};

השינוי ב View מציג את התמונה על פי מפתח האובייקט במקום על פי ערך במערך:

<img ng-repeat="(k,v) in party.votes" ng-src="http://graph.facebook.com/{{k}}/picture" width=40 /> 

לסיום נחליף את הנתיב ב Firebase אתו אנו עובדים לגירסא 2 בתוך אובייקט ההצבעות. כך יראה כעת החיבור:

var fb =  new Firebase('https://sizzling-inferno-9198.firebaseio.com/');
var entry = fb.child('/votes_v2/' + name);

אפשר לראות את התוצאה בצירוף הקוד המלא כאן:

See the Pen YPLBEP by ynonp (@ynonp) on CodePen.

7. הגדרת מדיניות האבטחה בשרת Firebase

אומנם יש לנו אפליקציית הצבעות עובדת אך הרשאות ברירת המחדל של Firebase ממש לא מספקות ולא יגנו עלינו מהצבעות מזויפות. נשים לב שלמרות שבצד הלקוח אנו כביכול מחברים בין מזהה הפייסבוק להצבעה, בפועל כל משתמש יכול לשנות את קוד ה JavaScript שרץ אצלו על המחשב, ומהר מאוד יכול להגיע לקוח זדוני שפשוט במקום לשלוח את מזהה הפייסבוק של עצמו ישלח מזהה פייסבוק של מישהו אחר (וכך יצביע בשם לקוח אחר).

כדי לתקן זאת נרצה לעדכן את מודל ההרשאות עבור היישום שלנו.
ניתן לשנות הרשאות ליישום בקלות דרך ממשק הבקרה של Firebase בטאב Security and Rules. הכללים מנוהלים כקובץ JSON ומגדירים מי רשאי לבצע איזו פעולה על איזה נתיב.

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

{
    "rules": {
      "votes" : {
        ".read" : true,
        ".write" : true
      },
      "votes_v2" : {        
        ".read" : true,
        ".write" : true,
        "$cand" : {
          "$vote" : {
            ".validate" : "$vote === auth.uid.replace('facebook:', '')"
          }
        }
      }
    }
}

קובץ הכללים מגדיר נתיב votes, ומאפשר לכל אחד לכתוב ולקרוא מנתיב זה. זכרו שכל מועמד להצבעה קיבל נתיב תחת votes, כך שבפועל זה אומר שכל משתמש רשאי להוסיף אפשרויות הצבעה ולקרוא את רשימת המועמדים. 
מאחר ואנו מתייחסים לכל אפשרות בתור אובייקט שמפתחותיו הם האנשים שבחרו באפשרות זו, ניתן להגדיר כללים על תוכן האובייקט. בכל מקום שיש רשימה ניתן להגדיר כללים על כל איברי הרשימה באמצעות סימן ה $, ובמקרה שלנו $candidate מייצג את כל המועמדים להצבעה (הרשימה תחת הנתיב votes), ולאחריו האובייקט $vote מייצג את כל המפתחות עבור מועמד מסוים.

כלל האימות שבחרתי דורש שכל הצבעה תוכל לכלול רק מידע עבור המשתמש המחובר הנוכחי. בפועל זה אומר שאתם יכולים להצביע רק עבור עצמכם. הוא משתמש במשתנה $vote עליו דיברנו ובמשתנה נוסף auth.uid הכולל מחרוזת זיהוי עבור המשתמש. מחרוזת זו מורכבת מספק הזיהוי, במקרה שלנו facebook, לאחריו סימן נקודותיים ולאחריו מזהה המשתמש בפייסבוק. תנאי האבטחה בודק שההצבעה נשלחה עבור המשתמש שמחובר, ותוכן ההצבעה הוא אכן מזהה המשתמש בלי התחילית. 

8. סיכום

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

לקריאה נוספת מוזמנים להכנס לתיעוד של Firebase בקישור:
https://www.firebase.com/docs/web/

ותיעוד של AngularFire, הספריה שמחברת בין Firebase ל Angular בה השתמשתי במדריך:
https://www.firebase.com/docs/web/libraries/angular/guide.html