在这个关于如何使用Flickr API开发简单画廊的迷你系列的第一部分中 ,我们讨论了项目的要求,构造HTML页面所需的标记以及五个CSS模块中的两个。
在第二部分(也是最后一部分)中,我们将介绍其余的CSS模块以及为项目提供动力的JavaScript代码。 事不宜迟,让我们开始吧。
样式(续)
在上一篇文章中,我们讨论了帮助程序类的模块和布局模块。 列表中的下一个是图库模块。
画廊模块
画廊模块定义画廊及其组件的样式。 它由简单的声明组成,我将重点介绍一些注意事项。
首先要指出的是,具有一类gallery
的元素的固定高度为500px,该元素用作以其自然尺寸显示的照片的容器。 然后,通过将其内部的img
元素(用于显示所选图像)设置为max-height
和max-width
属性,将其限制为100%
。 这样,我们确保图像不会溢出容器。
第二点是,我们定义了当用户将鼠标悬停或聚焦在箭头上时箭头的样式如何变化。 焦点事件的样式很重要,因为对于那些通过键盘(例如,通过按TAB键)浏览网站的用户,可以增强元素的可访问性。
如果您认为自己是CSS的初学者,则可能还需要研究如何将按钮制作成圆形以及如何绘制箭头。
该模块的完整代码如下所示:
.gallery
{
position: relative;
height: 500px;
border: 1px solid #FFFFFF;
}
.gallery img
{
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 100%;
}
.gallery__arrow
{
position: absolute;
top: 50%;
display: block;
width: 60px;
height: 60px;
border: none;
border-radius: 50%;
background-color: #000000;
opacity: 0.7;
cursor: pointer;
}
.gallery__arrow:hover,
.gallery__arrow:focus
{
opacity: 1;
}
.gallery__arrow:before,
.gallery__arrow:after
{
content: '';
position: absolute;
width: 10px;
height: 40%;
background-color: #FFFFFF;
}
.gallery__arrow:before
{
bottom: 12px;
}
.gallery__arrow:after
{
top: 12px;
}
.gallery__arrow:hover:before,
.gallery__arrow:focus:before,
.gallery__arrow:hover:after,
.gallery__arrow:focus:after
{
background-color: #FCB712;
}
.gallery__arrow--left
{
left: 0.5em;
}
.gallery__arrow--left:before
{
transform: rotate(-40deg);
left: 35%;
}
.gallery__arrow--left:after
{
transform: rotate(40deg);
left: 35%;
}
.gallery__arrow--right
{
right: 0.5em;
}
.gallery__arrow--right:before
{
transform: rotate(40deg);
right: 35%;
}
.gallery__arrow--right:after
{
transform: rotate(-40deg);
right: 35%;
}
缩略图模块
缩略图模块不包含任何花哨的内容。 通过将width
属性设置为19%
,将margin-right
为1%
,并将display
属性设置为inline-block
,将缩略图连续设置为五个。 值得一提的另一点是,当缩略图悬停或聚焦以增强可访问性时,会发生一种效果,如上一节所述。
该模块的完整代码如下:
.thumbnails__list,
.thumbnails__pager
{
margin: 0;
padding: 0;
list-style-type: none;
}
.thumbnails__list li
{
display: inline-block;
width: 19%;
margin-top: 1%;
margin-right: 1%;
}
.thumbnail
{
width: 100%;
}
.thumbnail:hover,
.thumbnail:focus
{
border: 1px solid #FCB720;
opacity: 0.7;
}
.thumbnails__pager
{
text-align: right;
margin: 0.5em 0;
}
.thumbnails__pager li
{
display: inline;
}
.thumbnails__pager a
{
margin: 0 0.2em;
color: #FFFFFF;
text-decoration: none;
}
.thumbnails__pager a.current,
.thumbnails__pager a:hover,
.thumbnails__pager a:focus
{
color: #FCB720;
text-decoration: underline;
}
主页模块
最后一个模块是主页模块。 在这里,我们可以为项目的元素设置样式,这些元素不适合其他任何模块,并且特定于主页。 在处理实际项目时,您经常会发现自己的样式元素仅在给定页面上具有某种外观,在这种情况下,仅为该页面创建特定的CSS文件是有意义的。
下面显示了homepage.css文件的完整代码:
.form-search
{
margin: 0.5em 0;
text-align: right;
}
.form-search #query
{
padding: 0.2em;
}
.form-search input
{
color: #000000;
}
.thumbnails
{
border-bottom: 3px solid #FFFFFF;
}
.copyright
{
margin-top: 0.5em;
margin-bottom: 0.5em;
text-align: right;
}
在最后一个模块中,我们完成了用于对项目进行样式设置的CSS文件的概述,因此现在该讨论业务逻辑了。
业务逻辑
该项目的业务逻辑也被组织成小的模块,其中一个文件“ main.js”充当标记和JavaScript模块之间的粘合剂。 在此文件中,我们将定义画廊按钮的事件处理程序,用户单击寻呼机中的链接之一时发生的情况以及用户搜索某些给定文本时的操作。
在检查每个模块的特性之前,我想重点介绍一下我已经使用的一些有趣的技术。 首先是每个模块都是使用IIFE(立即调用函数表达式)定义的 ,这使我们能够创建私有变量和方法并避免污染全局范围。 第二个问题是,在每个模块中,我都采用了strict模式 ,该模式对JavaScript代码的执行方式施加了更多限制性规则。 例如, 通过将它们更改为throw errors
, 消除了一些JavaScript静默错误
。 最后,每个文件都实现模块模式 。
考虑到这些要点,让我们看一下定义的模块。
实用程序模块
我们将讨论的第一个模块是实用程序模块。 它包含一些普遍感兴趣的方法,我们的JavaScript模块将使用这些方法。 它仅定义两种方法: extend
和buildUrl
。
extend
方法是jQuery中同名方法的简化版本,用于将两个或多个对象的属性合并为一个(第一个参数)。 如果您不是JavaScript忍者,则可能需要学习如何通过使用arguments
启用此方法以接受任意数量的对象。 arguments
是类似于数组的对象,与传递给函数的参数相对应。
buildUrl
方法用于从URL以及要在查询字符串中使用的名称和值的对象开始,创建包含查询字符串的有效URL。
该实用程序模块的代码定义如下:
(function(document, window) {
'use strict';
function buildUrl(url, parameters){
var queryString = '';
for(var key in parameters) {
if (parameters.hasOwnProperty(key)) {
queryString += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]) + '&';
}
}
if (queryString.lastIndexOf('&') === queryString.length - 1){
queryString = queryString.substring(0, queryString.length - 1);
}
return url + '?' + queryString;
}
function extend(object) {
for(var i = 1; i < arguments.length; i++) {
for(var key in arguments[i]) {
if (arguments[i].hasOwnProperty(key)) {
object[key] = arguments[i][key];
}
}
}
return object;
}
window.Utility = {
buildUrl: buildUrl,
extend: extend
};
})(document, window);
画廊模块
Gallery模块定义在全局范围内公开的Gallery
对象。 它的构造函数接受两个参数:属于图库的照片列表(即包含照片URL的数组),以及将以其自然大小显示图像的DOM元素。 该对象定义我们画廊的功能,例如显示上一个( showPrevious
方法)或下一个( showNext
方法)图像,或创建缩略图列表( createThumbnailsGallery
方法)的功能。
本模块演示了一种有趣的技术,用于解决在处理循环和事件处理程序时发生的常见关闭问题。 我已经在我的文章5更多JavaScript访谈练习 (第1点和第2点)中讨论了这个问题及其解决方案。 在此,在循环外部定义的函数是clickHandler()
。
现在您已经知道了本模块中使用的技巧,现在您可以阅读其完整源代码:
(function(document, window) {
'use strict';
function Gallery(photos, container) {
this.currentIndex = 0;
this.photos = photos;
this.container = container;
this.showPhoto(this.currentIndex);
}
Gallery.prototype.showPhoto = function(index) {
if (index >= 0 && index < this.photos.length) {
this.currentIndex = index;
this.container.src = Flickr.buildPhotoLargeUrl(this.photos[this.currentIndex]);
}
};
Gallery.prototype.showPrevious = function() {
if (this.currentIndex > 0) {
this.currentIndex--;
}
this.showPhoto(this.currentIndex);
};
Gallery.prototype.showNext = function() {
if (this.currentIndex < this.photos.length - 1) {
this.currentIndex++;
}
this.showPhoto(this.currentIndex);
};
Gallery.prototype.createThumbnailsGallery = function(container) {
function clickHandler(index, gallery) {
return function (event) {
event.preventDefault();
gallery.showPhoto(index);
};
}
container.textContent = '';
var image, link, listItem;
for (var i = 0; i < this.photos.length; i++) {
image = document.createElement('img');
image.src = Flickr.buildThumbnailUrl(this.photos[i]);
image.className = 'thumbnail';
image.alt = this.photos[i].title;
image.title = this.photos[i].title;
link = document.createElement('a');
link.href = image.src;
link.addEventListener('click', clickHandler(i, this));
link.appendChild(image);
listItem = document.createElement('li');
listItem.appendChild(link);
container.appendChild(listItem);
}
};
window.Gallery = Gallery;
})(document, window);
Flickr模块
从某种意义上说,Flickr模块是我们应用程序的核心,因为它定义了使用Flickr API的代码。 与到目前为止我们介绍的其他模块不同,您可能希望扩展此模块以提供更多功能。 例如,您可以将其扩展为根据用户名或照片位置搜索照片。 因此,我将使用Utility.extend()
方法,而不是仅在全局范围内公开Flickr
对象,如下所示:
window.Flickr = Utility.extend(window.Flickr || {}, {
/* methods of this module defined here */
});
Utility.extend()
方法用于此模块的另一部分,尤其是在searchText()
方法的第一条语句中。 在这种情况下,它用于将searchText()
方法的调用者传递的参数与调用者不应该知道的模块的私有信息合并(从而保持私有),例如要调用的API方法( flickr.photos.search
)。
该模块需要一个API密钥才能与Flickr API进行通信。 我无法与世界共享我的API密钥,因此您需要插入自己的API密钥作为变量apiKey
的值才能拥有一个完全正常的项目。 如果您不提供这样的密钥,那么您对Flickr的所有请求都会失败。
考虑到最后一点,这是此模块的完整代码:
(function(document, window) {
'use strict';
var apiKey = 'YOUR-API-KEY-HERE';
var apiURL = 'https://api.flickr.com/services/rest/';
function searchText(parameters) {
var requestParameters = Utility.extend(parameters, {
method: 'flickr.photos.search',
api_key: apiKey,
format: 'json'
});
var script = document.createElement('script');
script.src = Utility.buildUrl(apiURL, requestParameters);
document.head.appendChild(script);
document.head.removeChild(script);
}
function buildThumbnailUrl(photo) {
return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
'/' + photo.id + '_' + photo.secret + '_q.jpg';
}
function buildPhotoUrl(photo) {
return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
'/' + photo.id + '_' + photo.secret + '.jpg';
}
function buildPhotoLargeUrl(photo) {
return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
'/' + photo.id + '_' + photo.secret + '_b.jpg';
}
window.Flickr = Utility.extend(window.Flickr || {}, {
buildThumbnailUrl: buildThumbnailUrl,
buildPhotoUrl: buildPhotoUrl,
buildPhotoLargeUrl: buildPhotoLargeUrl,
searchText: searchText
});
})(document, window);
捆绑在一起:主模块
现在,我们已经讨论了项目的所有模块,我们需要将它们与页面的HTML元素绑定在一起,以便例如在单击向右箭头时,服务将在列表中显示下一张照片。 这就是main.js文件中包含的代码的作用。 我想讨论代码的两个部分:传呼机和箭头。
寻呼机最多显示六页,外加特殊的“按钮”。 (其实他们都是a
元素)去到第一页和最后一页,并移动到一个和下一个页面。 单击传呼机的元素之一时,服务必须显示属于该页面的缩略图。 例如,如果第3页用户点击(记住,每个页面包含15个缩略图),服务应该展现给用户属于此页的照片,从31 日到45 日的照片中发现(如果有的话) 。 要执行此操作,我们可以将监听程序添加到寻呼机的每个链接以及特殊按钮,但这会浪费内存。 通过采用一种称为事件委托的技术,我们可以更有效地做到这一点。 因此,我们将为寻呼机本身添加一个侦听器,而不是为该寻呼机的每个子级添加侦听器。 然后,根据触发点击事件的元素,我们将执行预期的操作。 (如果您不熟悉此主题,则可以阅读David Walsh的文章JavaScript事件委托的工作 原理 。)
我要提到的第二点是,我没有在两个箭头上仅为click
事件添加事件侦听器,而是为keydown
事件添加了侦听器。 通过这样做,我可以确定当焦点位于箭头上时用户是否按下了键盘的键。 然后,如果按下的键是ENTER键,那么我将执行用户期望的相同操作(如果触发了click事件)。 这种简单的方法使我们能够为那些通过键盘浏览网站的用户改善服务的可访问性。
这两个有趣的部分都可以在名为init()
的函数中找到,该函数与主模块的完整代码一起显示如下:
(function(document, window) {
'use strict';
var gallery;
var lastSearch = 'London';
function searchPhotos(text, page) {
if (text.length === 0) {
alert('Error: the field is required');
}
page = page > 0 ? page : 1;
Flickr.searchText({
text: text,
per_page: 15,
jsoncallback: 'Website.Homepage.showPhotos',
page: page
});
}
function createPager(element, parameters) {
var pagesToShow = 5;
var url = '/search/' + parameters.query + '/';
element.textContent = '';
var previousLinks = {
'<<': 1,
'<': (parameters.currentPage - 1 || parameters.currentPage)
};
for (var key in previousLinks) {
link = document.createElement('a');
link.href = url + previousLinks[key];
link.innerHTML = '<span class="js-page-number visually-hidden">' + previousLinks[key] + '</span>' + key;
var listItem = document.createElement('li');
listItem.appendChild(link);
element.appendChild(listItem);
}
// Avoid showing less than 6 pages in the pager because the user reaches the end
var pagesDifference = parameters.pagesNumber - parameters.currentPage;
var startIndex = parameters.currentPage;
if (pagesDifference < pagesToShow) {
startIndex = parameters.currentPage - (pagesToShow - pagesDifference - 1) || 1;
}
var link;
for(var i = startIndex; i < parameters.currentPage + pagesToShow && i <= parameters.pagesNumber; i++) {
link = document.createElement('a');
link.href = url + i;
link.innerHTML = '<span class="js-page-number">' + i + '</span>';
if (i === parameters.currentPage) {
link.className += ' current';
}
listItem = document.createElement('li');
listItem.appendChild(link);
element.appendChild(listItem);
}
var nextLinks = {
'>': (parameters.currentPage === parameters.pagesNumber ? parameters.pagesNumber : parameters.currentPage + 1),
'>>': parameters.pagesNumber
};
for (key in nextLinks) {
link = document.createElement('a');
link.href = url + nextLinks[key];
link.innerHTML = '<span class="js-page-number visually-hidden">' + nextLinks[key] + '</span>' + key;
var listItem = document.createElement('li');
listItem.appendChild(link);
element.appendChild(listItem);
}
}
function showPhotos(data) {
createPager(
document.getElementsByClassName('js-thumbnails__pager')[0], {
query: lastSearch,
currentPage: data.photos.page,
pagesNumber: data.photos.pages
}
);
gallery = new Gallery(data.photos.photo, document.getElementsByClassName('js-gallery__image')[0]);
gallery.createThumbnailsGallery(document.getElementsByClassName('js-thumbnails__list')[0]);
}
function init() {
document.getElementsByClassName('js-form-search')[0].addEventListener('submit', function(event) {
event.preventDefault();
lastSearch = document.getElementById('query').value;
if (lastSearch.length > 0) {
searchPhotos(lastSearch, 1);
}
});
var leftArrow = document.getElementsByClassName('js-gallery__arrow--left')[0];
leftArrow.addEventListener('click', function() {
gallery.showPrevious.bind(gallery)();
});
leftArrow.addEventListener('keydown', function(event) {
if (event.which === 13) {
gallery.showPrevious.bind(gallery)();
}
});
var rightArrow = document.getElementsByClassName('js-gallery__arrow--right')[0];
rightArrow.addEventListener('click', function() {
gallery.showNext.bind(gallery)();
});
rightArrow.addEventListener('keydown', function(event) {
if (event.which === 13) {
gallery.showNext.bind(gallery)()();
}
});
document.getElementsByClassName('js-thumbnails__pager')[0].addEventListener('click', function(event) {
event.preventDefault();
var page;
var currentLink = this.getElementsByClassName('current')[0];
if (event.target.nodeName === 'SPAN') {
page = event.target.textContent;
} else if (event.target.nodeName === 'A') {
page = event.target.getElementsByClassName('js-page-number')[0].textContent;
}
// Avoid reloading the same page
if (page && page !== currentLink.getElementsByClassName('js-page-number')[0].textContent) {
searchPhotos(lastSearch, page);
}
});
// Kickstart the page
searchPhotos(lastSearch, 1);
}
window.Website = Utility.extend(window.Website || {}, {
Homepage: {
init: init,
showPhotos: showPhotos
}
});
})(document, window);
Website.Homepage.init();
有了最后一个文件的代码,我们终于完成了我们的项目。
结论
在这篇由两部分组成的文章中,我已指导您完成一个基于外部API的简单服务的创建。 通过使用Flickr API,我们允许用户通过搜索照片的标题和描述来生成Flickr照片库。 希望您喜欢它,并学习了一些有趣的新技术或方法。
该项目的源代码可在我的GitHub帐户上的名为Flickr gallery demo的存储库中访问。
From: https://www.sitepoint.com/creating-image-gallery-flickr-api-style-logic/