《ExtJS详解与实践》阅读补充资料:单页面应用程序的设计

在一般的Web
GUI

中,每个应用都分散在一个页面中,会随着页面的跳转而反映在浏览器的地址栏上;稍微复杂的基于Web

系统中,都采用划分Frame

元素或打开浏览器新窗口的方式来组织页面,从浏览器的地址看起来,虽然只有一个地址,但是子Frame

的页面还是会整张页面地刷新。AJAX

改变了以往一张页面一次请求的模式,可以允许在同一张页面发起各种的请求,这样我们对于页面的组织形式有了新的途径。在单页面GUI

模型中,主页面是可以独立加载、更新和替换的一些可视元素的组合。通过这种方式,可以不必在每次用户操作后重新加载整个页面。在任何时候,都只显示与应用程序当前阶段相关的可视元素和内容。其他所有内容均被隐藏;但只要应用程序流程中需要用到它,它就会显示出来。

单页面GUI

与传统页面组织的本质区别在于,单页面只允许一个document

对象存在,一切UI

组件的根节点就是这个document

对象,程序所有
UI

的渲染任务均在这个页面内完成,即使是表单提交的任务也不需要作页面转向(Redirect

)。图一是单页面GUI

的对象组织示意图:

 

图一

单页面GUI

的DOM

内部结构

Ajax

技术的出现给页面带来了一些变化,其中最直观的莫过于站点的页面上出现越来越多的“loading

…”

,“正在加载中……”等提示信息,忽如一夜春风来,loading

加载处处开的意思。“loading

…”

或者“正在加载中……”表示浏览器正在与服务器之间进行交互,交互完成之后,将对页面进行局部刷新,这种交互模式虽然简单却极大的提高了Web

应用的用户体验,使单页面GUI

的设计成为可能。

单页面GUI

为我们带来了什么?

单页面GUI

的定义是相对于多页面概念(Multi-page

)的,两者的对比略举如下:

  • 多页面相对更容易。多页面的设计是你熟悉的开发理念,而且浏览器前进、后退或收藏的问题是从来不用考虑的
  • 多页面载入的时间更快。把程序的每一个模块分散的多个页面中,浏览器负荷小,加载时间短
  • 单页面提供更高级的历史记录;针对浏览器的历史记录跳转,有专门负责控制的脚本,如RSH

    、Roll-your-own

    。 

  • 单页面提供更快速的渲染 
  • 单页面使得共享UI

    组件更轻松。组件定义在同一页面中,调用更方便

单页面GUI

的应用情况

曾经有一个实际案例是,把74

张JSP

页面转化为单个页面,利用Java

下常用的Ajax

框架DWR

向负责后台的通讯,结果是450

行的HTML

和200

行的CSS

。有许多的Ajax

程序采用单页面的方案,典型的有Google

下的一系列在线应用,比如GMail

、Reader

、Maps

等,
雅虎的Oddpost

和微软的Kahuna

、Start

等。以上的应用都是大规模的Web

应用,如果在实际的项目开发中,单页面GUI

很难说一定比多页面的设计好,因为在传统开发模式大背景下,多数开发者希望利用IDE

或类似WinForms/Swing

的GUI

画出”控件这样强大的支持来解决表示层的方案,以提供工作效率;另一方面,大量JavaScript

投入项目产生,开发者会因JavaScript

的困惑而对项目总体把握度而大打折扣,因此开发人员对于哪些逻辑可以在客户端执行应该有一个清醒的认识。单页面实施起来可以说难度更高,但从后期的维护工作量和用户体验来讲,效果会更好。

动态资源下载

单页面GUI

意味着页面内所有功能会重新地被规划安排。通常,这个规划过程会被开发人员理解为所必需的模块化,因为通过其可以方便地进行调试和编码。当网页内的各种资源,包括HTML

、图片、脚本程序等的资源数量或体积特别大的时候,我们必须采取一定策略来优化资源的加载,例如HTML

内容过长我们可以分页,图片体积过大可以采用更高压缩比的格式或缩小尺寸等的手段优化,从而在同一时刻内,使得浏览器保持在一个合理的资源调控,和带来较好的用户体验。另一个方法减少初始下载是,对页面的内容部分进行延迟加载。

脚本程序属于网页资源中特殊的一种。传统多页面的设计下,每个页面需要的脚本文件会按照功能上划分而有所不同。这就需要使用者在页面上手动加入标签装载指定的脚本资源。随着脚本数量的增多,如果都要为这些文件去管理脚本的引用标签,组织页面,将会是一件痛苦的事情,尤其使用者在不清楚类库之间的依赖关系、加载先后顺序的情况下,更容易出现错误。最直截了当的解决办法是将常用的类库资源打包到单一的框架文件中,作为一个完整的加载资源出现。Ext

也是采用这种策略(ext-all.js

)。实际上,如果采用单页面GUI

的方案(One
Page One
Application

,简称OPOA

),——即所有的任务均在同一页面内完成的方案。相对而言,这种方案即使不过多关心如何按需加载的,也是情有可原的。当然从页面运行效率而言,完整加载的方式是有害的,因为同一个页面上基本不会用到所有的组件,简单说,用户有80%

的功能不会用到“那部分”的函数,浏览器却加载了,造成了不必要的资源、带宽(Bandwidth

)占用。

当今JavaScript

发展在加快,体积也随着加大,在功能与体积相矛盾的情况下,按需加载是行之有效的策略方式。按需加载又称动态加载、On-Demand

加载,意思都是相近的。目前按需加载常采用的主要有三种方式:

1.  即时同步加载式:

   
    此加载方式是利用XHR

(XMLHttpRequest

)对象,设置Open()

方法的第三个参数为true

,设置通过同步方式下载脚本。若采用了同步(synchronous

)通讯的设置,浏览器在内容未下载完毕之前,此时的readyState

状态属性是2

、3

之间,是一直处于等待的状态,渲染其他网页元素的任务亦伴随停止,包括渲染DOM

元素、加载图片、停止响应用户事件等的任务。因此,在加载所需JavaScript

文件刚好是比较慢的网络环境,时间一长浏览器就会变得好像僵硬(Freeze

)的状态,甚至最大化、最小化浏览器的操作用户都难以控制。虽然即时同步加载方式的算法实现起来不太困难,但主要的弊端是在非内网下获取资源时极容易导致浏览器的阻塞,尤其在网络速度较慢的情况下。使用此方式的库有早期的Dojo

、JSVM

等。

 

图二

同步加载:浏览器内的资源按顺序载入

2.    

异步加载式:

   
    异步加载式同样是使用XHR

对象进行资源的加载,但通讯方式改为异步。其特征是使用了eval

函数执行脚本。使用此方式加载需要指定函数所处于的作用域链(Scope
Chain

),因为我们知道eval

函数执行时是根据“就近原则”的,即声明的成员在当前函数作用域下是有效可被访问的;若需要应用到全局范围,这需要更技巧性的脚本编程,可参见modules.js

的做法。另外,由于采用了异步加载的方式,处理库内部之间的依赖关系(dependency

)会变得比较复杂,典型的库有新版Dojo
1.x

中的包加载机制。

3.    

异步加载式之动态标签:

   

有时候,仅在用户呼叫出某个功能的这个时候才加载应用程序的相应内容。这样就把若干的功能组合成为一个大的模块。我们不是把单个功能分散都做异步下载,因为这样的划分颗粒度过于细小了,而是把若干的功能组合在一起,用户一触发UI

的事件就动态下载所属模块,已经下载过的就不再重复。利用DOM

动态载入外部JavaScript

文件也是一种解决之道,这样的做法会更适合单页面GUI

的设计。

   

具体地说,我们首先把每一个模块都划分一个单独的脚本、CSS

资源,比如博客模块、论坛模块、商城模块等等……规划好之后,用EXT

制成一个全局的导航布局,把对应的模块功能都放在布局上。这里的安排并不是把所有的模块都给放上,而只是列出对应的菜单、对应的按钮……好了,有了这些菜单、按钮,我们的设计目的是,只要有用户按下这些控件的时候,浏览器就会下载那块功能对应的资源;如果是重复的就不用二次下载(脚本应能识别用户操作哪些是重复的)。

按需下载包文件

前面

谈到的动态资源下载方式有三种,与前两点比较,第三点是重点,也是本应用实例所使用原理。之前我们从最基础的内容说起,现在我们就在loadContent()

的原理基础上再进一步扩展,形成moduleLoader

类:

/**
 * 前端模块异步加载器。
 * 依赖ext的createCallback、createDelegate函数
 * @class   moduleLoader
 * @extends Object
 * @constructor
 * @param {Object} config 配置属性对象
 */
moduleLoader = function(config){
       // 复制属性到当前实例
    Ext.apply(this, config);
 
       /**
        * @property {Object} action 该模块的处理函数,类型为hash
        */
        
       /**
        * @property {Array} script 要异步下载的脚本列表
        */
      
       /**
        * @property {Array} style 要异步下载的样式列表
        */   
        
    /**
     * @propety {Boolean} loaded True表示当前资源已加载到浏览器渲染。此项是为了不会重复下载资源时的判读根据。只读的。Read-Only
     */
    
    this.loaded = false;
   
    // 鉴于下面使用的createCallback()方法没有指定scope的地方,所以在这里先绑定
    var moduleDetect = this.moduleDetect.createDelegate(this);
    for (var i in this.action) {
        var oldFn = this.action[i]; //原有的函数
        // 函数作为值送入createCallback方法,返回的类型是Function。作用是创建回调函数。
        this[i]   = moduleDetect.createCallback(oldFn);
    }
}
// 实例方法
moduleLoader.prototype = {
       /**
        * @private
        * @param  {Fucntion} onSuccessHandler “按需加载”完成后的回调函数
        * @param  {Object}   Scope            作用域
        */
    moduleDetect: function(onSuccessHandler, scope){
        if(this.loaded === false) {
            moduleLoader.load({
                script     : this.script
                ,style     : this.style
                ,onSuccess : onSuccessHandler
            });
            this.loaded = true;
        }
        else {
               // 如果已经下载过直接执行。
            onSuccessHandler.call(scope);
        }
    }
}

 

moduleLoader

还需要依赖一个静态方法load()

,负责加载脚本、样式。此方法是静态的因此也可以独立的使用。

 

 

/**
 * 局部加载JS或CSS文件
 * @static method
 * 静态用法:
 * <code>
       ModuleLoader.prototype.load({
              script : ['/ajaxee/test.js','/ajaxee/test2.js'],
              style : ['/ajaxee/test.css']
       })</code>      
 * @cfg {String} path The URL to request
 * @cfg {Function} onSuccess
 * @cfg {Object} scope
 */
moduleLoader.load= function(path){
    var dom;
       if(path.script){
              if(!path.script.pop)path.script = [path.script];
           for (var i = 0, j = path.script.length; i < j; i++) {
               dom     = document.createElement("script");
               dom.src = path.script[i];
                  document.getElementsByTagName("head")[0].appendChild(dom);
           }
           // 兼容IE、非IE浏览器的判断
           dom[Ext.isIE ? "onreadystatechange" : "onload"] = function(){
               if (this.readyState && this.readyState == "loading")
                   return;
                     dom = null; 
               if(path.onSuccess)path.onSuccess.call(path.scope, path.script);
           }
       }
       if(path.style){
              if(!path.style.pop)path.style = [path.style];
           for (var i = 0, j = path.style.length; i < j; i++) {
               dom      = document.createElement("link");
                     dom.type = "text/css";
                     dom.rel  = "stylesheet"
               dom.href = path.style[i];
                  document.getElementsByTagName("head")[0].appendChild(dom);
           }    
       }
}

 

熟悉了上面相关的类之后,下面我们以某个OA

项目中的地址簿为例子,建立该模块的功能管理者AppMgr

。我们只要实例化一次(头一次加载成功后就不再加载了),保存到全局变量中,也就是“单例”的形式创建对象。实际上这也是对该模块下各个功能先作一个分配。有了这种前期的思路后,我们写好的这个AppMgr

实例便是封装每个模块的公共属性和方法(如例子OA.Client.AddressBook.AppMgr

中的action

),而且还要定义模块所需的资源文件,说明按需加载的JS/CSS

文件是哪些。

OA.Client.AddressBook.AppMgr = new moduleLoader({
    script: [
              'Client/AddressBook/panel.js',
              'Client/AddressBook/windows.js'
],
    style: [
              'Style/AddressBook/default/AddressBook.css'
],
// 各种UI行为,开发者自己定义……
       action : {
              openFrontPage: function(){
                     App.mainTabPanel.addTab(new OA.Client.AddressBook.frontPage(), true);
              },
              openMainGrid : function(){
                     App.mainTabPanel.addTab(new OA.client.Portal.masterGrid());                    
              },
              openCommentWindow: function(){
                  (new Ext.Window({
                      title: "测试对话框",
                      iconCls: 'AppIcon_Comment_16x16',
                      resizable: false,
                      autoDestroy: true,
                      closeAction: 'close',
                      width: 610,
                      height: 400,
                      items: new OA.Client.Comment.Browser()
                  })).show();                                
              },   
              openConfigWindow: function(){
                     var Index = new OA.Client.Admin.frontPage();
                     App.mainTabPanel.addTab(Index, true);
                     Index.show();
              }
       }    
});

 

有了这个单例后,我们就可以在UI

上面分配该模块的各种方法。大多数Web

系统都会包含功能菜单和显示页面,功能菜单可以是UI

左面的一棵树,也可以是一个可以切换的踏板标签页,而显示页面无非就是一块显示得区域,点击相应的功能菜单,切换不同的内容。现在我们可以结合到一个按钮上、结合到菜单上、结合到树上……总之可以制定事件的地方就可以分配触发该功能的函数。下面我们就以一个树的根节点为示范例子,说明如何分配UI

模块:

// 创建树的根节点
var rootNode = new Ext.tree.TreeNode();
rootNode.appendChild(new Ext.tree.TreeNode({
       iconCls  :'oa-tree-myDesktop-Admin', //图标样式
       // 登记点击该节点时的事件
       listeners: {
              'click': OA.Client.AddressBook.AppMgr['openFrontPage']  // 该值的类型是Function函数。因此可以将功能分配给当前click事件的处理函数
       },
       text: '考勤事务'
}));
 
rootNode.appendChild(new Ext.tree.TreeNode({
       iconCls  :'oa-tree-myDesktop-Admin', //图标样式
       // 登记点击该节点时的事件
       listeners: {
              'click': OA.Client.Admin.AppMgr.openFrontPage
       },
       text: '考勤事务'
}));
 
// …………更多的树节点

小结

我们还本文中尝试在Ext实现“单一页面”的程序设计。通过内嵌页面iframe和传统的页面跳转方法,虽然可以实现数据的定位或功能的切换,但是遗憾的是在全面引入AJAX方案后这样的方式不够强大和灵活。本文同时也向大家介绍如何在单页面的基础上提供非跳转或iframe的GUI设计,以提供更合理的用户体验及彻底的按需加载方案。

 

此处披露的内容是《ExtJS 3详解与实践》
的补充内容,完整的资料敬请参阅《ExtJS 3 详解与实践》
一书的全面介绍。

 

时间: 2024-09-09 20:54:03

《ExtJS详解与实践》阅读补充资料:单页面应用程序的设计的相关文章

《ExtJS详解与实践》阅读补充资料:Grid如何高/宽自适应

Grid高度自适应是许多用户开发过程中碰到过的问题.问题在于,尽管本类是由Panel类继承而得到的,但是不支持其基类的某些功能,所以不能都做到好像一般Panel类那样的方法来解决,如autoScroll.autoWidth.layout.items等-- Grid需要指定一个宽度来显示其所有的列,也需要一个高度来滚动列出所有的行.这些尺寸都通过配置项BoxComponent.height和Ext.BoxComponen.width来精确的指定,又或者将Grid放置进入一个带有某种布局风格的容器中

《ExtJS 3详解与实践》阅读补充资料:编写Hello World

    使用Ext编写Ajax应用程序时,初学者往往都会感到迷惑:到底应该怎样编写Ajax程序?事实上,每个初学者都会遇到这种情况--不知该如何下手,有时只是因为一点点设置不对就卡住了整个程序的运行,连HelloWorld也可能成为新手的拦路虎.为了帮助新手尽快消除这种困惑,我们这里先为新手准备一份详尽的启动文件清单,说明清楚运行该框架的最基本条件到底是哪些:然后再简单地跑一趟对话框MessageBox作为Hello World.首先是对这份HTML文件的详解:   <!-- 标识html开始

《ExtJS 3详解与实践》阅读补充资料:Ext.extend()中使用super关键字

  既然一门语言被精简了,无论idea还是直观的语法,都务求精简的话,那么这便无形就是一个趋势,趋势往往不为人们的意志转移地转为自己的习惯,思维定性的习惯,连function这个关键字也有某仁兄觉得太长了,有缩减的必要.当然这只是开玩笑而已了.   好像Lisp那样满天 点号.冒号便是灾难.用过Ext继承的人都清楚,每每调用父类成员的时候就是Ext.subClass.superclass.methodName.call/apply(this).一整串的长,好处也是明显的,起码这种完全命名方式一个

《ExtJS 3详解与实践》阅读补充资料:capture()捕获事件

静态方法Ext.util.Observable.capture()是一项有趣的功能,它能够将一项事件进行捕获,跟踪该事件发生的经过.捕获事件就是观察Ext JS事件的调用过程.只要是继承了 Ext.util.Observable的组件,调用capture(),便可得知该组件何时何地怎样响应事件,也算是调试组件时的技巧. // 假设已有一个名为'myWindow'的UI组件,用Ext.getCmp()返回该对象. Ext.util.Observable.capture(Ext.getCmp('my

《ExtJS 3详解与实践》阅读补充资料:用BoxComponent制作Logger UI

如果要求的UI控件不需要其他的细节的控件,也就是,仅仅是封装某部分的HTML元素的话,还要听凭布局管理器提供的大小尺寸.布局的调控,那么这个的扩展对象就是Ext.BoxComponent.例如,假设一个Logger类打算是简单地显示log信息,就可以这样定义: Ext.ns('Ext.ux.Logger'); Ext.ux.Logger = Ext.extend(Ext.BoxComponent, { tpl: new Ext.Template("<li class='x-log-entr

javascript凌厉开发:extjs3详解与实践的光盘代码

问题描述 javascript凌厉开发:extjs3详解与实践的光盘代码 光盘不见了,网上压根就没这资源, 书中的代码都是片断 求助... 解决方案 重买一本或者联系作者发源代码,你的图书上应该有联系方式

《金蝶ERP-K/3完全使用详解》——6.2 产品预测单

6.2 产品预测单 金蝶ERP-K/3完全使用详解 产品预测单是指企业为满足市场和销售需要,根据公司以往的历史数据,如产品市场情况,以往年度或月份的产品销售情况,制订在未来一段时间内需要什么产品.需要多少数量的计划,以便公司安排生产什么产品.生产多少数量.什么时候生产完工的预测单据.产品预测单的作用是指导生产部门进行生产准备和生产,采购部门进行采购准备.预测单通常应用于"以预测作为计划"为主的企业使用. 6.2.1 产品预测单录入 产品预测单的录入方法基本同"销售管理&quo

《Android游戏开发详解》一2.3 编写第一个程序

2.3 编写第一个程序 Android游戏开发详解在选择了工作区之后,Eclipse将会打开,并且你将会看到图2-7所示的欢迎界面. 现在,我们已经准备好了IDE,可以开始编写第一个Java程序了.由于还没有构建任何的Android应用程序,我们可以安全地退出这个标签页.如图2-8所示. 完成之后,我们将可以访问几个不同的视图.现在,只需要关心其中的2个视图:Package Explorer 和Editor Window.如图2-9所示. 2.3.1 创建一个新的Java项目 我们终于开始编写第

《Ext详解与实践》简介

内 容 简 介 富客户端程序RIA 使Web表示层的技术向前迈进了一大步,开创了图形化编程的新一代先河.在它的带动下,许多优秀的RIA开发方案相继问世.这些开发方案各有个秋,但它们都或多或少地从传统桌面程序开发中汲取了营养.随着前端技术的不断进步,以及JavaScript 引擎的速度改善,基于Ajax 方案的Ext JS也在不断进步.Ext JS及与之相关的GXT.Ext SHARP等开发工具的推出,使快速开发.基于可视化快速开发工具又向前迈进一大步. 本书以学习Ext JS的开发人员为基本读者