引言
简介
JSI项目分两部分:框架内核、基于这个内核开发的系列功能子项目。
框架内核旨在提供一个 开放的、无侵入的 脚 本库管理解决方案,让类库编写者能够自己管理好自己编写的类库的相关依赖,提高类库的易用性,让最终用户从繁琐的依赖管理中解脱出来;隔离各个模块的执行 上下文,方便于重用、组织第三方脚本库,避免它们带来的命名污染问题。同时尽量做到简单,不加入特殊语法,被管理的脚本无框架依赖。
JSI的功能子项目,可以提供某方面的具体实用功能。如网页装饰框架,模板引擎。
作为一个开放的脚本管理框架,JSI不打算提供庞大的功能API,而是让最终用户根据自己喜好整合其他类库,我们也提供一些常用脚本类库的整合实例(如:jQuery、Prototype、Scriptaculous、FCKEditor 、YAHOO UI...)。
框架内核具体功能有:
l 依赖管理:
脚本依赖的暴露是复杂度陡增的最大祸根。JSI提供了完善的依赖管理方案,将依赖终结在类库开发者手中。提高类库的易用性。
对于一些简单的脚本,JSI很难体现它的优势,但是对于复杂的脚本库,特别是有多个且存在相互依赖脚本的复杂类库,那么最明显的一点,JSI可以简化网页上的脚本导入标记,不用成堆的script元素,只需导入直接使用的元素。JSI将自动导入间接依赖,且不污染全局空间。
l 真正意义的按需装载:
通常的安需装载是阻塞式的,没有实际意义,就一个宣传的幌子;JSI提供了无阻塞的按需装载方式(延迟同步装载,异步装载,JSI2.0+)
l 脚本执行上下文的隔离:
传统方式使用类库,将其直接导入进来,在全局上下文执行,这带来一个非常严重的冲突隐患,JSI不是这样,每个脚本都有单独的执行上下文,不必担心各个脚本的命名冲突。
更多更新信息请登录我们的网站:
学习内容
因为JSI没有复杂的API,可以说学习成本是较低的。主要的难点是接受JSI脚本管理的思想,JSI中重要的元素有:
l $import 函数:用于导入指定脚本元素
eg:
$import(‘example.test1’);//同步导入
$import(‘example.test1’,true);//延迟非阻塞同步导入
$import(‘example.test1’,function(test1){…});//异步导入
更对信息见:$ import函数参考
l $JSI 对象:有如下四个公开方法
/* 系统日志设置函数*/
$JSI.setDefaultLogLevel(level ) //设置默认日志等级.
$JSI.setLogLevel(scriptPath , level ) //设置指定路径模式的日志级别,一般在配置文件(config.js)中调用改方法做类库的全局日志设置.
/* 装饰引擎相关函数 */
$JSI.addDecoratorProvider(pkg, alias…) //添加装饰包.
$JSI.decorate ( ) //准备执行装饰器任务,一般在配置文件(config.js)中调用.
更对信息见:$JSI对象参考
l 编写自己的包,那么还有需要使用如下函数(都是#Package对象的成员函数):
/* 添加脚本 */
addScript( scriptPath , objectNames )//添加脚本及其声明的对象(函数、方法名).
/* 管理脚本依赖 */
add*Dependence(thisPath,targetPath,beforeLoad )//添加脚本依赖.
更对信息见: #Package对象参考
JSI使用指南
5.1 包定义文件(__package__.js) 处理机制
1 体系结构(Architecture)
1.1 基于JSI的JS程序功能模块分类
类库脚本:该部分可以是完全与核心框架无关的第三方脚本库,也可以是自己编写的类库,对该部分的脚本没有对框架的依赖要求。
包定义文件(__package__.js):用于描述脚本类库结构及其依赖关系的脚本。由类库开发者或集成者编写。
JSI引导脚本(核心框架):用于支撑装包系统工作的启动脚本。
装饰引擎:在JSI上编写的一个组件引擎,用于自动渲染网页上的装饰元素
装饰器:一些用于渲染网页装饰元素的JS类,集成自js.html.Decorator
网页脚本:具体页面的脚本,是最前端的部分,由类库最终使用者编写。
1.2 JSI脚本对象装载过程
我们以猫和老鼠的例子演示
有3个类:动物(Animal),猫(Cat),老鼠(Mouse);
他们有如下依赖关系:
猫、鼠 装载前依赖动物类(装载这两个类时,需要创建动物实例作为其原型;该操作必须在装载时完成,为装载前依赖)
猫鼠装载后,在使用过程中,相互依赖(猫鼠的行为中需要判别对方的行为,使用到了相互的引用,为相互装载后依赖)。
可表示为如下图例:红色代表装载前依赖,浅绿色表示装载后依赖:
Cat 类加载过程的伪码表示:
// Cat 类加载的伪码表示 //声明依赖的Animal, Mouse类:<code>eval('var Animal, Mouse')</code>; declareAllValiables('Animal','Bait'); //加载装载前依赖的动物类,赋值给上面声明的变量Animal(如果已经加载,直接给引用赋值) loadAndAssignObject('Animal',Animal);
//正式加载Cat类 loadClass(' Cat');
//加载装载后依赖的Mouse类,赋值给上面声明的变量Mouse //如果已经加载,直接给引用赋值,未装载需要先装载之,装载过程中还需装载它的依赖Mouse loadAndAssignObject('Mouse', Mouse); |
2 框架体验
i. 实例下载
本文中的实例程序,同框架一起打包,可从如下网址下载:http://sourceforge.net/project/showfiles.php?group_id=175776
ii. 目录说明
JSI对目录设置没有什么特别的要求,实例中的目录是:
脚本目录放在scripts内,根据包名映射到具体子目录,示例html文件放在example目录下。
iii. 浏览器支持
核心框架(不包括集成的第三方类库)支持到
l Internat Explorer 5.0、5.5、6.0、7.0
l Firefox 1.0 +
l Mozilla 1.0 +
l Netscape 6.2.2 +
l Opera 8.0 +
l Konqueror(KHTML)3.5.5 +。
其他浏览器未测试过,不确定。
这里我们编写一个简单Hello World例子,一个脚本文件,一个sayHello函数,显示“Hello World”,把这段简单的脚本集成到JSI框架中,展示一下本框架基本的功能,给您一个初步的印象。
实现脚本(hello- world .js):
/** * 显示信息Hello Word */ function sayHello(){ alert("Hello World "); } |
使用JSI集成:
定义包(__package__.js):
//添加脚本及其元素 this.addScript("hello-world.js","sayHello"); |
Html代码:
<html> <head> <title>Test Hello Word</title> <script src="../scripts/boot.js"></script> <script> $import("example.sayHello"); sayHello(); </script> </head> <body> </body> </html> |
与传统方法相比,使用JSI管理,代码不需要做什么改动,只需添加一个包定义文件,省去原来页面的script元素,换成JSI的引导脚本和导入指令。
这个例子简单的展示一下JSI的组织方式,由于脚本过于简单,不能体现JSI作为一个脚本管理框架的优势。
2.3 百花齐放 日月同辉――冲突的隔离
众所周知, Scriptaculous所依赖的Prototype库与jQuery存在冲突。所以同时使用比较困难。
JSI针对每一个装载的脚本都有完全独立的执行上下文。所以这个问题能在JSI上彻底解决。
下面的例子,我们将在同一个页面上同时使用Scriptaculous和 jQuery 类库。证实一下JSI隔离冲突功能。
示例页面(hello-jquery-aculo.html):
<html> <head> <title>Hello jQuery And Scriptaculous</title> <!-- 加入引导脚本 --> <script src="../scripts/boot.js"></script> <script> //导入jQuery $import("org.jquery.$"); //导入Scriptaculous $import("us.aculo.script.Effect");
$(document).ready(function(){ //使用jQuery添加一段问候语 $("<p id='helloBox' style='background:#0F0;text-align:center;font-size:40px;cursor:pointer;'>Hello jQuery And Scriptaculous</p>") .appendTo('body'); $('#helloBox').ready(function(){ //使用Scriptaculous高亮显示一下刚才添加的内容 new Effect.Highlight('helloBox'); }).click(function(){ //当用户单击该内容后使用jQuery实现渐出 $('#helloBox').fadeOut(); }); }); </script> </head> <body> <p>文档装载后,jQuery将在后面添加一段问候语;并使用Scriptaculous高亮显示(Highlight);在鼠标点击后在使用jQuery渐出(fadeOut)。</p> </body> </html>
|
2.4 海纳百川 有容乃大――封装、集成
Java的成功,离不开它那个庞大的类库,不单是sun的类库,很多细节的实现都取自第三方(如xml解析采用Apache的实现)。
如前言所述,我们暂时不大算编写丰富的公共API,但是我们可以集成其他成熟的类库,同时隔离他们的依赖,隔离各个脚本的执行上下文,消除命名冲突的危险。
这里我们详细介绍一个复杂一点的实例:类似Windows XP文件浏览器左侧的滑动折叠面板(任务菜单)效果。
我们先集成Scriptaculous Effect类库,并且在这个基础上按我个人的习惯对一个面板折叠效果做一个简单的封装,展示框架的类库封装功能。
1. 集成Scriptaculous类库:
这里我们不做过多介绍,详细情况请参考集成实战;我们发布的版本中已经把Scriptaculous放置于us.aculo.script包中,您可以把这些作为系统内置的类库使用。
2. 编写我们的折叠面板函数(example/effect.js):
/** * 滑动面板实现. * 当指定元素可见时,将其第一个子元素向上滑动至完全被遮掩(折叠)。 * 当指定元素不可见时,将其第一个子元素向下滑动至完全显示(展开)。 */ function slidePanel(panel){ panel = document.getElementById(panel); if(panel.style.display=='none'){ //调用Scriptaculous Effect的具体滑动展开实现 new Effect.SlideDown(panel); }else{ //调用Scriptaculous Effect的具体滑动闭合实现 new Effect.SlideUp(panel); } } |
3. 编写包定义脚本(example/__package__.js):
//添加slidePanel(滑动面板控制)函数 this.addScript("effect.js","slidePanel",null); //给effect.js脚本添加对us.aculo.script包中effects.js脚本的装载后依赖 this.addObjectDependence("slidePanel", "us.aculo.script.Effect",false); |
4. 编写我们的HTML代码:
<html> <head> <title>重用aculo Effect脚本实例</title> <link rel="stylesheet" type="text/css" href="/styles/default.css" /> <script src="/scripts/boot.js"></script> <script> $import("example.slidePanel"); </script> </head> <body> <div class="menu_header" onclick="slidePanel('block1')"> 面板 1 </div> <div class="menu_block" id="block1"> <ul> <li>text1</li> <li>text2</li> <li>text3</li> </ul> </div> </body> </html> |
onclick="slidePanel('block1')"这个事件函数将在我们点击面板标题时触发,能后会调用Scriptaculous Effect的具体实现去实现我们需要的滑动折叠功能。
2.5 壁立千仞 无欲则刚――控制依赖
Java可以随意的使用第三方类库,可是JavaScript却没那么幸运,随着类库的丰富,烦杂的依赖关系和可能的命名冲突将使得类库的发展越来越困难。程序的易用性也将大打折扣。
命名冲突的危险无形的增加你大脑的负担;随着使用的类库的增加,暴露的依赖也将随之增加,这是复杂度陡增的极大祸根,将使得系统越来越复杂,越来越难以控制。潜在的问题越来越多,防不胜防。
JSI的出现,可以解决上述问题,我们建议类库的开发者将自己类库的依赖终结在自己手中,避免依赖扩散,以提高类库的易用性。
同样使用上面的例子,假如我们想抛开JSI,实现同样的功能,那我们的页面代码将是(类库代码不用改动):
<html> <head> <title>重用aculo Effect脚本实例</title> <link rel="stylesheet" type="text/css" href="/styles/default.css" /> <!-- <script src="/scripts/boot.js"></script> <script> $import("example.slidePanel"); </script> --> <script src="/scripts/net/conio/prototype/v1_5/prototype.js"> </script> <script src="/scripts/us/aculo/script/v1_7/effects.js"> </script> <script src="/scripts/us/aculo/script/v1_7/builder.js"> </script> <script src="/scripts/example/effect.js"> </script> </head> <body> <div class="menu_header" onclick="slidePanel('menu_block1')"> 面板 1 </div> <div class="menu_block" id="menu_block1"> <ul> <li>text1</li> <li>text2</li> <li>text3</li> </ul> </div> </body> </html> |
这个例子的html代码明显比上面的复杂了,一堆堆的script标签,而且还是有序的;还出现在页面上,重构起来也极其麻烦。
可以看出,JSI的加入可以让类库更加易用,html代码更为简洁,最终用户已经不必关心所用类库的依赖了。
JSI中每一个脚本有一个单独的执行上下文。各个脚本顶部变量你可以随便使用,不必担心不同脚本中的命名冲突,不会污染全局变量空间,这种方式可以用于解决某些类库间变量冲突的问题(如jQuery和Prototype的$函数)。我们甚至可以做到同一个页面上间接加载同一种类库的两个不同版本,不相互影响。
使用JSI后,很多细节我们可以在包中封装掉,不需要告诉类库使用者太多。大大增加类库的易用性。同时,类库封装的支持可以让我们在第三方库的基础上轻松的按自己的喜好编写自己的类库,同时避免依赖扩散造成的复杂度增加。
使用JSI唯一多出的负担就是编写包定义文件,不过想想这种定义文件可是一劳永逸的(以后就不需要每次导入脚本的时候都小心翼翼的判断那个脚本先导入那个后导入,有那些间接使用到的类库需要导入,等等),而且有了包结构后对于代码组织、重用,以及文档的编写阅读,都将非常有利。
何为阻塞?浏览器使用XMLHttpRequest同步获取资源时将导致浏览器停止响应用户事件、停止页面重画操作。感觉就像死机似的,用户体验极差。
目前为止,所有宣称可以按需装载的框架都有这个毛病。所以,大家都不敢使用真正的安需装载,大都采用一起打包的方式,把所有可能使用到的脚本都打包在一个文件中。而所谓的按需装载只是一个口号罢了。
JSI2开始着手解决这个问题,有两种解决方案:异步导入、延迟同步导入。
i. 异步导入
这种方式用户体验最好,但是因为其异步特征,处理起来比较麻烦。
用法:将导入函数的第二个参数设置为某个函数(回调函数),那么这个函数将使用异步方式执行,并且当导入完成之后,回调该函数。参数为同步时的返回值(导入对象或对象表)
以第一个例子中hello world为例,若使用异步方式,html代码应改为:
<html> <head> <title>Test Hello Word</title> <script src="../scripts/boot.js"></script> <script> $import("org.xidea.example.sayHello", function(sayHello){ sayHello(); }); </script> </head> <body> </body> </html> |
ii. 延迟同步导入
JSI通过动态预装载功能实现的一种同步获取资源的方法,虽然也是同步,但没有阻塞,可以算时兼顾易用性和用户体验的机决方案。缺点是有一定延迟,当前脚本标签中不可用。
依然以第一个例子中hello world为例,若使用延迟同步方式,html代码应改为:
<html> <head> <title>Test Hello Word</title> <script src="../scripts/boot.js"></script> <script> $import("org.xidea.example.sayHello",true); </script>
<script>sayHello();</script> </head> <body> </body> </html> |
2.7 云想衣裳花想容--JSI的组件模型
用于装饰朴素html元素的框架,使用简单的xml标记,标识其装饰行为,比如将一个普通的input装饰成一个日期输入控件,将一个html ul标记装饰成菜单或树,将一个textarea装饰成一个代码语法高亮显示区域,或一个wysiwyg html编辑器。
JSI启动后将自动检查decorator标记,构建层次结构,自动做相关类的寻找、导入和装饰操作;实现零脚本代码的web富客户端编程。
示例:
1. 日期选择器 (DatePicker):
<d:datepicker> <input type="text" name="test2" /> </d:datepicker> |
2. 编辑器示例 (Editor):
<d:editor> <textarea name='editorText'>This is some <strong>sample text</strong>. You are using <a href="http://www.fckeditor.net/">FCKeditor</a>.</textarea> </d:editor> |
3. Spinner控件(Spinner 类似window时间日期管理中,年份调节的控件):
<d:spinner start='0' end='8' step='2'> <input type="text" name="test2" value='0' /> </d:spinner> |
4. 客户端包含(Include):
<d:include url='menu.xml' xslt="menu.xsl"></d:include> |
5. 代码语法高亮显示控件(Code):
<d:code language="js"> <textarea>alert(‘Hello World’)</textarea> </d:code> |
6. 标签页控件(TabBox参照xul tabbox标签):
<d:tabbox> <d:tabs> <d:tab>tab1</d:tab> <d:tab>tab2</d:tab> <d:tab>tab3</d:tab> </d:tabs> <d:tabpanels> <d:tabpanel>content1</d:tabpanel> <d:tabpanel>content2</d:tabpanel> <d:tabpanel>content3</d:tabpanel> </d:tabpanels> </d:tabbox> |
7. 综合示例:
来一个复杂一点的完整的例子,以日期选择控件的演示页面为例
页面上有: 标签页装饰器(TabBox….)、源代码高亮显示装饰器(Code)、日期选择装饰器(DatePicker)、包含装饰器(Include):
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:d="http://www.xidea.org/taglib/decorator" xml:lang="zh_CN" lang="zh_CN"> <head> <script src="../scripts/boot.js"></script> <title>DatePicker 示例</title> </head> <body> <h2>DatePicker 示例</h2> <!-- 开始标签页装饰器 --> <d:tabbox> <d:tabs> <d:tab>效果</d:tab> <d:tab>代码</d:tab> </d:tabs> <d:tabpanels> <d:tabpanel> <!-- 开始日期装饰器(内嵌式) --> <d:datepicker type='grid'> <input type="text" name="test1" /> </d:datepicker> <!-- 开始日期装饰器(弹出式) --> <d:datepicker> <input type="text" name="test2" /> </d:datepicker> </d:tabpanel> <d:tabpanel> <!-- 开始代码高亮显示 --> <d:code language="xml"> <textarea> <!-- 开始日期装饰器(内嵌式) --> <d:datepicker type='grid'> <input type="text" name="test1" /> </d:datepicker> <!-- 开始日期装饰器(弹出式) --> <d:datepicker> <input type="text" name="test2" /> </d:datepicker></textarea> </d:code> </d:tabpanel> </d:tabpanels> </d:tabbox> <select style="margin-left:120px"><option>弹出的datepicker 可覆盖IE select</option></select> <hr /> <!-- 开始Include装饰器,包含装饰器菜单 --> <d:include url='menu.xml' xslt="menu.xsl"></d:include> </body> </html> |
该装饰器演示见:
http://www.xidea.org/project/jsi/decorator/index.html
3 集成实战
3.1 集成jQuery 实例
jQuery是一个快速,简练的JavaScript工具箱它能够让你以简单的方式来操作HTML元素,处理事件,实现特效并为Web页面添加Ajax交互。jQuery设计用于改变你编写JavaScript的方式。本实例介绍一下这个类库如何与本框架集成。
i. 设置jQuery包:
我们将jQuery脚本放置在如下位置/scripts/org/jquery/v1_1_2/jquery.js,声明jQuery的包定义文件__package__.js:
this.addScript("jquery.js",['$','jQuery']);
|
这样jQuery所在的包为org.jquery.v1_1_2,但是让用户死死记住一个类库的版本号码,不是一个好的做法,为此,我们设置一个抽象包org.jquery 来指向jQuery1.1.2这个具体的实现:在/scripts/org/jquery/目录中添加包定义脚本__package__.js:
this.setImplementationPackage(".v1_1_2"); |
ii. 测试成果
至此jQuery两个脚本库已经成功集成至本系统中,你现在可以检测一下集成的结果。
example/jquery.html:
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh_CN"> <head> <!-- 加入引导脚本 --> <script src="../scripts/boot.js"></script> <title>Hello jQuery</title> </head> <body> <script> //导入jQuery $import("org.jquery.$"); //测试jQuery $(document).ready(function(){ alert("Hello jQuery"); }); </script>
</body> </html>
|
运行测试,如果您看到效果与原来无异。说明集成成功。
我们也可以运行他的测试用例,可以看到,结果与原来方式无异。
Scriptaculous是一个基于Prototype的一个脚本库。提供网页上的托拽,显示效果,UI组件等方面的支持。本实例介绍一下这两个类库如何与本框架集成。
i. 设置Scriptaculous包:
本实例中用到的的Scriptaculous版本是1.7,我们把他的全部脚本文件拷贝到如下位置 /scripts/us/aculo/script/v1_7,声明Scriptaculous的包定义文件__package__.js:
this.addScript("builder.js","Builder"); this.addScript("effects.js","Effect"); this.addScript("effects.js","Effect2","Effect"); this.addScript("dragdrop.js",["Droppables","Draggable","Draggables","SortableObserver","Sortable","Element"]); this.addScript("slider.js","Control.Slider"); this.addScript("controls.js",["Autocompleter","Autocompleter.Local","Ajax.Autocompleter","Ajax.InPlaceEditor","Ajax.InPlaceCollectionEditor","Form.Element.DelayedObserver"]); this.addScript("unittest.js","Test");
//Scriptaculous 依赖与prototype this.addScriptDependence("*","net/conio/prototype/prototype.js",true); this.addScriptDependence("effects.js","builder.js"); |
与上例相同的理由,我们声明一个抽象包 us.aculo.script在/scripts/us/aculo/script/目录中添加包定义脚本__package__.js:
this.setImplementationPackage(".v1_7"); |
ii. 测试成果
至此Scriptaculous已经成功集成至本系统中,你现在可以检测一下集成的结果。
简单起见,我们可以修改(将原来的<script>元素去掉,换成系统的启动脚本和导入语句)Scriptaculous自带的测试例子来测试我们使用JSI之后的效果,我们将rest目录拷贝至我们的网站某个目录中(如:/test/aculo)。
其自带的例子分两类,一种是功能演示性测试:test/run_functional_tests.html, test/ functional/*;另外一种是单元测试型测试:test/run_unit_tests.html, test/ unit/*;
原测试文件中原来的脚本一般为:
<script src="../../lib/prototype.js" type="text/JavaScript"></script> <script src="../../src/scriptaculous.js" type="text/JavaScript"></script> <script src="../../src/unittest.js" type="text/JavaScript"></script>
|
现在修改成:
<script src="/scripts/boot.js" type="text/JavaScript"></script> <script type="text/JavaScript"> $import("us.aculo.script.*");
//该导入指令只是某些直接用到prototype类库的网页需要 $import("net.conio.prototype.*"); </script> |
运行测试,如果您看到效果与原来无异。说明集成成功。
我们也可以运行他的测试用例,可以看到,结果与原来方式无异
4 类库使用者参考(页面脚本)
4.1 $import 函数用法
请查阅: JSDocAPI参考 $ import函数
说明:
简单的用法 即时同步装载:$import(‘com.mycompany.MyClass’),即时装载类,缺点:网速慢的时候浏览器会阻塞。
解决办法 1,延迟同步导入:$import(‘com.mycompany.MyClass’,true),设置第二个参数为真,表示采用延迟装载模式。缺点:当前script块后才开始装载,即 装载脚本当前script块不可用。
解决办法 2,异步装载:$import(‘com.mycompany.MyClass’,callbackFunction),设置第二个参数为函数(回调函数),表示采用异步装载模式。缺点:异步操作使用难度较大。
如果你想同时使用不同类库的同名函数(如jQuery和Prototype都有个$函数),则需要制定导入到制定目标,方法是:指定第三个参数target(任意object对象),以后可以通过targetx.MyClass的方式使用导入的元素。
5 类库开发者参考(托管脚本)
5.1 包定义文件(__package__.js) 处理机制
这段脚本将在包描述对象(#Package)的构造时调用,所以这段脚本中的this指向的是当前包描述对象。
例:
this.addScript("helloworld.js","sayHello"); //添加slidePanel(滑动面板控制)函数 this.addScript("effect.js","slidePanel"); //给effect.js脚本添加对us.aculo.script包中effects.js脚本的装载期依赖 this.addObjectDependence("slidePanel","us.aculo.script.Effect",false); |
5.2 包描述对象(#Package)常用方法
添加脚本及其声明的对象(常用)
public <void> addScript ( scriptPath , objectNames )
添加脚本及其声明的对象(函数、方法名)。 需要指定脚本位置(必须在当前包目录中),元素名(可用数组,同时指定多个)。 如果实际脚本中的变量名并不是你希望添加的名字,该成员函数只在包定义文件(__package__.js)中调用参数
参数:
scriptPath 指定脚本路径
objectName 类|函数名 或其数组,若为数组,realName参数将忽略
realName 可选(一般不指定,默认与对象名同名,这是常见情况)
添加依赖(比较常用)
void addScriptDependence(thisPath,targetPath,requiredBefore)
添加脚本文件对脚本文件的依赖。 需要指定当前脚本位置(必须在当前包目录中)、 被依赖的脚本位置(当前包中的脚本,或者通过抽象路径指定其他包中的脚本)、 是否需要执行前导入(装载期依赖)。 该成员函数只在包定义文件(__package__.js)中调用
参数:
thisPath - 本包中当前脚本文件(*.js),使用*可表示当前该包中已添加全部脚本文件(将逐一添加同样的依赖)。
targetPath - 依赖的脚本文件抽象路径(可不包括最后的版本包)
requiredBefore - 是否需要执行前导入(装载前依赖)
addSctiptObjectDependence(thisPath,targetPath,requiredBefore)
同上,但targetPath为对象路径
addObjectSctiptDependence(thisPath,targetPath,requiredBefore)
同上,但thisPath为对象路径,targetPath为脚本路径
addObjectDependence(thisPath,targetPath,requiredBefore)
同上,但thisPath,targetPath均为对象路径
设置真正实现功能的包名(不常用,详细请参看接口包与实现包)
void setImplementation (pkgPath)
设置具体实现包名。 比如,我们可以给prototype库一个统一的包, 但是我们的内容都放在具体的实现版本里, 我们可以通过该设置(setImplementation (this.name+".v1.5");)来指定默认的具体实现版本。 该成员函数只在包定义文件(__package__.js)中调用
参数:
pkgPath - {String} 指定实现包名,全路径(ID(.ID)*)或相对路径("." 开始的为本包下的相对路径)
5.3 接口包与实现包
接口包,是指没有放置具体脚本的包。该包中将声明一个指定的实现包。
接口包只是一个用户导入操作时的一个别名(与编程语言中的概念不尽相同),实现包中才是真正的内容。
常见情况是类库的版本管理,比如某个用户在用prototype库,具体版本是1.5,但是一般情况下不想让用户知道这些细节(也允许使用其他版本),于是,可以声明一个抽象包:net.conio.prototype,指向具体的实现包:net.conio.prototype.v1_5。一般情况下,用户只要导入net.conio.prototype中的类即可, 而且我们还可以把其他版本放上去,已备万一之用。
net/conio/prototype/__package__.js
this.setImplementationPackage(".v1_5"); |
net/conio/prototype/v1_5/__package__.js
this.addScript("prototype.js", ['Prototype','Class','Abstract','PeriodicalExecuter','$','$break','$continue','Enumerable','$A','Hash','$H','ObjectRange','$R','Ajax','Toggle','Insertion','Element','Field','Form','$F','Event','Position'], null); |
6 JSI组件模型JSDecorator
用于装饰简单html元素的框架,使用简单的xml标记,标识其装饰行为。
JSI将自动检查装饰标记,构建层次结构,自动做相关类的寻找、导入和装饰操作;实现零脚本代码的web富客户端编程。
6.1 装饰引擎简介:
系统默认的装饰引擎实现为:org.xidea.decorator.DecoratorEngine。
JSI装载后,将做如下操作:
1. 判断有无装饰器命名空间声明(xmlns:d= "http://www.xidea.org/taglib/decorator")
2. 若有,将在文档装载结束后,启动装饰引擎,初始化当前可用的装饰提供者表。(装饰提供者是一个JavaScript包,在注册这种装饰包时可同时指定他的别名,别名*表示默认包)
3. 遍历当前文档,凡是该命名空间的节点,都被看作需要装饰的元素。若当前文档存在装饰元素,启用遮罩(关机效果),页面将不可操作(仍可查阅)。
4. 查找装饰元素对应的装饰类(通过tagName判断类名),采用异步方式动态装载这些装饰器类(不会装载到全局空间),并更新当前进度信息,同时设置装饰器之间的关系(parent,children)。
5. 以深度遍历的方式遍历这些节点,注册组件(以后可以通过$JSI.getDecorator函数获取装饰器对象),依次执行他们的before操作,和decorate操作。
6. 完成装饰,取消遮罩,页面进入可用状态。
6.2 装饰器规范简介:
装饰器指的是所有继承自 js.html.Decorator的类。一般来说,可将一组装饰器归为同一个包中(太复杂的装饰器,应将具体逻辑放置在其他包中),能后在配置文件中定义装饰包。
scripts/config.js $JSI.addDecoratorProvider("org.xidea.decorator","xidea","*");
装饰器类包含两个方法before、docorate分别在遍历前(子节点未装饰)和遍历后(子节点装饰完成)调用。
同时,装饰引擎遍历时还将注入如下三个属性:
1. parent:父装饰器
2. children:子装饰器集合
3. attributes:装饰器属性集对象(只有一个成员函数:get(attrName) )
6.3 默认装饰器集合简介
目前JSI2最高版本2.0预览版 (2007-04-14)包含如下装饰器:
1. DatePicker
日期选择控件,参照xul datepicker标签,支持弹出方式(默认值 type='pop'),和内嵌式(type='grid')
2. Editor
编辑器控件,参照xul editor标签
3. Spinner
Spinner控件(window时间日期管理中,年份调节的控件),参照backbase 的 Spinner标签
4. TabBox、Tabs、Tab、TabPanels、TabPanel
TabBox(标签页)控件,参照xul tabbox标签
5. Code
代码语法高亮显示控件,参照SyntaxHighlighter的显示风格
6. Include
片断包含标签,支持xpath选取文档片断,支持xslt转换
这些装饰器的演示见:
http://www.xidea.org/project/jsi/decorator/index.html
目前JSI自带的装饰器不够丰富,而且都还是初级阶段,不够完善。现在发布的这些装饰器,主要是为了演示JSI的工作方式,编码风格,希望能吸引第三方团队、公司在这个基础上开发出自己的更加实用的装饰器集合。
JSI及其装饰引擎采用LGPL协议。可以商业应用,当能,更希望能开源。
6.4 JSI装饰器编写方面的资料:
基于FCKEditor 开发JSI Editor装饰器:
http://www.javaeye.com/article/79063
从零开始 Spinner(微调器)装饰器开发:
http://www.javaeye.com/article/79064
7 脚本API实时解析工具(JSDoc)
基于xml template(org.xidea.xml.Template) 开发的文档工具,实时解析,无需安装任何软件,纯html/js应用程序。可以简化文档写作及api查阅。
链接: JSI API 文档
8 其他支持
8.1 系统日志(#Log)
全局环境和每个脚本装载器中都有一个独立的#Log的实例,变量名为$log。
日志级别分为 :
"TRACE","DEBUG","INFO","WARN","ERROR","FATAL","NONE"
可以通过$JSI对象的静态函数setLogLevel(pattern,level)指定全局日志级别设置。Pattern为类似ant中的目录设置那样的匹配模式(*代表一级目录或目录下的文件,**d代表任意级目录或文件),用于匹配脚本。如果多个模式同时匹配,以最长的模式为准。
全局日志输出默认为INFO,您可以在全局环境中调用$log.setLevel()函数设置。
记录日志时我们调用$log对象的成员函数error、info、warn、debug输出日志,参数任意个数任意类型。如:
$log.error(msg1,msg2,msg3);
参考: JSI API 文档
8.2 统一XMLHTTPRequest 构造器
JSI中对不支持XMLHttpRequest的浏览器(IE)编写了一个模拟构造器,统一了XMLHtTTPRequest的创建方式。使得用户在常用浏览器上可以使用W3C推荐的标准方式创建XMLHttpRequest对象:
var request = new XMLHTTPRequest(); request.open(“post”,”test.jsp”); request.send(“name=jindw&email=jindw@xxx.com”); |
8.3 js.html.EventUtil
说明见:http://www.javaeye.com/article/87329
参考: JSI API 文档
8.4 js.html.BrowserInfo
说明见:http://www.javaeye.com/article/87329
参考: JSI API 文档
8.5 js.util.TaskQueue
异步装载就使用到了这个类库。
参考: JSI API 文档
8.6 js.util.Request封装
参考: JSI API 文档
Request对象,可以通过他更加简便的使用XMLHTTPRequest对象,该对象既可以同步的方式直接调用,直接通过其取值函数获取请求数据(getText、getXML、getResult、evalResult……);也可以使用异步的方式调用,此时根据其状态的改变触发该请求对象的事件处理函数,事件依次是: start -> receiving -> success/failure -> finish
例:
$import(“js.util.Request”);
//同步方式,我们直接获取请求数据 var jsonObj= new Request(“test-json.jsp”) |
$import(“js.io.Request”); //异步方式,通过我们的事件函数完成需要的工作 new Request(“test.jsp”) .setSuccessListener(function(){ alert('success,the content is' +this.getText()); }) .setFailureListener (function(){ alert('fail,the content is' +this.getText()); }) .send({param1:1}, true); |
参数说明
url:请求地址
options:请求选项
默认值为:
{ method: 'get', asynchronous: false, contentType: 'application/x-www-form-urlencoded', headers:{"Accept":"'text/JavaScript, text/html, application/xml, text/xml, */*'"} } |
Request再send调用的时候,发送请求,请求的第一个参数表示代发数据(可以是简单的queryString,也可以是一个对象,这时将根据对象的属性集合递归生成一个一维的参数表)。二个元素表示是否异步(默认同步)。
采用set*Listener/add* Listener系列函数代设置事件函数。
Request对象有一些列的set* Listener/add* Listener方法,用于注册事件。这系列方法都返回本身(this),可连续注册。
其实Request 对象的大部分常用方法都将返回Request对象本身,这点类似于java.lang.StringBuffer (append\insert等方法)。这样,我们可以吧一系列操作(构造Request对象,设置事件处理函数,发送请求等)放在同一行代码内完成,通常情况下,我们甚至不需要声明Request对象的临时变量即可完成我们需要的工作,使得代码更为简洁。
8.7 包对象装载测试
我们编写了一个简单的测试,用于测试指定包下面全部对象都能通过$import函数单独加载(需要包定义文件__package__.js编写正确),可以简单的测试脚本的装载前依赖的完整性,但是对于具体实现的问题和装载后依赖测试,需要您自己编写具体单元测试。
测试自己编写的类库时,可以直接在test/test-package-object-load.html中添加你自己的包,运行测试即可。
<script> var console = document.getElementById("log"); $JSI.setConsole(console); $import("js.test.loader.*"); LoaderTest.test(["net.conio.prototype", "us.aculo.script", "org.xidea.example.fish", "org.xidea.example.display", "org.xidea.example"]); </script> |
9 附录
9.1 其他相似框架对比
JSVM:
JSVM是一个功能非常强大的脚本框架,但是实现也相应复杂,它甚至提供多种自己的中间编程语言,相比之下JSI是一个简单的框架,没有特别的语法,不需要程序员学习过多的东西。
JSVM有一个和JSI的脚本加载方式有相似之处。不过它好像只支持装载前依赖。他的装载过程是:
1. 加载目标源代码文本(JSC文件)。
2. 编译源代码为标准的JavaScript。
§ 将package指令编译为_$package函数。该函数会根据包路径声明对象属性,根对象为window。
§ 将import指令编译为_$import函数。该函数运行时会加载相关的类库(相当于JSI中的装载前依赖)。
§ 此外,其他脚本也会做些更改,例如,它会把当前对象放置到包对象属性上;声明导入对象的别名(去除包名后的简写名)。
3. 运行编译后的代码(加载)。
JSVM功能强大,甚至有自己的语法和编译过程。而JSI非常简洁,目标在于管理普通脚本。JSVM自成体系,而JSI海纳百川。
dojo:
dojo有一个完整的模块导入解决方案。也可以支持JSI中类似的运行时依赖和装载前依赖。而且功能丰富,有较多基础类库可用,但是普遍反应:过度的复杂,失望的效率。
相比之下,JSI是一个开放的管理框架(JSI可以管理已有第三方脚本,脚本对管理框架是无依赖的),dojo不是如此,而且侵入比较严重。
细看dojo的包加载方案实现,先创建包对象(一个简单的Object实例,dojo为默认);获取脚本,使用js的eval函数,运行之;相当于修改了这个包对象,将新的属性、函数、添加上去。
所以,从代码上就可以看到,dojo的包在代码中有直接的反映(这样不利于重构),以后的引用也是直接使用包全名,所以,dojo的导入称为require 而不是JSI的$import。
总结:dojo是一个功能比较完善的复杂的类库,而JSI是一个开放的脚本管理框架,本身不提供丰富的基础类库,但是它可以重用第三方实现来弥补这一不足。
9.2 框架内核代码相关约定
基本原则:
让简单的事情更简单,让复杂的事情可实现。
对于私有方法,尽量使用强类型,不做容错处理
对于公开方法,尽量做容错处理,尽量兼容各种可能的用户使用习惯。
目录路径以/结尾
自动系统ID模式:__\$[\w]+\$\d+
如装饰引擎的装饰元素id:
__$decorator$autoId(autoId为一个递增数字)
9.3 常见问题(FAQ)
名词&&概念
i.自由脚本
通过<script>标记引入或直接编写的脚本,我们不建议在使用JSI之后,仍旧使用script src导入JSI启动脚本(boot.js)之外的脚本。
ii.托管脚本
通过$import函数直接或间接加载的脚本。这些脚本将在一个独立的执行上下文装载,不会污染全局环境。
iii.脚本依赖
若要使用A,需要装载B,则A依赖B。
A、B可以是脚本文件,也可以是脚本文件中的某个脚本元素。
iv.直接依赖
A中直接使用到B,则A直接依赖B
v.间接依赖
A直接依赖B,B依赖C,则A间接依赖C
vi.装载前依赖
真正的装载期间依赖,脚本装载时即需要所依赖的东西,所以需要在当前脚本加载之前加载或声明依赖元素
vii.装载后依赖
并不是真正的转载期间依赖,是执行期间的依赖,只有真正使用到被依赖对象的直接引用(被注入的对象除外),才需要声明加载后依赖元素。
例如:function F1(f2){};//f2为函数F2的引用,但是这种使用方式并不需要申明依赖
但是:
function F1(f2){
if(f2 !=F2){//这里直接使用到了依赖对象的引用,所以需要申明依赖
alert(‘非法参数’);
}
}
viii.类库使用者
类库的使用者,您只需再页面上书写脚本,导入类库,编写自己的简单脚本,一般情况请不要编写js文件,那是类库开发者做的事.
ix.类库开发者
在此框架下开发出方便实用的脚本库,您需要编写一些通用脚本,同时设置脚本依赖,做到任何类/函数,导入即可运行。
x.框架开发者
负责系统框架的设计,维护。
JSI的忌讳
虽然在大方向上,JSI是无侵入的,但是,因为JSI托管脚本的执行环境与正常脚本的执行环境的细微区别,有些用法在JSI中可能出现问题,我们能作的只能时尽量回避。
i.全局变量的分支内申明覆盖:
这个问题存在于Prototype1.5中,导致了装载失败。
错误分析:
JS本身没有块变量,变量是函数范围的。
语句: if(window.Event ==null){var Event = new Object()}
如果在全局环境下,这等价于:
var Event = <有效值>//这句可能有,也可能没有,浏览器不同。
var Event; //没有赋值,但是其值可能浏览器已经初始化了(上句描述)。
if(window.Event ==null){
Event = new Object();
}
而装载单元环境下,这等价于:
var Event; //没有赋值,初始为undefined
if(window.Event ==null){ Event = new Object()}
Prototype中这一语句,目的在于在未有Event对象的浏览器上,创建一个空对象,作为一系列方法的载体,但是,当这类脚本在JSI装载单元内装载时,真正有这个对象的浏览器,不能使用这个对象(变量覆盖为undefined)。
Prototype1.5通过这种方式条件覆盖了Event,Element两个变量。
对上面这种逻辑,JSI推荐写法: var Event = window.Event || {};
同时Prototype还有诺干变量未申明,为此我们也可以在不修改其源代码的情况下,编写一个装载前依赖补丁来解决这种问题(当能,为了更好的效率,还是修改源代码吧):
//prototype1.5 补丁1 var Event = window.Event; //2177行 //if (!window.Event) { // var Event = new Object(); //} var Element = window.Event; //1106 行: //if (!window.Element) // var Element = new Object();
// Prototype中有下列变量未申明,我们不想其污染全局环境,所以加上声明。 var ObjectRange,key,depth; |
ii.通过Function构造器创建函数:
这个问题存在于Prototype1.5中,导致运行时错误。
错误分析:
通过Function构造的函数,与通过表达式创建的函数有点区别。它存在一个上下文链的短路问题。通过Function构造的函数,是不知道构造时的上下文的。它只知道全局上下文。而JSI的脚本装载后,一般时不会保露到全局环境的,所以,这里容易出现问题。
Prototype1.5开始,为CSSSelector更快的运行速度,采取了一种函数编译的操作,为特定选择器动态生成更简洁的匹配函数:
compileMatcher : function() { this.match = new Function('element', 'if (!element.tagName) return false; \ element = $(element); \ return ' + this.buildMatchExpression()); }; |
对于这类问题,我们可以使用eval来达到相通的功能:
this.match = eval('function(element){if (!element.tagName) return false;element = $(element);return '+this.buildMatchExpression()+';}'); |
为此,我们可以通过修改源代码,或者打装载后补丁的方式来解决。
iii.装载过程中使用document.write函数打印html:
这个问题存在于jQuery1.1.2中,可导致异步装载失败,其他装载模式不受影响。
错误分析:
当异步装载时,脚本可能在一个定时器内触发,装载,这时是不能用document.write函数的。出现这种问题,我们只能通过修改源代码来修复。或者对该类库不使用异步装载(同步装载,延迟同步装载还是可以用的)。
JSI 的价值
JavaScript的历史可算悠久了,使用它编写的类库也不少,但是因为JavaScript自身的特点,难以组织管理,难以重用,这些零星的类库缺乏有效的管理而杂乱的堆砌在一起将会给以后的维护造成很大的麻烦。
JSI的出现,将有望结束这个噩梦,它将普通的JavaScript加上包的概念,通过包去组织脚本,而且,还提供了一个统一的依赖管理,将脚本间复杂的依赖关系有效的组织起来、封装起来。只要求类库开发者管理好自己类库的直接依赖即可(不需要管理间接依赖了),而类库的使用者更是只需导入使用即可,完全不必理会烦杂的依赖。
类似的装包工具也出现过,如dojo的装包系统,但是那是一个封闭的系统,不能重用已有的类库,而且有代码依赖。与之相比,JSI可以轻松加入已有的成熟的脚本库,而且代码完全可以脱离框架的依赖,包结构信息也只存在于独立的定义文件中。编写类库时,你甚至感觉不到包的存在。这还将有利于包的重构。
此外JSI2加入的非阻塞导入的支持,将使得按需装载成为一种真正实用的技术。
JSI 的性能
JSI2Alpha发布时,对装载性能和运行效率都做过比较全面的测试,下载包的doc目录下有详细测试数据。
总之,JSI在装载类库时,与传统模式相比,是增加了些额外的运算,但这点开销连脚本解析的开销的级别都不到,根本无需考虑。脚本运行时,效率不会受印象,甚至可能更高(变量空间更小,局部变量访问速度更快,FF上效果明显)。
对于同步装载,从装载时间的测试数据上,看不出什么差距,相反,由于同步装载阻塞用户事件线程,导致装载时间可能比传统方式更短,这点在IE上反应明显。
对于异步装载和延迟装载,所需要的时间大约时同步方式的两倍。异步的时间最长,可能与任务之间的时间间隔有关。由于测试数据时本地的,不考虑任何网络传输速度的问题,所以,如果放在真实的网络环境下,这点开销将再次被冲淡。
如果采用同步加载脚本的方式(JSVM,dojo等框架只支持这种模式),如果网速度很慢,浏览器会阻塞,会让人感觉很慢,此类其他框架也存在着这个问题。
造成这种情况是浏览器本身设计的问题,因为浏览器脚本设计为单线程运行模式,就是说脚本执行时时不响应用户事件的,浏览器上虽然有setTimeout、setInterval 之类看是多线程支持的函数,但是真正执行过程中,是不可能两段连续代码交叉执行的。必须等待,而当代码执行中间有需要等待的操作,如同步下载,那么就需要长时间占用脚本时间。
因为这个问题,JSI2中,我们增加了异步导入和延迟导入的功能。为了避免这种浏览器假死的现象,我们建议用户对于未缓存的脚本,尽量采用延迟装载模式或者异步模式装载。
延迟装载模式最晚将在下一个script标签开始之前完成装载行为,用法与同步装载模式基本一样,而异步装载可能就要影响用户的编码习惯了。确实,异步编码比较繁琐。可以在框架级别的类库中使用,如装饰引擎的实现。
JSI 的学习成本
保持JavaScript的简单性这点我们非常重视,我们在设计框架的过程中也非常注重这点,虽然JSI框架本身逻辑略显繁琐,但是真正需要开发者掌握的API非常少,也没有新加什么特别的语法(导入指令其实就是一个普通的全局函数)。
要说难点,那就是接受JSI的脚本管理思想,而目前来说介绍JSI的资料太少。
编写基于JSI的应用程序必需掌握的API只有:
类库使用:
$import函数:用于导入指定元素(类、函数、变量)
类库编写:
addScript、addScriptDependence函数:都是Package的成员函数,用于定义包信息(包中每个脚本包含的圆熟)、脚本依赖。