יצירת הגלריה של פרס Google לצילום
לאחרונה השקנו את קטע הגלריה באתר Google Photography Prize. בגלריה מוצגת רשימה של תמונות שנגללת בלי סוף, שנלקחו מ-Google+. היא מקבלת את רשימת התמונות מאפליקציית AppEngine שבה אנחנו משתמשים כדי לנהל את רשימת התמונות בגלריה. בנוסף, פרסמנו את אפליקציית הגלריה כפרויקט קוד פתוח ב-Google Code.
הקצה העורפי של הגלריה הוא אפליקציית AppEngine שמשתמשת ב-Google+ API כדי לחפש פוסטים עם אחד מההאשטאגים של Google Photography Prize (למשל, #megpp ו- #travelgpp). לאחר מכן, האפליקציה מוסיפה את הפוסטים האלה לרשימת התמונות שלא עברו ניהול תוכן. פעם בשבוע, צוות התוכן שלנו עובר על רשימת התמונות שלא עברו ניהול תוכן ומסמן את אלה שמפירות את ההנחיות שלנו בנושא תוכן. אחרי שלוחצים על כפתור הניהול, התמונות שלא סומנו מתווספות לרשימת התמונות שמוצגות בדף הגלריה.
חזית הגלריה
ממשק המשתמש של Gallery מבוסס על ספריית Google Closure. הווידג'ט של הגלריה עצמו הוא רכיב Closure. בחלק העליון של קובץ המקור, אנחנו אומרים ל-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. אחרי השם של שיטות פרטיות מופיע קו תחתון (_).
/**
* 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: הפונקציה 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 לשאילתה. אחרי שיוצרים את הספק, אפשר לשלוח אליו שאילתה עם אובייקט של פרמטרים של שאילתה ופונקציית קריאה חוזרת (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 בתגובות כדי להגדיר את סוג המאפיין) והוא גם מודיע כשאין תגובות לשיטה.
בדיקות יחידה (unit tests)
היינו צריכים גם בדיקות יחידה לסקריפט של הגלריה, אז זה נוח שבספריית 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. ה-backend פועל ב-App Engine. התכונה Gallery+ משמשת באתר Google Photography Prize להצגת גלריית ההגשות. במאמר הזה עברנו על החלקים המעניינים של הסקריפט של ממשק המשתמש. הקולגה שלי, יוהאן אופרוסין, מצוות קשרי המפתחים של App Engine, כותב מאמר שני על אפליקציית הקצה העורפי. הקצה העורפי כתוב ב-Go, שפת צד השרת החדשה של Google. אז אם אתם רוצים לראות דוגמה לקוד Go בסביבת ייצור, המשיכו לעקוב.