Спонсор: Microsoft
Опубликован: 21.03.2013 | Доступ: свободный | Студентов: 6312 / 126 | Длительность: 06:49:00
Лабораторная работа 3:

Работа в офф-лайне и файл настроек

< Онлайн-консультация 1 || Лабораторная работа 3: 12

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:

JSON.stringify(blogs);

В результате работы функция выдаст тест в правильном формате (внутри кавычек):

[{"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;
}

Обратите внимание на два важных момента:

  1. Адресацию локальных файлов проекта с использованием протокола ms-appx
  2. Постоянное использование асинхронных операций (через 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)
< Онлайн-консультация 1 || Лабораторная работа 3: 12
Андрей Милютин
Андрей Милютин

Будьте добры сообщите какой срок проверки заданий и каким способом я буду оповещен!

Данила Слупский
Данила Слупский

К сожалению, я не могу выполнить данную практическую работу в VS 2013 на WIndows 8.1. Код описанных файлов отличается от кода в моем проекте. Как мне быть?