![]() |
Здравствуйте, гость ( Вход | Регистрация )
![]() ![]() |
![]() |
![]()
Сообщение
#1
|
|
![]() Администратор ![]() ![]() ![]() ![]() ![]() Группа: Главные администраторы Сообщений: 14349 Регистрация: 12.10.2007 Из: Twilight Zone Пользователь №: 1 ![]() |
![]() Описанная схема имеет все недостатки толстого клиента. Это и необходимость разработки отдельного приложения для работы из браузеров (что в современном мире является нормальным требованием), и необходимость установки дополнительного ПО, и проблема его обновления, и вообще необходимость найма специалистов по разработке десктоп приложений. Согласитесь, нам, как веб разработчикам, проблема работы офлайн всегда была костью в горле. Сегодня этот вопрос решается элегантно — с помощью HTML5 с его локальным хранилищем (local storage), Ext JS 4 с возможностью прозрачно работать с этим хранилищем, и HTML5 кэшем приложений (Application Cache). Совокупность этих технологий позволяет реализовать следующую схему: при наличии сети статичные файлы (HTML/CSS/JS код и картинки) загружаются с сайта и мы работаем с серверной централизованной базой данных, при отсутствии сети статика загружается из Application Cache и мы работаем с локальным хранилищем, которое сохраняется в серверную БД при появлении доступа к Интернет. При этом без активного подключения по URL адресу страницы браузер отображает не ошибку доступа к сети, а функциональную систему, работающую с локальным хранилищем. Пояснения и рабочий пример (да не упадет мой vds под хаброэффектом) — под катом. Статья получилась немаленькая, но, надеюсь, весьма содержательная. HTML5 Если Вы знакомы с HTML 5 – смело пропускайте эту главу, если нет – здесь вы найдете краткое описание используемых технологий. Application Cache Application Cache – кэш приложения, позволяет сохранять локально статичные файлы и использовать их без подключения к сети. Список файлов к кэшированию находится в файле-манифесте, адрес которого указывается в тэге html, например: … Mime-type файла-манифеста должен быть установлен в text/cache-manifest. Для веб сервера Apache, например, добавьте в конфигурационный файл: AddType text/cache-manifest .appcache Для Java добавьте в web.xml: <mime-mapping> <extension>appcache</extension> <mime-type>text/cache-manifest</mime-type> </mime-mapping> Пример простейшего файла-манифеста: CACHE MANIFEST index.html stylesheet.css images/logo.png scripts/main.js Первая строка (CACHE MANIFEST) обязательна. Если Вы хотите добавить ресурсы, которые всегда требуют наличия сети – добавьте их после строки NETWORK: CACHE MANIFEST index.html NETWORK: login.php Поближе познакомиться с форматом манифест-файлов можно здесь. Обновиться Application Cache может тремя способами: его может принудительно удалить пользователь в браузере, кэш обновится при обновлении файла-манифеста, и, наконец, кэш можно принудительно обновить из JavaScript. Поддержка браузеров: Chrome 4+, Firefox 4+, Safari 4+, Opera 11+, IE 10, iOS 5+, Android 3+. Local storage Локальное хранилище в HTML5 позволяет сохранять данные локально, рекомендуемое ограничение на размер – 5 Mb, однако, в ряде браузеров может быть увеличено. Данные не пропадают после закрытия страницы или браузера. Хранилище одно на домен, то есть одни и те же данные доступны с разных страниц Вашего сайта. Более того, Вы можете отслеживать изменения данных в хранилище со всех открытых одновременно страниц. Например, одна из страниц может вызвать изменение хранилища, которое приведет к изменениям на другой странице, открытой в соседней вкладке. Круто, не правда ли? Работать с локальным хранилищем просто – это всего лишь key-value структура: localStorage.setItem('name', 'Hello World!'); localStorage.getItem('name'); localStorage.removeItem('name'); Поддерживаемые браузеры: Chrome 5+, Firefox 3.6+, Opera 10+, Safari 4+, IE 8+. <a name="localStorageExtJS4"> Локальное хранилище в Ext JS 4 Четвертый Ext JS позволяет работать с локальным хранилищем прозрачно, предоставляя для него отдельный прокси. Таким образом, Вы просто изменяете тип proxy с ajax на localstorage – и все данные Ext JS хранилища (store) загружаются не с сервера, а из локального хранилища браузера. Погнали? ![]() Структура файлов показана на скриншоте, для нас фактически важны файлы index.html, /app.js и каталог /app/. Четвертая версия Ext JS предлагает использовать для разработки интерфейса модель MVC, будем следовать ей:
Приложение будет состоять из двух виджетов — окна (window) с табицей (grid) внутри. Таблица в зависимости от наличия подключения к Интернет будет использовать серверное или локальное хранилище. Итак, все, конечно, начинается с HTML: <!-- Обратите внимание на указание файла-манифеста Application Cache --> <html manifest="UsersApp.appcache"> <head> <link rel="stylesheet" type="text/css" href="ext-4.0.7-gpl/resources/css/ext-all.css" /> <link rel="stylesheet" type="text/css" href="style.css" /> <script src="ext-4.0.7-gpl/ext-dev.js" type="text/javascript" charset=utf-8quot;utf-8"></script> <script src="app.js" type="text/javascript" charset=utf-8quot;utf-8"></script> </head> <body style="padding: 25px;"><div id="console"><h2>Тестовое приложение</h2></div></body> </html> Здесь нет ничего особенного — указывается файл-манифест с указанием необходимых к кэшированию ресурсов, подключаются стили фреймворка и собственные стили, загружается ядро Ext JS и входная точка приложения app.js. В Ext JS 4 реализован механизм динамической загрузки, позволяющей на лету подключать необходимые JS файлы — таким образом, непосредственно в html прописывается только один JS файл самого приложения app.js. Входной JavaScript файл прост: // включаем динамическую загрузку JS файлов Ext.Loader.setConfig({ enabled: true, disableCaching: false, paths: {UsersApp: 'app', Ext: 'ext-4.0.7-gpl/src'} }); // указываем зависимости, которые необходимо предварительно загрузить Ext.require(["UsersApp.view.win"]); Ext.application({ name: 'UsersApp', launch: function(){ Ext.create("UsersApp.view.win").show(); }, controllers: ["Main"] }); Конструкция Ext.require() предназначена для указания зависимостей, то есть объектов, которые необходимо загрузить предварительно — до запуска приложения вызовом метода launch(). Вообще говоря, если такие зависимости не указать — при настроенном загрузчике Ext.Loader они загрузятся автоматически в процессе выполнения, но это может несколько снизить скорость работы и вообще не является кошерным, Ext.Loader в процессе такой неоптимальной загрузки выдаст сообщение в JS консоль браузера о целесообразности использования Ext.require(). Обратите внимание, что фактически имена объектов соответствуют пути, в которых хранятся эти объекты. Например, объект UsersApp.store.storeLocal хранится в директории /app/store/storeLocal.js, в то время как сопоставление имени приложения UsersApp имени физической директории app задано в настройках загрузчика Ext.Loader. Примечание: механизм динамической загрузки удобен на этапе разработки, в production системе лучше собрать весь JS код в один файл с помощью Sencha SDK Tools (пример) во избежание загрузки большого количества файлов с кодом, генерирования лишних запросов к серверу и т.п. Итак, наше приложение создает виджет UsersApp.view.win и использует контроллер Main. Контроллер всегда вызывается до вызова метода launch(), он выполняет все необходимые подготовительные работы по связыванию компонентов системы. Код окна UsersApp.view.win прост (здесь и далее приведены основные конфигурационные параметры, визуальные конфиги типа высоты-ширины и прочие маловажные моменты можно посмотреть в исходниках по ссылке в конце статьи): Ext.define('UsersApp.view.win', { extend: 'Ext.Window', requires: ['UsersApp.view.grid'], itemId: 'usersWindow', layout: 'fit', items: [ { xtype: 'NamesGridPanel', itemId: 'NamesGrid' } ] }); Здесь мы определяем класс UsersApp.view.win, расширяющий класс стандартного окна Ext.Window, и требующий для себя загрузки UsersApp.view.grid. Код таблицы: Ext.define('UsersApp.view.grid', { extend: 'Ext.grid.Panel', alias: 'widget.NamesGridPanel', requires: ['Ext.grid.plugin.CellEditing', 'Ext.form.field.*'], itemId: 'usersGrid', // конструктор таблицы - будет вызван при создании экземпляра initComponent : function() { // хранилище для таблицы будет установлено в контроллере // устанавливаем возможность редактирования таблицы, для // добавляем плагин CellEditing this.cellEditing = Ext.create('Ext.grid.plugin.CellEditing', { clicksToEdit: 2 }); this.plugins = this.cellEditing; this.columns = this.columnsGet(); this.tbar = this.tbarGet(); // и не забываем вызывать родительский конструктор this.callParent(); }, tbarGet: function(){ return[ { text: 'Добавить', iconCls: 'add', handler: this._onUserAddClick }, { text: 'Удалить', iconCls: 'delete', handler: this._onUserDelClick } ] }, columnsGet: function(){ return [ { text: 'Имя', field: 'textfield', dataIndex: 'firstName' }, { text : 'Фамилия', field: 'textfield', dataIndex: 'secondName' } ] }, _onUserAddClick: function(button){ // код метода добавления новых записей }, _onUserDelClick: function(button){ // код метода удаления выделенной записи } }) Здесь ничего нового — создается класс таблицы, производится настройка колонок (columns), добавляется Toolbar с кнопками добавления/удаления записей (реализация этих методов скрыта для лучшей читаемости кода). Обратите внимание, что к таблице пока не привязано хранилище, это будет сделано в контроллере. Пришло время создать два хранилища — серверное и локальное. Оба эти хранилища будут иметь одну модель (так как они содержат фактически данные одной структуры), но разные прокси. Модель описывается классом UsersApp.model.Names: Ext.define('UsersApp.model.Names', { fields: [{name: 'id', type: 'int', useNull: true}, {name: 'firstName'}, {name: 'secondName'}], extend: 'Ext.data.Model', // имя и фамилия не могут быть пустыми, запретим это validations: [{ type: 'length', field: 'firstName', min: 1 },{ type: 'length', field: 'secondName', min: 1 } ] }); Модель состоит из трех полей — идентификатора человека, его имени и фамилии. Для идентификатора указан целочисленный тип и использован параметр useNull, устанавливающий значение в null, если оно не может быть распознано как целочисленное (в противном случае оно будет приравнено к 0). Также для модели указываются валидаторы — имя и фамилия человека должны быть не короче 1 символа. Создаем хранилище с серверной загрузкой данных: Ext.define('UsersApp.store.store', { extend: 'Ext.data.Store', requires : ['UsersApp.model.Names', 'Ext.data.proxy.Ajax'], model: 'UsersApp.model.Names', proxy: { type: 'ajax', api: { read: 'crud.php?act=read', update: 'crud.php?act=update', create: 'crud.php?act=create', destroy: 'crud.php?act=delete' }, reader: { type: 'json', root: 'names', idProperty: 'id' }, writer: { type: 'json', writeAllFields: false, root: 'names' } } }); Итак, модель с серверной загрузкой использует созданную модель, описывающую структуру данных, и Ajax прокси с настроенным «читателем» reader и «писателем» writer для чтения и записи данных соответственно. Параметр api указывает URL адреса, по которым будет обращаться Ext JS для операций чтения, обновления, добавления и удаления данных. Код локального хранилища: Ext.define('UsersApp.store.storeLocal', { extend: 'Ext.data.Store', requires : ['UsersApp.model.Names', 'Ext.data.proxy.LocalStorage'], model: "UsersApp.model.Names", proxy: { type: 'localstorage', id : 'Names' } }); В качестве прокси указываем localstorage — все данные будут загружаться из локального хранилища. В качестве id указываем уникальный идентификатор прокси, используемый для создания имен в key-value локальном хранилище. Подведем итоги. У нас есть окно, содержащее в себе таблицу с настроенными колонками, но не подключенным хранилищем, есть два хранилища — серверное и локальное — с одной моделью. Нужно связать всё это добро в рабочее приложение! Этим займется контроллер: Ext.define("UsersApp.controller.Main", { extend: 'Ext.app.Controller', requires: [ // утилита проверки наличия связи - "пингует" сервер 'UsersApp.Utils', 'UsersApp.store.storeLocal', 'UsersApp.store.store' ], init: function(){ // метод getStore контроллера возвращает экземпляр хранилища, // если он уже создан - или создаёт его var storeLocal = this.getStore("storeLocal"); var store = this.getStore("store"); // вешаем обработчик на событие загрузки локального хранилища, он будет вызван // сразу _после_ успешной загрузки storeLocal.addListener('load', function(){ // локальное хранилище загружено - самое время // проверить, есть ли связь с Интернет. UsersApp.Utils.ping принимает // в качестве параметров callback функции UsersApp.Utils.ping({ success: this._onPingSuccess, // Интернет есть failure: this._onPingFailure // Интернета нет }, this); }, this); // инициируем загрузку локальное хранилище storeLocal.load(); }, _onPingSuccess: function(){ // сеть есть var win = Ext.ComponentQuery.query('#usersWindow')[0]; var storeLocal = this.getStore('storeLocal'); var store = this.getStore('store'); var grid = win.getComponent('NamesGrid'); win.setTitle("Люди, онлайн") // выясняем количество записей в локальном хранилище localCnt = storeLocal.getCount(); // проверяем состояние локального хранилища, // выясняя, необходима ли синхронизация if (localCnt > 0){ // синхронизация нужна, добавляем записи // по одной из локального хранилища // в серверное for (i = 0; i < localCnt; i++){ var localRecord = storeLocal.getAt(i); var deletedId = localRecord.data.id; delete localRecord.data.id; store.add(localRecord.data); localRecord.data.id = deletedId; } // сохраняем серверное хранилище store.sync(); // очищаем локальное хранилище for (i = 0; i < localCnt; i++){ storeLocal.removeAt(0); } } store.load(); // подключаем к таблице серверное хранилище grid.reconfigure(store); grid.store.autoSync = true; }, _onPingFailure: function(){ // сети нет, работаем с локальным хранилищем var win = Ext.ComponentQuery.query('#usersWindow')[0]; var storeLocal = this.getStore('storeLocal'); var store = this.getStore('store'); var grid = win.getComponent('NamesGrid'); win.setTitle("Люди, офлайн") // устанавливаем хранилище таблицы на локальное grid.reconfigure(storeLocal); grid.store.autoSync = true; } }); Много кода? Пройдем по порядку. В первую очередь для контроллера нужно указать нужные ему зависимости — в нашем случае это внутренние утилиты UsersApp.Utils и два наших хранилища. Метод init будет вызван при инициализации контроллера, то есть до запуска приложения, в нем должны быть выполнены все подготовительные действия. Мы создаем экземпляры хранилищ и загружаем локальное хранилище (оно работает вне зависимости от наличия доступа к сети), предварительно указав callback — после загрузки проверяем наличие сети вызовом метода UsersApp.Utils.ping. Функция ping посылает Ajax запрос к файлу на сервере, и в случае успеха вызывает callback функцию success, в противном случае вызывается failure. Итак, если сеть есть, в серверное хранилище добавляются записи хранилища локального, после чего хранилище таблицы устанавливается на серверное. В случае отсутствия сети хранилище таблицы просто устанавливается на локальное. Пример работы можно посмотреть здесь. Исходники (в качестве серверной части PHP, писал еще на Java — если кому надо, выложу) здесь. PS. Ценителей с новым альбомом Руставели — и тёплых зимних программерских вечером Вам:) Original source: habrahabr.ru (comments, light). Читать дальше -------------------- |
|
|
![]() ![]() |
Текстовая версия | Сейчас: 17.6.2025, 16:43 | |
|