【翻译】在Sencha Touch中创建离线/在线代理

原文:Creating an Online/Offline proxy in Sencha Touch

概述

在Sencha Touch中,一个常见的需求就是,当设备在没有连接互联网的时候,应用程序必须能够继续工作。Sencha Cmd为实现应用程序离线工作提供了一切所需的工具,如自动生成应用程序清单文件,不过,这其中最大问题是如何处理数据。有许多方式可以用来处理数据,而一个常用的技术就是在本地存储代理和AJAX代理之间实现切换。

在本文,ProWeb软件公司的Tom Cooksey将展示如何使用一个代理来实现类似的效果,而且该代理的存储配置对于使用它的程序员来说是完全透明的。

代理

在示例中,虽然是从AJAX代理扩展的,但你可以根据所好,从所喜欢的代理中进行扩展,不过使用AJAX是比较常见的需求,因而使用了这个。在代理中,除了要重写两个方法来处理数据流外,还需要创建几个配置项,这个后面将会进行说明。以下是将要创建的不包含任何逻辑的骨架类:

/**
 * Offline Proxy
 * @extend Ext.data.proxy.Ajax
 */
Ext.define(‘proxy.OfflineProxy‘, {

    extend: ‘Ext.data.proxy.Ajax‘,
    alias: ‘proxy.offline‘,

    config: {
        storageKey: null,

        storageFacility: null,

        online: true
    },

    originalCallback: null,

    /**
     * Override doRequest so that we can intercept the request and
     * catch a failed request to fall back to offline
     * @param operation
     * @param callback
     * @param scope
     * @returns {*}
     */
    doRequest: function(operation, callback, scope) {

    },

    /**
     * Override processResponse so that if we are online we can store the response
     * into the offline storage method provided and if a response fails,
     * we can fall back.
     * @param success
     * @param operation
     * @param request
     * @param response
     * @param callback
     * @param scope
     */
    processResponse: function(success, operation, request, response, callback, scope) {

    }

});

方法doRequest的实际用途是用来执行服务器请求的。要重写该方法是因为在这里需要判断设备是离线,还是不能访问服务器,以及是否需要将伪响应返回存储。

方法processResponse是用来解释服务器响应的,重写改方法的主要原因是除了要保留所有原始功能外,还需要将成功获取的数据存储到存储设施。如果获取数据不成功,还需要告诉代理让它再做一次,不过不是使用上面所说的伪响应。

存储设施

代理需要传递一个存储设施供它使用,在这里,它只是一个带有getItem和setItem这两个方法的单例类。也可以使用其他的任何存储设施,不过必须与示例相同的方式来实现API,它才能工作:

/**
 * A class that gives access into WebSQL storage
 */
Ext.define(‘storage.WebSQL‘, {

    singleton: true,

    config:{
        /**
         * The database capacity in bytes (can‘t be changed after construction). 50MB by default.
         */
        capacity:50 * 1024 * 1024
    },

    /**
     * @private
     * The websql database object.
     */
    storage:null,

    connected: false,

    constructor: function (config) {
        this.callParent(config);

        this.storage = openDatabase(‘storage‘, ‘1.0‘, ‘Offline resource storage‘, this.getCapacity());

        this.storage.transaction(function (tx) {
            tx.executeSql(‘CREATE TABLE IF NOT EXISTS items (key, value)‘);
        }, function (error) {
            console.error(‘WebSQL: Connection Error‘);
        }, function () {
            console.log(‘WebSQL: Connected‘);
        });
    },

    /**
     * Get an item from the store.
     * @param key The key to get.
     * @param callbacks object of success and failure callbacks
     */
    getItem:function (key, callbacks) {

        this.storage.transaction(function (tx) {
            tx.executeSql(‘SELECT * FROM items WHERE key = ?‘, [key], function (tx, results) {

                var len = results.rows.length;

                if (len > 0) {
                    callbacks.success(results.rows.item(0).value)
                } else {
                    callbacks.failure(); // no result
                }
            });
        }, function (error) {
            console.log(‘WebSQL: Error in getItem‘);
            callbacks.failure(error);
        });
    },

    /**
     * Set an item in the store.
     * @param key The key to set.
     * @param value The string to store.
     * @param callbacks object of success and failure callbacks
     */
    setItem:function (key, value, callbacks) {

        this.storage.transaction(function (tx) {
            //remove old version first
            tx.executeSql(‘DELETE FROM items WHERE key = ?‘, [key]);
            tx.executeSql(‘INSERT INTO items (key, value) VALUES (?, ?)‘, [key, value]);
        }, function (error) {
            console.log(‘WebSQL: Error in setItem:‘ + error.message);
            callbacks.failure(error.message);
        }, function () {
            callbacks.success(); // no value.
        });
    }
});

在这里,没什么特别的东西,但要注意的是setItem和getItem方法都要在成功或失败的时候执行回调。另外,在构造函数中创建了SQL数据库,不过这对于一些诸如本地存储这样的简单存储就不需要了。

下面来深入探讨一下setItem方法是如何工作的:

setItem:function (key, value, callbacks) {

        this.storage.transaction(function (tx) {
            //remove old version first
            tx.executeSql(‘DELETE FROM items WHERE key = ?‘, [key]);
            tx.executeSql(‘INSERT INTO items (key, value) VALUES (?, ?)‘, [key, value]);
        }, function (error) {
            console.log(‘WebSQL: Error in setItem:‘ + error.message);
            callbacks.failure(error.message);
        }, function () {
            callbacks.success(); // no value.
        });
    }
});

在这里将会将要设置的key(来自于代理的存储键)、新的值(在当前示例是序列号的JSON对象)和一个包含了回调的对象作为参数接收。代码将根据键值删除旧的引用并插入新的值。

以下这几行:

tx.executeSql(‘DELETE FROM items WHERE key = ?‘, [key]);
            tx.executeSql(‘INSERT INTO items (key, value) VALUES (?, ?)‘, [key, value]);
 

对应的使用本地存储的代码是:

localstorage.removeItem(key);
localstorage.setItem(key, value);

如果该事务执行成功,就要调用传递过来的success回调,否则则调用error回调。

方法getItem的工作方式与之类似:

getItem:function (key, callbacks) {

        this.storage.transaction(function (tx) {
            tx.executeSql(‘SELECT * FROM items WHERE key = ?‘, [key], function (tx, results) {

                var len = results.rows.length;

                if (len > 0) {
                    callbacks.success(results.rows.item(0).value)
                } else {
                    callbacks.failure(); // no result
                }
            });
        }, function (error) {
            console.log(‘WebSQL: Error in getItem‘);
            callbacks.failure(error);
        });
    }
 

在这里,只有key和callbacks两个参数。参数key是用来检索数据的,如果找到数据就调用success回调并返回数据,否则,调用error回调。

最终的代理

现在,已经有了存储设施,可以来完成代理了。要实现这个,需要在定义proxy配置项的时候将存储设施传递给它。

doRequest: function(operation, callback, scope) {

        var that = this,
            passCallback,
            request,
            fakedResponse = {};

        this.originalCallback = callback;

        function failedRequest() {
            fakedResponse.status = 500;
            fakedResponse.responseText = ‘Error‘;
            fakedResponse.statusText = ‘ERROR‘;

            that.processResponse(false, operation, request, fakedResponse, passCallback, scope);
        }

        if(this.getOnline()) {
            console.log(‘PROXY: Loading from online resource‘);
            return this.callParent(arguments);
        }else{
            console.log(‘PROXY: Loading from offline resource‘);
            request = this.buildRequest(operation);
            passCallback = this.createRequestCallback(request, operation, callback, scope);

            if(this.getStorageKey() && this.getStorageFacility()) {

                this.getStorageFacility().getItem(this.getStorageKey(),  {
                    success: function(dataString) {

                        fakedResponse.status = 200;
                        fakedResponse.responseText = dataString;
                        fakedResponse.statusText = ‘OK‘;

                        that.processResponse(true, operation, request, fakedResponse, passCallback, scope);

                    },
                    failure: failedRequest
                });
            }else{
                console.error(‘No storage key or facility for proxy‘);
                setTimeout(function() {
                    failedRequest();
                }, 1);

            }

        }

    },
 

要重写的第一个方法是doRequest方法。在原来的AJAX类,该方法用来处理实际的服务器请求,而在这里,当设备是在线的时候,将使用callParent方法来调用父类的方法,而如果设备是离线状态,则生成伪响应,从离线存储设施来获取数据。要生成伪响应是因为processResponse方法会分析传递给它的数据以确保是合法响应。伪装的方法是设置正确的http状态代码(200),设置responseText为从存储设施返回的数据,以及设置statusText为OK。伪装后的对象会让processResponse方法认为这是正常的请求响应。这种抽象方法是Sencha框架非常擅长且用来解耦代码的好东西。

processResponse: function(success, operation, request, response, callback, scope) {

        var that = this;

        if(success) {

            console.log(‘PROXY: Request succeeded‘);

            this.callParent(arguments);

            if(this.getOnline()) {
                if(this.getStorageKey() && this.getStorageFacility()) {
                    this.getStorageFacility().setItem(this.getStorageKey(), response.responseText, {
                        success: function() {
                            console.log(‘PROXY: Data stored to offline storage: ‘ + that.getStorageKey());
                        },
                        failure: function(error) {
                            console.log(‘PROXY: Error in storing data: ‘ + that.getStorageKey());
                        }
                    });
                }else{
                    console.error(‘PROXY: No storage key or facility for proxy‘);
                }
            }

        }else{
            if(this.getOnline()) {
                //If the request failed and we were online, we need to try and fall back to offline
                console.log(‘PROXY: Request failed, will try to fallback to offline‘);
                this.setOnline(false);

                this.doRequest(operation, this.originalCallback, scope);
            }else{
                this.callParent(arguments);
            }
        }

    }

第二个要重写的方法是processResponse方法。同样,在正常情况下,当服务器请求成功后,将调用callParent方法,除此之外,还要将请求返回的数据保存打离线存储设施。

在该处理过程中有几个阶段。首先,如果请求的success标志为true(即是从服务器得到了一个有效响应),则要检查代理的配置项online。该值可以在代理初始化的时候传递给代理的。或者,代理可以默认设置该值为true,直到请求失败的时候,再将设备置于离线状态。如果标志为true且存储设施存在,则存储数据并返回。每当请求成功的时候,都需要这样做,这样,每当设备离线的时候,就可以在这个时候访问到最后的数据。

如果请求失败,则设置标志online为false并重新运行doRequest方法,这时候,online标志的值为false,就可以从存储设施返回数据了。

综合使用

当将proxy设置为上面定义的存储的时候,就可以将他们糅合在一起了:

proxy: {
            type            : ‘offline‘,
            url             : ‘/test-api/test-resource.json‘,
            storageKey      : ‘buttons‘,
            storageFacility : storage.WebSQL,

            reader : {
                type         : ‘json‘,
                rootProperty : ‘data‘
            }
        }
 

正如所看到的,将type设置为offline意味着jangle代理的别名设置为proxy.offline。配置项storageKey就是将请求返回的数据存储到离线存储的键值。在当前示例中,由于存储被定义为buttons,因此存储的键值使用了相同的名字。存储设施(storageFacility)就是上面创建的类,而其他的配置与标准的代码配置没有任何区别。

结果

为了演示这些代码,我们开发了一个Sencha Touch的演示应用程序。此外,下面还有一个屏幕截图。该演示应用程序包含一个工具栏,而它的内容则由服务器端的JSON文件决定。

在第一张图可以看到按钮已经生成了,而咋控制台,可以观察到数据已经被存储到离线存储。

在第二张图,test-resource.json文件已经不能加载了。在这里,只是修改了一下文件名,因此返回了404错误(这意味着设备不能再访问互联网或服务器已经宕机等等)。从控制台日志可以看到,作为替代,从离线版本加载了数据,而按钮也成功加载了。

小结

Sencha类系统的灵活性意味着很容易去扩展和重新利用内置组件和内置功能。通过示例就已经证明了,潜在的困难可以通过挂入已明确定义的工作流和简单添加所需功能来轻易解决。其结果就是可以在保留原有代理的强大功能的同时,添加所需的离线功能,并让开发人员可以完全透明的去使用它。

作者:Tom Cooksey
Tom is the CTO of ProWeb Software, a UK-based Sencha Partner providing dedicated Sencha resources and development internationally. He has extensive experience building web and mobile apps, using Sencha frameworks, JavaScript and Node.js, that have a complex system architecture and compelling user interface.

时间: 2024-10-12 07:35:26

【翻译】在Sencha Touch中创建离线/在线代理的相关文章

【翻译】在Ext JS和Sencha Touch中创建自定义布局

原文:Creating Custom Layouts in Ext JS and Sencha Touch 布局系统是Sencha框架中最强大和最独特的一部分.布局会处理应用程序中每个组件的大小和位置,因而,不需要手动去管理那些碎片.Ext JS与Sencha Touch的布局类有许多相似之处,最近在 Ivan Jouikov的这篇博文中对他们进行了详细的分析. 虽然是这样,但很多Ext JS和Sencha Touch开发人员可能永远都不会去了解布局系统的机制原理.Sencha框架已经提供了最常

sencha touch 中获取picker的text值

标签: Sencha Touch [1].[代码] [HTML]代码 跳至 [1] ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 {                                               xtype:'picker',                        id:'picker',                        slots: [            

sencha touch 项目创建

点击开始==>运行==>cmd, 找到sdk路径 输入sencha cmd创建项目指令: sencha generate app HelloWorld ../helloworld generate app==创建一个新的项目, 第一个参数HelloWorld==对应的是项目的名称, 第二个参数../helloworld对应是项目的输出地址,是相对路径,指向是cmd当前指定路径的上级目录. 创建成功后在可以看到创建成功的helloworld项目. 运行HelloWorld项目:

【翻译】探究Ext JS 5和Sencha Touch的布局系统

原文:Exploring the Layout System in Ext JS 5 and Sencha Touch 布局系统是Sencha框架中最强大和最有特色的一个部分. 布局要处理应用程序中每一个组件的尺寸和位置.在Ext JS和Sencha Touch直接有很多相似之处.尤其是如今Ext JS 5開始支持平板更是如此.以下让我们来探讨一下布局系统是怎样跨域Sencha框架进行工作的. 布局简史 最主要的HTML一直都缺乏一个严格定义的布局系统. 很多年来,因为CSS实现的差距.开发跨浏

用 Sencha Touch 构建移动 web 应用程序

Sencha Touch 是一个使用 HTML5.CSS3 和 JavaScript 语言构建的移动 web 应用程序框架,在本文中,学习如何应用您当前的 web 开发技能进行移动 web 开发.下载和建立 Sencha Touch,通过一个样例应用程序探究基本原理.学习开始使用 Sencha Touch 框架所需的一切 2012 年 3 月 19 日 内容 概述 Sencha Touch 准备开始 UI components 结束语 参考资料 评论 概述 在软件开发领域中,有两个重要的趋势越来

使用 crosswalk-cordova 打包sencha touch 项目,再也不用担心安卓兼容问题!

国内的安卓手机品牌众多,安卓操作系统碎片化也很严重,我们使用sencha touch 开发的应用不可避免的出现了各种无解的兼容性问题. 有时候我就在想,有没有既能支持cordova,又能让我们把Chromium内核打包到应用里面的平台呢?这样就不用担心兼容性了. 最近阿赛向我推荐了Crosswalk,满足了我的愿望,不过可惜的是,Crosswalk仅支持Android4.0+.并且这个是将Chromium内核打包到了应用中,所以安装包略大一些. Crosswalk是基于Chromium内核打造的

HTML5开发移动web应用——Sencha Touch篇(8)

DataView是Sencha Touch中最重要的组件,用于数据的可视化.数据可视化的重要性不言而喻,可以讲任何数据以形象的方式展示给用户.目前,如何更好地可视化是许多公司或框架都在追求的.通过数据的可视化可以发现数据之间的规律,预测未来的情况.下面我们就看看Sencha Touch中是怎么进行数据可视化的. 由于DataView组件内容非常多,所以将在以后的一段时间内持续这一部分的学习. 首先废话不多说,直接上使用DataView组件的代码框架. launch:function(){ var

再探 Ext JS 6 (sencha touch/ext升级版) 变化篇 (编译命令、滚动条、控制层、模型层、路由)

从sencha touch 2.4.2升级到ext js 6,cmd版本升级到6.0之后发生了很多变化 首先从cmd说起,cmd 6 中sencha app build package不能使用了,sencha app build native好像也不能用了. 有个好消息就是我们可以用sencha ant native buildsencha ant package build 这两个命令,目测和以前的效果差不多了 然后再说说ext js 6相对sencha touch 2.4.2的变化 首先最只

sencha touch打包成安装程序

为了更好地向大家演示如何打包一个sencha touch的项目,我们用sencha cmd创建一个演示项目,如果你的sencha cmd环境还没有配置,请参照 sencha touch 入门系列 (二)sencha touch 开发准备 进行配置. 首先在开始->运行中输入cmd,回车,打开命令行工具,cd指令进入到我们的sencha touch的sdk目录中,执行项目创建的指令: 此时在webtest路径中便创建了一个testPackage的项目,我们打开项目中创建生成的packager.js