Thư viện giải thưởng ảnh của Google

Ilmari Heikkinen

Trang web của Google Photography Prize

Gần đây, chúng tôi đã ra mắt phần Thư viện trên trang web Giải thưởng nhiếp ảnh của Google. Thư viện hiển thị danh sách ảnh cuộn vô hạn được tìm nạp từ Google+. Thư viện này lấy danh sách ảnh từ ứng dụng AppEngine mà chúng tôi sử dụng để kiểm duyệt danh sách ảnh trong thư viện. Chúng tôi cũng đã phát hành ứng dụng thư viện dưới dạng một dự án nguồn mở trên Google Code.

Trang thư viện

Phần phụ trợ của thư viện là một ứng dụng AppEngine sử dụng API Google+ để tìm kiếm các bài đăng có một trong các hashtag của Google Photography Prize (ví dụ: #megpp và #travelgpp). Sau đó, ứng dụng sẽ thêm các bài đăng đó vào danh sách ảnh chưa được kiểm duyệt. Mỗi tuần một lần, nhóm nội dung của chúng tôi sẽ xem danh sách ảnh chưa được kiểm duyệt và gắn cờ những ảnh vi phạm nguyên tắc về nội dung của chúng tôi. Sau khi nhấn vào nút Kiểm duyệt, những bức ảnh chưa được gắn cờ sẽ được thêm vào danh sách ảnh xuất hiện trên trang thư viện.

Phần phụ trợ kiểm duyệt

Giao diện người dùng của Thư viện được tạo bằng thư viện Google Closure. Bản thân tiện ích Thư viện là một thành phần Đóng. Ở đầu tệp nguồn, chúng ta cho Closure biết rằng tệp này cung cấp một thành phần có tên là photographyPrize.Gallery và yêu cầu các phần của thư viện Closure mà ứng dụng sử dụng:

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');

Trang thư viện có một chút JavaScript sử dụng JSONP để truy xuất danh sách ảnh từ ứng dụng AppEngine. JSONP là một đoạn mã JavaScript đơn giản, đa nguồn gốc, chèn một tập lệnh có dạng như jsonpcallback("responseValue"). Để xử lý nội dung JSONP, chúng ta sẽ sử dụng thành phần goog.net.Jsonp trong thư viện Closure.

Tập lệnh thư viện sẽ xem qua danh sách ảnh và tạo các phần tử HTML để hiển thị các ảnh đó trên trang thư viện. Tính năng cuộn vô hạn hoạt động bằng cách kết nối với sự kiện cuộn cửa sổ và tải một loạt ảnh mới khi cửa sổ cuộn gần đến cuối trang. Sau khi tải phân đoạn danh sách ảnh mới, tập lệnh thư viện sẽ tạo các phần tử cho ảnh và thêm các phần tử đó vào phần tử thư viện để hiển thị.

Hiển thị danh sách hình ảnh

Phương thức hiển thị danh sách hình ảnh là một nội dung khá cơ bản. Trình này sẽ xem danh sách hình ảnh, tạo các phần tử HTML và nút +1. Bước tiếp theo là thêm phân đoạn danh sách đã tạo vào phần tử thư viện chính của thư viện. Bạn có thể thấy một số quy ước của trình biên dịch Closure trong mã bên dưới, hãy lưu ý các định nghĩa loại trong nhận xét JSDoc và chế độ hiển thị @private. Phương thức riêng tư có dấu gạch dưới (_) sau tên.

/**
 * 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;
};

Xử lý sự kiện cuộn

Để xem thời điểm khách truy cập cuộn trang xuống dưới cùng và chúng ta cần tải hình ảnh mới, thư viện sẽ liên kết với sự kiện cuộn của đối tượng cửa sổ. Để khắc phục sự khác biệt trong việc triển khai trình duyệt, chúng ta sẽ sử dụng một số hàm tiện ích hữu ích từ thư viện Closure: goog.dom.getDocumentScroll() trả về đối tượng {x, y} có vị trí cuộn tài liệu hiện tại, goog.dom.getViewportSize() trả về kích thước cửa sổ và goog.dom.getDocumentHeight() chiều cao của tài liệu 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() {
  // ...
};

Tải hình ảnh

Để tải hình ảnh từ máy chủ, chúng ta sẽ sử dụng thành phần goog.net.Jsonp. Cần có goog.Uri để truy vấn. Sau khi tạo, bạn có thể gửi truy vấn đến trình cung cấp Jsonp bằng đối tượng tham số truy vấn và hàm gọi lại thành công.

/**
 * 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);
};

Như đã đề cập ở trên, tập lệnh thư viện sử dụng trình biên dịch Closure để biên dịch và rút gọn mã. Trình biên dịch Closure cũng hữu ích trong việc thực thi cách nhập chính xác (bạn sử dụng ký hiệu JSDoc @type foo trong các nhận xét để đặt loại thuộc tính) và cũng cho bạn biết khi nào bạn không có nhận xét cho một phương thức.

Kiểm thử đơn vị

Chúng ta cũng cần kiểm thử đơn vị cho tập lệnh thư viện, vì vậy, thư viện Closure có một khung kiểm thử đơn vị tích hợp sẵn. Thư viện này tuân theo các quy ước jsUnit, vì vậy bạn có thể dễ dàng bắt đầu sử dụng.

Để giúp tôi viết mã kiểm thử đơn vị, tôi đã viết một tập lệnh Ruby nhỏ để phân tích cú pháp tệp JavaScript và tạo mã kiểm thử đơn vị không thành công cho từng phương thức và thuộc tính trong thành phần thư viện. Cho một tập lệnh như:

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

Trình tạo kiểm thử sẽ tạo một kiểm thử trống cho từng thuộc tính:

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

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

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

Những chương trình kiểm thử được tạo tự động này giúp tôi dễ dàng bắt đầu viết chương trình kiểm thử cho mã và tất cả các phương thức và thuộc tính đều được đề cập theo mặc định. Các bài kiểm thử không thành công tạo ra một hiệu ứng tâm lý tốt, trong đó tôi phải lần lượt xem xét các bài kiểm thử và viết các bài kiểm thử thích hợp. Cùng với một đồng hồ đo mức độ sử dụng mã, đây là một trò chơi thú vị để làm cho các bài kiểm thử và mức độ sử dụng mã đều có màu xanh lục.

Tóm tắt

Gallery+ là một dự án nguồn mở để hiển thị danh sách ảnh được kiểm duyệt trên Google+ khớp với một #hashtag. Ứng dụng này được xây dựng bằng Go và thư viện Closure. Phần phụ trợ chạy trên App Engine. Gallery+ được dùng trên trang web của Google Photography Prize để hiển thị thư viện ảnh đã gửi. Trong bài viết này, chúng ta đã tìm hiểu các phần thú vị của tập lệnh giao diện người dùng. Đồng nghiệp của tôi, Johan Euphrosine thuộc Nhóm quan hệ với nhà phát triển App Engine, đang viết bài thứ hai về ứng dụng phụ trợ. Phần phụ trợ được viết bằng Go, ngôn ngữ phía máy chủ mới của Google. Vì vậy, nếu bạn quan tâm đến ví dụ về mã Go trong sản xuất, hãy chú ý theo dõi!

Tài liệu tham khảo