Это вторая часть серии о Audero Feed Reader . В этой статье мы углубимся в бизнес-логику нашего приложения и предоставим дополнительную информацию о плагинах и API, используемых для нашего проекта.
1. Плагин и обзор API
Плагин уведомлений
В нескольких точках приложения Audero Feed Reader мы будем использовать метод alert()
плагина уведомлений . Как будет отображаться предупреждение, зависит от платформы, на которой будет работать приложение. Фактически, большинство поддерживаемых операционных систем используют собственное диалоговое окно, но другие, такие как Bada 2.x, используют классическую функцию alert()
браузера, которая менее настраиваема. Этот метод принимает до четырех параметров:
-
message
: строка, содержащая сообщение для отображения. -
alertCallback
: обратный вызов, который вызывается приalertCallback
диалогового окна предупреждения. -
title
: заголовок диалогового окна (значение по умолчанию «alert»). -
buttonName
: текст кнопки, включенный в диалог (значение по умолчанию «ОК»)
Помните, что Windows Phone 7 и 8 не имеют встроенного предупреждения браузера. Итак, если вы хотите использовать alert('message');
, вы должны назначить window.alert = navigator.notification.alert
.
Плагин InAppBrowser
В первой части этой серии я упоминал, что интересным моментом на странице кредитов является атрибут target="_blank"
применяемый к ссылкам. В этом разделе объясняется, как openLinksInApp()
метод openLinksInApp()
класса Application
.
InAppBrowser — это веб-браузер, который отображается в вашем приложении при использовании вызова
Как я уже говорил в первой части, начиная с версии 2.3.0, он имеет два новых метода: window.open
.executeScript()
и insertCSS()
. В настоящее время этот плагин предоставляет следующие пять методов:
-
addEventListener()
: позволяет пользователю прослушивать три события (loadstart
,loadstop
иexit
) и прикреплять функцию, которая запускается сразуloadstart
loadstop
этих событий. -
removeEventListener()
: Используется для удаления ранее подключенного слушателя. -
close()
: используется для закрытия окна InAppBrowser. -
executeScript()
: включает внедрение кода JavaScript в окноInAppBrowser
. -
executeScript()
: включает внедрение кода CSS в окноInAppBrowser
.
Если вы не использовали Cordova в течение нескольких месяцев или придерживаетесь версии 2.0.0, вы помните, что по умолчанию она открывала внешние ссылки в том же Cordova WebView, в котором работало приложение. Поэтому при посещении внешней страницы последняя отображаемая страница отображалась в точности так, как она была перед тем, как пользователь покинул ее. Начиная с этой версии, это больше не стандартное поведение. Фактически внешние ссылки теперь открываются с помощью Cordova WebView, если URL находится в белом списке вашего приложения. URL-адреса, которых нет в вашем белом списке, открываются с помощью плагина InAppBrowser (подробнее об этом в документации ). Но что это значит практически? Это означает, что если вы неправильно управляете ссылками и если пользователи вашего приложения щелкают ссылку и затем возвращаются к приложению, все jQuery Mobile или другие подобные улучшения будут потеряны. Это происходит потому, что все файлы CSS и JavaScript загружаются только на главной странице, а последующие URL-адреса загружаются с использованием AJAX (система по умолчанию, принятая jQuery Mobile).
Исправление этой проблемы реализовано в openLinksInApp()
. Фактически, решение состоит в том, чтобы отлавливать клики по всем внешним ссылкам, устанавливая атрибут target="_blank"
, предотвращая нежелательное поведение по умолчанию и открывая ссылки с помощью window.open()
. Для работы этого решения потребуется, чтобы вы установили белый список в файле конфигурации.
Google Feed API
Прежде чем говорить о классах Audero Feed Reader , нам нужно углубиться в волшебный мир Google Feed API и интерфейса Google Feed JSON, потому что мы будем использовать их в основной функции нашего приложения. Как я указывал в первой части этой серии, интерфейс анализирует канал RSS или ATOM и возвращает унифицированный и простой для анализа объект JSON. Конечно, мы можем счастливо управлять этим объектом JSON, используя JavaScript.
Этот интерфейс поддерживает два типа запросов: Find Feed и Load Feed. Первый выполняет поиск каналов на основе заданных ключевых слов, переданных в качестве аргумента, а второй выполняет поиск каналов на основе предоставленного URL-адреса канала. В нашем приложении мы будем использовать только функцию загрузки каналов.
Каждый запрос к этому API Google должен отправлять как минимум два параметра: v
и q
. Да, у них очень загадочные имена! Первый параметр v
указывает номер версии протокола. На момент написания этой статьи единственное допустимое значение — «1.0». Во втором параметре q
мы передаем URL для разбора. В дополнение к этому наше приложение будет использовать также параметр num
. В документации указывается количество записей для загрузки из канала, указанного в q.
Значение -1 указывает максимальное количество поддерживаемых записей, в настоящее время 100. По умолчанию загрузка канала возвращает четыре результата.
Поэтому важно реализовать нашу функцию загрузки 10 записей по умолчанию, а затем увеличения еще на 10 каждый раз, когда пользователь должен показать больше.
Теперь, когда вы знаете, как мы будем запрашивать службу Google, важно уточнить результат, который она даст. Если предоставленный нами URL-адрес правильный, мы найдем записи фида в свойстве responseData.feed.entries
. Каждая запись содержит много информации, но мы будем использовать только некоторые из них. В частности, мы напечатаем следующие свойства:
-
title
: Заголовок записи. -
link
: URL-адрес HTML-версии записи. -
author
: автор записи. -
contentSnippet
: фрагмент длиной менее 120 символов атрибута содержимого. Фрагмент не содержит тегов HTML.
Подробных данных, которые я предоставил выше, достаточно для целей нашего приложения, но если вы хотите узнать больше, посмотрите документацию Google Feed .
2. Создание класса корма
В этом разделе описывается класс Feed
и его методы, все они включены в файл feed.js
Как я указывал в предыдущей части, мы сохраним только два поля для каждого канала: заголовок и URL. Итак, этот класс принимает эти две точки данных в качестве параметров. Внутри него мы создаем два приватных свойства: _db
и _tableName
. Помните, что JavaScript на самом деле не имеет модификаторов видимости свойств и методов, поэтому мы фактически эмулируем частные данные.
Первый — это ярлык для свойства localStorage
объекта window
. Он используется для доступа к методам, предоставляемым плагином Storage, на котором основано наше приложение, и который мы будем использовать для хранения каналов. Вторая строка содержит имя ключа, в который мы сохраним данные. На самом деле, ссылаясь на спецификации хранилища , он сохраняет данные в формате ключ-значение. Следовательно, для хранения нашего массива каналов нам нужно JSON-ify. Это именно то, что будет делать наш метод save()
. Таким же образом, чтобы получить данные, мы должны проанализировать строку JSON, чтобы превратить ее в объект. Эта задача достигается методом load()
. Эти методы — единственные два, которые должны быть внутри определения класса, потому что они используют частные свойства.
Относительный раздел файла feed.js
указан ниже:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
function Feed(name, url) {
var _db = window.localStorage;
var _tableName = ‘feed’;
this.name = name;
this.url = url;
this.save = function (feeds) {
_db.setItem(_tableName, JSON.stringify(feeds));
};
this.load = function () {
return JSON.parse(_db.getItem(_tableName));
};
}
|
Вокруг этих двух простых методов мы создадим кучу других общих методов. В частности, мы создадим некоторые методы экземпляра, такие как add()
, чтобы добавить новый канал, delete()
, удалить канал и compareTo()
, чтобы сравнить экземпляр канала с другим каналом. Помимо этого, мы также разработаем некоторые статические методы, такие как getFeeds()
для извлечения всех каналов из хранилища, getFeed()
для извлечения только одного, и getFeed()
compare()
для сравнения двух объектов.
Методы сравнения заслуживают небольшого обсуждения, чтобы понять, как мы их будем сравнивать. Я пропущу описание compareTo()
потому что он ничего не делает, кроме как вызывает свой статический аналог, compare()
, который фактически выполняет свою работу. В нем мы сначала проверим, является ли одно из заданных значений нулевым. Если ни один из них не является нулевым, мы сравним лексикографически их имя и, если они равны, сравним их URL. Однако, как вы узнаете позже, мы заставим пользователя никогда не иметь двух каналов с одинаковым именем или URL.
Метод compare()
важен, потому что он определяет способ сравнения двух каналов, и это важно для определения порядка сортировки list-feeds.html
странице list-feeds.html
. Фактически, мы будем использовать собственный метод массива sort()
который принимает необязательный параметр, функцию, которая определяет порядок сортировки массива на основе его возвращаемых значений.
Код, который реализует то, что я описал, выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Feed.prototype.compareTo = function (other) {
return Feed.compare(this, other);
};
Feed.compare = function (feed, other) {
if (other == null) {
return 1;
}
if (feed == null) {
return -1;
}
var test = feed.name.localeCompare(other.name);
return (test === 0) ?
};
|
В дополнение к методам, рассмотренным до сих пор, мы создадим два метода поиска, которые мы будем использовать для поиска и удаления заданного searchByName()
: searchByName()
и searchByUrl()
. Последний метод, который я хочу выделить, — это getIndex()
, и именно он используется для получения индекса определенного файла.
Теперь, когда мы раскрыли все детали этого класса, я могу перечислить весь исходный код файла:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
|
function Feed(name, url) {
var _db = window.localStorage;
var _tableName = ‘feed’;
this.name = name;
this.url = url;
this.save = function (feeds) {
_db.setItem(_tableName, JSON.stringify(feeds));
};
this.load = function () {
return JSON.parse(_db.getItem(_tableName));
};
}
Feed.prototype.add = function () {
var index = Feed.getIndex(this);
var feeds = Feed.getFeeds();
if (index === false) {
feeds.push(this);
} else {
feeds[index] = this;
}
this.save(feeds);
};
Feed.prototype.delete = function () {
var index = Feed.getIndex(this);
var feeds = Feed.getFeeds();
if (index !== false) {
feeds.splice(index, 1);
this.save(feeds);
}
return feeds;
};
Feed.prototype.compareTo = function (other) {
return Feed.compare(this, other);
};
Feed.compare = function (feed, other) {
if (other == null) {
return 1;
}
if (feed == null) {
return -1;
}
var test = feed.name.localeCompare(other.name);
return (test === 0) ?
};
Feed.getFeeds = function () {
var feeds = new Feed().load();
return (feeds === null) ?
};
Feed.getFeed = function (feed) {
var index = Feed.getIndex(feed);
if (index === false) {
return null;
}
var feed = Feed.getFeeds()[index];
return new Feed(feed.name, feed.url);
};
Feed.getIndex = function (feed) {
var feeds = Feed.getFeeds();
for (var i = 0; i < feeds.length; i++) {
if (feed.compareTo(feeds[i]) === 0) {
return i;
}
}
return false;
};
Feed.deleteFeeds = function () {
new Feed().save([]);
};
Feed.searchByName = function (name) {
var feeds = Feed.getFeeds();
for (var i = 0; i < feeds.length; i++) {
if (feeds[i].name === name) {
return new Feed(feeds[i].name, feeds[i].url);
}
}
return false;
};
Feed.searchByUrl = function (url) {
var feeds = Feed.getFeeds();
for (var i = 0; i < feeds.length; i++) {
if (feeds[i].url === url) {
return new Feed(feeds[i].name, feeds[i].url);
}
}
return false;
};
|
3. Построение класса приложения
В этом разделе обсуждается второй и последний класс проекта, Application
, содержащийся в файле application.js
. Его цель — инициализировать макет страниц, прикрепить события к элементам страницы приложения и использовать класс Feed
для сохранения, загрузки и выборки каналов.
Этот класс организован так, чтобы иметь точку входа в initApplication()
. Он вызывается, как только Cordova была инициализирована и его API готовы к работе. В рамках этого метода мы прикрепляем определенный обработчик к каждой инициализации страницы, чтобы мы могли управлять событиями, запускаемыми их виджетами. В нем мы также будем вызывать Application.openLinksInApp()
по причинам, обсуждавшимся ранее. Кроме того, чтобы улучшить взаимодействие с пользователем, мы будем ловить каждое нажатие физической кнопки «назад» (там, где она существует), чтобы перенаправить пользователя на домашнюю страницу нашего приложения.
Основная функция нашего приложения — initShowFeedPage()
потому что оно использует интерфейс JSON в Google Feed. Перед запуском запроса к службе мы подсчитываем количество уже загруженных записей (переменная currentEntries
) и вычисляем, сколько записей должна получить entriesToShow
(переменная entriesToShow
). Затем мы запустим запрос AJAX, используя метод jQuery ajax()
, и в то же время покажем пользователю виджет загрузки страницы. Когда успешный обратный вызов выполняется, мы сначала проверяем, совпадает ли количество возвращаемых записей с уже показанным числом, и в этом случае мы показываем сообщение «Нет записей для загрузки». В противном случае мы добавляем их в список и обновляем виджет аккордеона ( $list.collapsibleset('refresh')
). Вместе с каждой записью мы также прикрепляем обработчик к создаваемой кнопке, поэтому, если соединение отключено, вместо доступа к странице запрашивается сообщение.
Наконец, метод updateIcons()
будет обсуждаться в следующей и последней части серии.
Код, который реализует обсуждаемый класс, приведен ниже:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
var Application = {
initApplication: function () {
$(document)
.on(‘pageinit’, ‘#add-feed-page’, function () {
Application.initAddFeedPage();
})
.on(‘pageinit’, ‘#list-feeds-page’, function () {
Application.initListFeedPage();
})
.on(‘pageinit’, ‘#show-feed-page’, function () {
var url = this.getAttribute(‘data-url’).replace(/(.*?)url=/g, »);
Application.initShowFeedPage(url);
})
.on(‘pageinit’, ‘#aurelio-page’, function () {
Application.initAurelioPage();
})
.on(‘backbutton’, function () {
$.mobile.changePage(‘index.html’);
});
Application.openLinksInApp();
},
initAddFeedPage: function () {
$(‘#add-feed-form’).submit(function (event) {
event.preventDefault();
var feedName = $(‘#feed-name’).val().trim();
var feedUrl = $(‘#feed-url’).val().trim();
if (feedName === ») {
navigator.notification.alert(‘Name field is required and cannot be empty’, function () {
}, ‘Error’);
return false;
}
if (feedUrl === ») {
navigator.notification.alert(‘URL field is required and cannot be empty’, function () {
}, ‘Error’);
return false;
}
if (Feed.searchByName(feedName) === false && Feed.searchByUrl(feedUrl) === false) {
var feed = new Feed(feedName, feedUrl);
feed.add();
navigator.notification.alert(‘Feed saved correctly’, function () {
$.mobile.changePage(‘index.html’);
}, ‘Success’);
} else {
navigator.notification.alert(‘Feed not saved! Either the Name or the Url specified is already in use’, function () {
}, ‘Error’);
}
return false;
});
},
initListFeedPage: function () {
var $feedsList = $(‘#feeds-list’);
var items = Feed.getFeeds();
var htmlItems = »;
$feedsList.empty();
items = items.sort(Feed.compare);
for (var i = 0; i < items.length; i++) {
htmlItems += ‘<li><a href=»show-feed.html?url=’ + items[i].url + ‘»>’ + items[i].name + ‘</a></li>’;
}
$feedsList.append(htmlItems).listview(‘refresh’);
},
initShowFeedPage: function (url) {
var step = 10;
var loadFeed = function () {
var currentEntries = $(‘#feed-entries’).find(‘div[data-role=collapsible]’).length;
var entriesToShow = currentEntries + step;
$.ajax({
url: ‘https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=’ + entriesToShow + ‘&q=’ + encodeURI(url),
dataType: ‘json’,
beforeSend: function () {
$.mobile.loading(‘show’, {
text: ‘Please wait while retrieving data…’,
textVisible: true
});
},
success: function (data) {
var $list = $(‘#feed-entries’);
if (data.responseData === null) {
navigator.notification.alert(‘Unable to retrieve the Feed. Invalid URL’, function () {
}, ‘Error’);
return;
}
var items = data.responseData.feed.entries;
var $post;
if (currentEntries === items.length) {
navigator.notification.alert(‘No more entries to load’, function () {
}, ‘Info’);
return;
}
for (var i = currentEntries; i < items.length; i++) {
$post = $(‘<div data-role=»collapsible» data-expanded-icon=»arrow-d» data-collapsed-icon=»arrow-r» data-iconpos=»right»>’);
$post
.append($(‘<h2>’).text(items[i].title))
.append($(‘<h3>’).html(‘<a href=»‘ + items[i].link + ‘» target=»_blank»>’ + items[i].title + ‘</a>’)) // Add title
.append($(‘<p>’).html(items[i].contentSnippet)) // Add description
.append($(‘<p>’).text(‘Author: ‘ + items[i].author))
.append(
$(‘<a href=»‘ + items[i].link + ‘» target=»_blank» data-role=»button»>’)
.text(‘Go to the Article’)
.button()
.click(function (event) {
if (Application.checkRequirements() === false) {
event.preventDefault();
navigator.notification.alert(‘The connection is off, please turn it on’, function () {
}, ‘Error’);
return false;
}
$(this).removeClass(‘ui-btn-active’);
})
);
$list.append($post);
}
$list.collapsibleset(‘refresh’);
},
error: function () {
navigator.notification.alert(‘Unable to retrieve the Feed. Try later’, function () {
}, ‘Error’);
},
complete: function () {
$.mobile.loading(‘hide’);
}
});
};
$(‘#show-more-entries’).click(function () {
loadFeed();
$(this).removeClass(‘ui-btn-active’);
});
$(‘#delete-feed’).click(function () {
Feed.searchByUrl(url).delete();
navigator.notification.alert(‘Feed deleted’, function () {
$.mobile.changePage(‘list-feeds.html’);
}, ‘Success’);
});
if (Application.checkRequirements() === true) {
loadFeed();
} else {
navigator.notification.alert(‘To use this app you must enable your internet connection’, function () {
}, ‘Warning’);
}
},
initAurelioPage: function () {
$(‘a[target=_blank]’).click(function () {
$(this).closest(‘li’).removeClass(‘ui-btn-active’);
});
},
checkRequirements: function () {
if (navigator.connection.type === Connection.NONE) {
return false;
}
return true;
},
updateIcons: function () {
var $buttons = $(‘a[data-icon], button[data-icon]’);
var isMobileWidth = ($(window).width() <= 480);
isMobileWidth ?
},
openLinksInApp: function () {
$(document).on(‘click’, ‘a[target=_blank]’, function (event) {
event.preventDefault();
window.open($(this).attr(‘href’), ‘_blank’);
});
}
};
|
Вывод
В третьей и последней части этой серии статей мы увидим, как создавать и тестировать установщики, используя CLI и Adobe PhoneGap Build .