גלריית פרסי הצילום של Google

Ilmari Heikkinen

האתר של פרס Google לצילום

לאחרונה השקנו את הקטע 'גלריה' באתר של פרס הצילום של Google. בגלריה מוצגת רשימה אינסופית של תמונות שמוצגות בגלילה. התמונות האלה אוחזו מ-Google+. רשימת התמונות מתקבלת מאפליקציית AppEngine שבה אנחנו משתמשים כדי לנהל את רשימת התמונות בגלריה. אפליקציית הגלריה פורסמה גם כפרויקט בקוד פתוח ב-Google Code.

דף הגלריה

הקצה העורפי של הגלריה הוא אפליקציית AppEngine שמשתמשת ב-Google+ API כדי לחפש פוסטים עם אחד מההאשטאגים של פרס Google לצילום (למשל, #megpp ו-#travelgpp). לאחר מכן, התמונות האלה מתווספות לרשימה של התמונות שלא עברו בקרה. פעם בשבוע, צוות התוכן שלנו עובר על רשימת התמונות שלא עברו תהליך ניהול וסימון תמונות שמפירות את הנחיות התוכן שלנו. אחרי שלוחצים על הלחצן 'ניהול', התמונות שלא סומנו מתווספות לרשימת התמונות שמוצגות בדף הגלריה.

הקצה העורפי של תהליך הניהול

ממשק הקצה של הגלריה נוצר באמצעות ספריית Closure של Google. הווידג'ט של הגלריה הוא רכיב סגירה. בחלק העליון של קובץ המקור, אנחנו אומרים ל-Closure שהקובץ הזה מספק רכיב בשם photographyPrize.Gallery ודורש את החלקים של ספריית Closure שבהם האפליקציה משתמשת:

goog.provide('photographyPrize.Gallery');

goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.net.Jsonp');
goog.require('goog.style');

בדף הגלריה יש קטע של JavaScript שמשתמש ב-JSONP כדי לאחזר את רשימת התמונות מאפליקציית AppEngine. ‏JSONP הוא פריצה פשוטה של JavaScript במקור שונה, שמחדירה סקריפט שנראה כמו jsonpcallback("responseValue"). כדי לטפל בדברים של JSONP, אנחנו משתמשים ברכיב goog.net.Jsonp בספריית Closure.

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

הצגת רשימת התמונות

שיטת התצוגה של רשימת התמונות היא די בסיסית. הוא עובר על רשימת התמונות, יוצר רכיבי HTML ואסימוני +1. בשלב הבא מוסיפים את פלח הרשימה שנוצר לרכיב הגלריה הראשי של הגלריה. בקוד שבהמשך אפשר לראות כמה מוסכמות של Closure compiler. שימו לב להגדרות הסוגים בתגובה של JSDoc ולחשיפה @private. ל-methods פרטיים יש קו תחתון (_) אחרי השם שלהם.

/**
 * Displays images in imageList by putting them inside the section element.
 * Edits image urls to scale them down to imageSize x imageSize bounding
 * box.
 *
 * @param {Array.<Object>} imageList List of image objects to show. Retrieved
 *                                   by loadImages.
 * @return {Element} The generated image list container element.
 * @private
 */
photographyPrize.Gallery.prototype.displayImages_ = function(imageList) {
  
  // find the images and albums from the image list
  for (var j = 0; j < imageList.length; j++) {
    // change image urls to scale them to photographyPrize.Gallery.MAX_IMAGE_SIZE
  }

  // Go through the image list and create a gallery photo element for each image.
  // This uses the Closure library DOM helper, goog.dom.createDom:
  // element = goog.dom.createDom(tagName, className, var_childNodes);

  var category = goog.dom.createDom('div', 'category');
  for (var k = 0; k < items.length; k++) {
    var plusone = goog.dom.createDom('g:plusone');
    plusone.setAttribute('href', photoPageUrl);
    plusone.setAttribute('size', 'standard');
    plusone.setAttribute('annotation', 'none');

    var photo = goog.dom.createDom('div', {className: 'gallery-photo'}, ...)
    photo.appendChild(plusone);

    category.appendChild(photo);
  }
  this.galleryElement_.appendChild(category);
  return category;
};

טיפול באירועי גלילה

כדי לראות מתי המבקר גולל את הדף לתחתית ואנחנו צריכים לטעון תמונות חדשות, הגלריה מתחברת לאירוע הגלילה של אובייקט החלון. כדי להתגבר על ההבדלים בהטמעות בדפדפנים, אנחנו משתמשים בפונקציות שימושיות מ-Closure Library: goog.dom.getDocumentScroll() מחזירה אובייקט {x, y} עם מיקום הגלילה הנוכחי במסמך, goog.dom.getViewportSize() מחזירה את גודל החלון ו-goog.dom.getDocumentHeight() את הגובה של מסמך ה-HTML.

/**
 * Handle window scroll events by loading new images when the scroll reaches
 * the last screenful of the page.
 *
 * @param {goog.events.BrowserEvent} ev The scroll event.
 * @private
 */
photographyPrize.Gallery.prototype.handleScroll_ = function(ev) {
  var scrollY = goog.dom.getDocumentScroll().y;
  var height = goog.dom.getViewportSize().height;
  var documentHeight = goog.dom.getDocumentHeight();
  if (scrollY + height >= documentHeight - height / 2) {
    this.tryLoadingNextImages_();
  }
};

/**
 * Try loading the next batch of images objects from the server.
 * Only fires if we have already loaded the previous batch.
 *
 * @private
 */
photographyPrize.Gallery.prototype.tryLoadingNextImages_ = function() {
  // ...
};

התמונות נטענות

כדי לטעון את התמונות מהשרת, אנחנו משתמשים ברכיב goog.net.Jsonp. זמן הטיפול בשאילתה הוא goog.Uri. לאחר היצירה, אפשר לשלוח שאילתה לספק Jsonp עם אובייקט של פרמטר שאילתה ופונקציית קריאה חוזרת (callback) לאחר הצלחה.

/**
 * Loads image list from the App Engine page and sets the callback function
 * for the image list load completion.
 *
 * @param {string} tag Fetch images tagged with this.
 * @param {number} limit How many images to fetch.
 * @param {number} offset Offset for the image list.
 * @param {function(Array.<Object>=)} callback Function to call
 *        with the loaded image list.
 * @private
 */
photographyPrize.Gallery.prototype.loadImages_ = function(tag, limit, offset, callback) {
  var jsonp = new goog.net.Jsonp(
      new goog.Uri(photographyPrize.Gallery.IMAGE_LIST_URL));
  jsonp.send({'tag': tag, 'limit': limit, 'offset': offset}, callback);
};

כפי שצוין למעלה, סקריפט הגלריה משתמש במהדר Closure כדי לקמפל ולצמצם את הקוד. אפשר להשתמש במהדר Closure גם כדי לאכוף סיווג נכון (משתמשים בסימון @type foo של JSDoc בתגובות כדי להגדיר את הסוג של מאפיין), וגם כדי לדעת מתי אין תגובות לגבי שיטה.

בדיקות יחידה

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

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

Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";

הכלי ליצירת בדיקות יוצר בדיקה ריקה לכל אחד מהנכסים:

function testFoo() {
  fail();
  Foo();
}

function testFooPrototypeBar = function() {
  fail();
  instanceFoo.bar();
}

function testFooPrototypeBaz = function() {
  fail();
  instanceFoo.baz;
}

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

סיכום

Gallery+ הוא פרויקט קוד פתוח שמציג רשימה של תמונות מ-Google+ שמותאמות ל-#hashtag מסוים, לאחר שהן עברו תהליך של ניהול. הוא נוצר באמצעות Go וספריית Closure. הקצה העורפי פועל ב-App Engine. המערכת Gallery+ משמשת באתר של פרס הצילום של Google כדי להציג את גלריית התמונות שנשלחו. במאמר הזה התמקדנו בחלקים החשובים של הסקריפט לקצה הקדמי. הקולגה שלי, Johan Euphrosine מצוות קשרי המפתחים של App Engine, כותב מאמר שני על האפליקציה לקצה העורפי. הקצה העורפי נכתב ב-Go, השפה החדשה של Google בצד השרת. אם אתם רוצים לראות דוגמה לקוד Go בסביבת ייצור, כדאי לעקוב אחרינו.

קובצי עזר