Будьте добры сообщите какой срок проверки заданий и каким способом я буду оповещен! |
Работа в офф-лайне и файл настроек
HTML + JS: Практическое занятие №3
Файлы и работа offline
Задание: продолжая проект, полученный в результате выполнения второго практического задания, вынесите список источников новостей в отдельный и добавьте кеширование данных и вывод информации о наличии интернет-соединения в приложении.
- В качестве подтверждения выполнения лабораторной работы от вас потребуется предоставить скриншот первой страницы приложения, отображающей данные при отсутствии интернет-соединения.
ЗАМЕЧАНИЕ: напоминаем, что ваше приложение должно быть уникальным:
- Использовать в качестве источников данных источники, отличные от тех, которые приводятся в инструкциям к практическим занятиям
- Внешний вид приложения должен соответствовать выбранной вами тематике приложения
Настройки и файлы
В первом практическом упражнении мы научились подключать к нашему приложению внешние источники данных, используя для этого RSS-потоки в качестве источника и XMLHTTPRequest (в обертке WinJS.xhr) для запроса данных.
Если вы вернетесь к написанному коду (data.js), вы увидите, что список источников со всеми необходимыми атрибутами прописан непосредственно в приложении.
var blogs = [ { key: "MyBlogKey", url: "http://myblog.com/rss", title: 'My Blog Title, rsstitle: 'tbd', updated: 'tbd', dataPromise: null }, … ]
Во многих сценариях это может быть неудобно и непрактично. Фактически, в текущем варианте данные смешаны с кодом и, что еще более важно, эти данные неудобно обновлять. Например, легко можно представить ситуацию, когда вы хотите добавить в список еще один источник или заменить ссылку на какой-либо из источников, причем сделать это необходимо удаленно, без повторного размещения приложения в Windows Store. Одним из шагов к решению такой задачи будет вынесение списка источников в отдельный файл, который потом можно будет, например, синхронизировать с облачным хранилищем или вашим веб-сервером.
Давайте попробуем вынести эти настройки в отдельный файл внутри нашего проекта. Первым делом, необходимо определиться с форматом данных. Для JavaScript одним из наиболее подходящих форматов является JSON – его легко парсить средствами самого JavaScript, более того, он в точности соответствует нашему описанию списка блогов.
Создайте внутри проекта новую папку data и внутри новый пустой файл, дав ему соответствующее название, например, "sources.js" (так как формат описания данных повторяет формат описания в самом языке JavaSсript, то расширение .js позволяет автоматически использовать подсветку синтаксиса из VS, но можно дать и другое расширение, например, .json).
Чтобы составить описание в виде JSON, который будет стабильно распознаваться при парсинге, лучше всего поручить это какому-либо автоматизированному средству. Например, можно вызвать JavaScript-консоль внутри VS во время выполнения проекта (Ctrl+Alt+V, C), либо открыть инструменты разработчика в любом современном браузере (обычно F12) и переключиться там на JavaScript-консоль.
После чего, достаточно вызвать функцию JSON.stringify(…), передав в нее в качестве параметра значение blogs. Например, в IE10 это выглядит так:
Сначала объявить переменную blogs
Далее в консоли вызвать функцию JSON.stringify:
В результате работы функция выдаст тест в правильном формате (внутри кавычек):
[{"key":"BBlogging","url":"http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx", "title":"Blogging Windows","rsstitle":"tbd","updated":"tbd","dataPromise":null}, {"key":"AExperience","url":"http://blogs.windows.com/windows/b/windowsexperience/rss.aspx", "title":"Windows Experience","rsstitle":"tbd","updated":"tbd","dataPromise":null}, {"key":"CExtreme","url":"http://blogs.windows.com/windows/b/extremewindows/rss.aspx", "title":"Extreme Windows","rsstitle":"tbd","updated":"tbd","dataPromise":null}]
Такой текст и нужно вставить в наш файл описания источников данных. Как видите, в целом он повторяет описание коллекции данных непосредственно в JavaScript.
Замечание: не забудьте заменить этот текст-описание блогов на ваш собственный, отвечающий вашим предпочтениям.
Замечание: будьте внимательны – функция JSON.parse, которую мы будем использовать ниже, чувствительна к кодировке и связанными с ней переносам строки.
Вернитесь в файл data.js и удалите исходное описание списка блогов:
var blogs = […];
Перейдите к функции getBlogPosts, в которой мы использовали этот список для загрузки соответствующих rss-потоков:
function getBlogPosts(postsList) { blogs.forEach(function (feed) { … }); return postsList; }
Как видите, здесь мы проходимся по списку блогов внутри переменной blogs, которую мы только что удалили. Нам необходимо получить список блогов из файла и подставить его вместо переменной blogs.
Для этого создайте рядом новую функцию getBlogsList, которая будет считывать локальный файл sources.js, обрабатывать его и передавать дальше список блогов:
function getBlogsList() { var sourcesFile = "data/" + "sources.js"; var sourceUri = new Windows.Foundation.Uri("ms-appx:///" + sourcesFile); var listPromise = Windows.Storage.StorageFile.getFileFromApplicationUriAsync(sourceUri).then(function (file) { return Windows.Storage.FileIO.readTextAsync(file, Windows.Storage.Streams.UnicodeEncoding.utf8); }).then(function (text) { return JSON.parse(text); }); return listPromise; }
Обратите внимание на два важных момента:
- Адресацию локальных файлов проекта с использованием протокола ms-appx
- Постоянное использование асинхронных операций (через Promise) для получения файла и чтения данных из него.
Функция getBlogsList возвращает новый Promise-объект, которые после выполнения вернет JavaScript-объект описания данных.
Замечание: попробуйте также добавить обработчик ошибки при чтении файла на случай, если у вас будет ошибка в формате JSON-файла.
Замечание: помимо использования системных API для чтения файлов, в принципе, мы также могли воспользоваться XMLHTTPRequest (WinJS.xhr) для считывания данных.
Еще раз вернитесь к функции getBlogPosts, теперь надо внутри вызвать функцию getBlogsList и после возвращения результата проделать все остальные операции. Обновленная функция должна выглядеть следующим образом:
function getBlogPosts(postsList) { getBlogsList().then(function (list) { list.forEach(function (feed) { // Создание Promise … }); }); return postsList; }
Обратите внимание, что фактически весь внутренний код мы обернули функцией then, доступной из Promise, который в свою очередь возвращает функция getBlogsList.
Попробуйте запустить проект и убедитесь, что он работает также, как и раньше.
В будущем вы можете синхронизировать эти настройки с внешним сервисом, например, расположенном с облаке.
Теперь давайте научимся сохранять полученные с сервера данные локально и использовать их как кеш.
Сохранение и кеширование
Для сохранения данных в локальном хранилище, Windows предоставляет специальное API, доступное через объект Windows.Storage.ApplicationData. Подобное хранилище предоставляется для каждого приложения.
В нашем приложении мы будем реализовывать следующее поведение:
- Если приложение подключено к интернету, оно загружает данные и сохраняет их локально.
- Если подключения к интернету нет, приложение пытается использовать локальный кеш, если он есть.
- Если локального кеша нет, выводится сообщение о необходимости подключения.
Давайте разбираться по порядку. И начнем с сохранения локальных данных. Для этого снова вернемся к функции getBlogPosts, в которой мы загружаем данные.
Найдите код обработки результата запроса к внешнему сервису:
function (response) { if (response) { var syndicationXML = response.responseXML || (new DOMParser()).parseFromString(response.responseText, "text/xml"); processRSSFeed(syndicationXML, feed, postsList); } },
Этот код вызывается в цепочке событий и, чтобы мы могли ее продолжить для локального сохранения файлов, изнутри функции должен возвращаться результат. Давайте "наружу" передадим текстовый ответ, который мы постараемся сохранить:
function (response) { if (response) { var syndicationXML = response.responseXML || (new DOMParser()).parseFromString(response.responseText, "text/xml"); processRSSFeed(syndicationXML, feed, postsList); return response.responseText; } },
Теперь добавьте в конец цепочки Promise еще одну функцию:
feed.dataPromise = WinJS.xhr({ url: feed.url }).then( … ).then(function(text) { });
Внутри добавьте следующий код для сохранения локального кеша (мы записываем в настройки дату последнего сохранения и в файл сам текст):
var localSettings = Windows.Storage.ApplicationData.current.localSettings; var localFolder = Windows.Storage.ApplicationData.current.localFolder; if (text) { localSettings.values[feed.key] = Date.now.toString(); localFolder.createFileAsync(feed.key + ".rss", Windows.Storage.CreationCollisionOption.replaceExisting).then(function (file){ Windows.Storage.FileIO.writeTextAsync(file, text); }); }
Здесь мы записываем в настройки для каждого источника дату сохранения (на самом деле, нам достаточно записать любой параметр, по которому мы сможем понять, что у нас есть локальный кеш) и далее сохраняем полученный текст в локальный файл.
Теперь давайте перейдем ко второй задаче, если нет интернета, будем пытаться отобразить локальную копию данных. Для этого найдите код, в котором мы проверяем наличие интернет-соединения:
function tryUpdateData() { if (isInternetConnection()) { getBlogPosts(list); } else { showConnectionError("Please check your internet connection. "); } };
Давайте добавим дополнительную логику:
function tryUpdateData() { if (isInternetConnection()) { getBlogPosts(list); } else { getLocalBlogPosts(list); } };
Здесь мы ссылаемся на некоторую локальную функцию getLocalBlogPosts, которую еще необходимо написать. Эта функция похожа на обычную функцию загрузки постов с той лишь разницей, что данные мы будем брать локально:
function getLocalBlogPosts(postsList) { getBlogsList().then(function (list) { // Локальные настройки var localSettings = Windows.Storage.ApplicationData.current.localSettings; // Локальная папка с файлами var localFolder = Windows.Storage.ApplicationData.current.localFolder; list.forEach(function (feed) { if (localSettings.values[feed.key]) { // Доступ к файлу localFolder.getFileAsync(feed.key + ".rss").then(function (file) { // Чтение файла return Windows.Storage.FileIO.readTextAsync(file); }).then(function (responseText) { // Обработка результата var syndicationXML = (new DOMParser()).parseFromString(responseText, "text/xml"); processRSSFeed(syndicationXML, feed, postsList); }); } }); }); }
Внутри мы сначала проверяем через настройки, сохраняли ли мы файл с нужным именем (в целом это быстрее, чем проверить наличие файла). Если файл сохраняли, мы пытаемся получить к нему доступ и далее считываем информацию в текстовую переменную, после чего пытаемся обработать точно также, как мы это делали в случае удаленного запроса.
Наконец, осталось обработать ситуацию, в которой, никакого локального кеша нет. Для этого мы можем завести аккумулирующую переменную hasCache, но нам также понадобится собрать все отдельные Promise обработки файлов в общую коллецию (используя join), чтобы после этого обработать результат:
function getLocalBlogPosts(postsList) { var hasCache = false; getBlogsList().then(function (list) { // Локальные настройки var localSettings = Windows.Storage.ApplicationData.current.localSettings; // Локальная папка с файлами var localFolder = Windows.Storage.ApplicationData.current.localFolder; var filePromises = []; list.forEach(function (feed) { if (localSettings.values[feed.key]) { // Доступ к файлу var filePromise = localFolder.getFileAsync(feed.key + ".rss").then(function (file) { // Чтение файла return Windows.Storage.FileIO.readTextAsync(file); }).then(function (responseText) { // Обработка результата var syndicationXML = (new DOMParser()).parseFromString(responseText, "text/xml"); processRSSFeed(syndicationXML, feed, postsList); hasCache = true; return hasCache; }); filePromises.push(filePromise); } }); return WinJS.Promise.join(filePromises); }).then(function () { if (!hasCache) showConnectionError("Please check your internet connection. "); }, function error(e) { showConnectionError("Please check your internet connection. "); }); }
Последний штрих. Давайте при работе offline, сообщим пользователю, что он работает в offline-режиме. Откройте файл groupedItems.html и добавьте небольшой фрагмент html-кода (aside):
<div class="fragment groupeditemspage"> <aside class="online" id="offlineNotification">offline</aside> … </div>
Далее добавьте в сопутствующем файле groupedItems.css необходимые стили:
#offlineNotification { background: red; color:white; position:absolute; top:0; right: 50px; padding: 8px; font-size: 11pt; } #offlineNotification.online { display:none; }
Осталось добавить дополнительную логику. Перейдите к определению глобальной переменной Data и добавьте внизу информацию о статусе подключения:
WinJS.Namespace.define("Data", { items: groupedItems, groups: groupedItems.groups, getItemReference: getItemReference, getItemsFromGroup: getItemsFromGroup, resolveGroupReference: resolveGroupReference, resolveItemReference: resolveItemReference, online: isInternetConnection() });
Наконец, откройте файл groupedItems.js и добавьте в нем переключение внешнего вида нотификации в зависимости от наличия подключения к интернету (это можно сделать внутри функции _initializeLayout):
if (Data.online) { document.getElementById("offlineNotification").className = "online"; } else { document.getElementById("offlineNotification").className = ""; }
Отключите интернет (например, перейдя в Airplane-режим) и попробуйте запустить приложение. Если вы до этого кешировали данные, то должен отобразиться закешированный контент:
Если вы запустили приложение впервые, то должно появиться сообщение об ошибке:
Попробуйте самостоятельно:
- добавить сообщение об отсутствии интернет-соединения (при закешированном контенте) на других страницах;
- добавить обновление контента при подключении интернета (это можно отследить через событие Windows.Networking.Connectivity.NetworkInformation.onnetworkstatuschanged)