必须承认,要战胜人的惰性实在是太难了,我看起来不算很宅,但是这一整个夏天(在跟随电台去达里诺尔转过一圈之后)都没有出门,情愿在床上翻来覆去睡的腰酸背痛,或者在家里走来走去直到百无聊赖,都兴不起出去逛逛的兴头,我一边自责,一边幻想自己在下一刻就能忽然像吃了菠菜一样爆发起来,立刻去做一些有追求的事情。
本来说不定十年八年就这样幻想过去了,所幸在这个时候,忽然安子从天而降,把我和其他几个宅男宅女们从深宅之中解救出来,因此就有了这一次的甘南-青海湖之行。
安子(MM),导游+策划+组织者,资深行者,在甘肃兰州,因此也是地接导游;
此次活动起因是我们大学同学相识10周年的一次纪念,本计划是去年去敦煌、月牙泉一带的,但是去年因各种原因没有去成,因此延迟到今年,而且重新计划了行程,今年最后参加的也就我们几个人(这可以说明班上更多的人是已经宅的无可救药,已经远离安子解救的能力范围了),而且因为我没有能成功的把我MM忽悠去,因此,这次就是一个纯同学小组。
就这样,在8月12日(星期五),我照例背上我的包,和同事们告别,开始一个为期9天的旅行。
按照能者多劳的分配原则,我在旅游团之中,担任最重要角色:游客。
今天我要分享的是模块管理,这是Jla框架的一个非常重要的内容,本篇所介绍的模式也将比前面介绍的Jla框架核心存在更多争议,坦白的说,我的这个解决方案面向了复杂的设计模式问题,却并没有提出自己的解决方案,最多只是将问题变得更清晰一些。
大家都知道,要开发一个较为复杂的程序,光靠OOP的思路肯定是解决不了问题,因为OOP解决的是类之间组织的问题, 对于各个功能之间的耦合的解决办法,并没有提出什么实质的解决方案,针对这一点网上有一个流传广泛的《可扩展的JavaScript架构》的解决方案,还有这个PPT,这个解决方案的主要思路如图:
.jpg)
总体上来讲,就是沙箱的机制,将各个功能逻辑拆分为一个个的模块,各个模块之间相互独立并且互不耦合,让每一个模块只知道沙箱,而不知道有其他模块,这样打断各个模块之间的关系,就能够保证各个模块的独立性,并且在拿掉一某些模块之后不会影响整个架构的执行。
这个架构确实能解决很多问题,我在以前开发之中采用的"主体+控件"的模式实际上也是采用这样的机制来解决耦合的问题,只是没有沙箱罢了 。
但是,我在新的Jla框架设计的时候,发现这个设计模式还是存在一些问题,总体来讲,如下:
1.任何一个模块,在开发的时候就确定好是一个模块;假设你想简单的使用某一个模块的功能而不想构建一个庞大的沙箱来适应,会比较复杂,同样你如果将一个类改造成一个模块,可能也需要一番功夫,因为每一个模块虽然没有和其他的模块耦合,但是和沙箱本身的耦合太过紧密。
2.开发的时候,因为模块会将自己的方法和事件赋给沙箱, 假如两个模块采用了同样的方法和事件名称,将引起混乱,这样每个模块在开发的时候必须严格注意不要和别的类重名,影响了模块开发的独立性。
3.有时候,页面上的功能并不是简单的扁平的结构,可能更多像是一个树形的结构,例如,整个页面分为多个模块,一个模块又由多个更细的模块组成,目前的沙盒模式,可能在按照这种结构开发的时候比较麻烦。
有没有办法可以解决这些问题?
当我开始仔细分析主体和模块之间的关系时,发现其实就是各个类实例之间的关联,这种关联,其实和类本身之间的关联是比较像的,考虑到这一点,我们可以考虑把上面的沙盒改造成一个简单的实例管理器,而不用实现任何其他功能,各个模块之间的耦合采用类似Jla.require的模式实现即可。
我们先看看两个简单的模块代码,下面是第一个简单模块:
1
2 Jla.require(["Js.Event"],2,function(Event){
3 /**
4 Js架构之中用来记录提示消息的模块
5 @class
6 */
7 function App()
8 {
9 }
10 App.prototype.log=function(message,level)
11 {
12 Event.trigger(this,"message",[message,level]);
13 }
14 App.modFactory=function(holder,name)
15 {
16 holder.require([],2,function()
17 {
18 holder.set(name,new App());
19 });
20 }
21 Jla.set("Test.logging",App)
22 });
从上面这个简单的模块代码可以看出如下特点:
1.这段代码就是一个简单的类定义 ,这个类的命名空间是"Test.logging",唯一的不同就是增加了modFactory静态方法,当调用这个方法的时候,要求为指定参数的holder创建一个指定名称的实例,当创建完成之后调用holder.set函数注册即可
2.如果不需要使用模块机制,通过直接去new这个类照样可以使用这个功能逻辑。
3.模块不需要知道主体是谁,也不关心自己在模块体系之中自己的名称,这样在一个页面上多次使用此模块完全没有问题
我们再看另一个模块的代码:
Jla.require(["Js.Event"],2,function(Event){
function App(logging,div)
{
this.div=div||document.getElementById("msgDiv");
Event.bind(logging,"message",this,this.showMessage);
}
App.prototype.showMessage=function(message)
{
this.div.innerHTML=message||"";
}
App.modFactory=function(holder,name)
{
holder.require(["test.logging"],2,function(logging)
{
holder.set(name,new App(logging));
});
}
Jla.set("test.pageLogging",App)
});
这个代码使用到了两个模块之间的耦合,从这个实例,我们进一步可以了解如下特点:
1. 两个模块之间的耦合没有通过holder中转,而是直接向holder请求到了对方模块的实例,之后的操作直接进行,不再通过holder,也没有给Holder添加任何方法或事件,因此不需要担心方法和事件名称冲突的问题
2. 假如通过new test.pageLogging(new Test.logging())这种方式来直接使用这两个类,照样可以实现功能,因此,模块体系对这两个类来说是可选的,不管有没有模块体系,都可以运行并完成耦合
下面再看看一个Holder的简单实例代码:
Js.Holder
只要继承以上的Js.Holder,就都可以加载自己的子模块,综合以上的三个代码范例可以知道:
1.模块的开发不再有严格的格式要求,只需要实现modFactory静态方法,任何类都可以成为一个模块;
2.大部分模块都可以脱离模块体系完成自己的功能,当然,假如模块在运行之中需要调用holder.require方法,就必须要求在模块体系下工作
3.某个模块本身也可以通过继承Js.Holder来将自身的功能进行进一步的拆分
4.与沙箱体系不允许模块之间直接交互不同的是,本模式要求每个模块明确的了解自己需要耦合的模块,对目标模块的事件和方法必须有明确的了解。
这个模式对比上面的沙箱模式,似乎各有优缺点,但是究竟哪个好,一些,哪个更适用与何种情况,我还没有想太清楚,但是对于Jla框架来讲,非常关注每个代码单元的重用性和独立性,因此,本文提到的模式似乎会更好一些,希望大家能留下自己的见解,一起讨论一下。
今天轮到分享这个框架的配置管理机制,一般来讲,每个代码单元都可能允许通过配置参数来实现定制化的界面和功能,如果没有配置,一般是很难将一个代码单元直接移植到另一个应用里面去使用,这一章,我简单的分享一下我为Jla框架设计的配置机制。
配置机制需要实现在任意地方,对任意的代码单元进行统一的配置,而代码单元在读取这个配置的时候,按照统一的标准去读取,而不应该该配置是由哪个代码单元设置的,这就是配置机制的设计思路,进一步将代码单元和应用本身分离开来。
我设计的配置机制非常简单,对代码单元来讲,只需要这样使用即可读取配置:
1 Jla.require("Js.Config",2,function(Config)
2 {
3 function App()
4 {
5 var num1=Config.get(App,"configItem1",5);
6 var str1=Config.get(App,"configItem2");
7 alert(str1);
8 }
9 Jla.set("Test.ClassA");
10 })
这个程序简单的读取了自己的两项配置,Js.Config的get方法有三个参数:
a.参数1是类的本身或者命名空间字符串,在读取的时候,建议使用类本身,这样就可以和类的命名空间无关了
b.参数2是类的配置项名称,因为一个类可能有多项配置,这儿可以指定读取哪一项配置
c.参数3是在指定的配置没有被设置时的默认值,如果没有配置该项,则会直接返回该值
在任何时候需要写入配置的时候,也非常简单,按照上面的方式调用Js.set方法即可,不过因为在设置配置的时候,该类可能还没有加载,因此在写入配置的时候,可能更多是采用字符串作为第一个参数:
1 Jla.require("Js.Config",2,function(Config)
2 {
3 Config.set("Test.ClassA",configItem1,10);
4 Config.set("Test.ClassA",configItem2,"test string");
5 })
这就是配置的设置过程,虽然非常简单,但是我希望通过这样简单的指定,将配置处理的繁杂工作从各个代码单元之中解放出来,而由Config类来完成,设想只需要改写get方法,就可以实现从服务器或者Cookie等位置读取配置的功能,这些功能的实现将不再和买个代码单元相关,进一步保证了代码单元的独立性和重用性。
前面的一篇文章,我介绍了Jla框架的代码单元的规范,为什么需要有这样的规范?最主要的目的还是将能够将代码和功能进行有条理的拆分,让每个代码单元仅仅关注自身的逻辑,这样就可以提高代码的重用性。
可是要真的实现让每个代码单元能够心无旁骛的开始自身逻辑的实现,仅仅有一个框架规范还远远不够,我们必须对一些大部分代码单元都会关心的问题提供一个合理的解决方案,保证代码单元不再为这些事情而操心,其中最重要的两个解决方案是资源管理和配置管理,关于配置的管理,我会在下一篇文章之中讲到,本文将首先讲到资源管理。
在本框架之中,每个程序是以JavaScript为主体的,但是单靠JavaScript,肯定不能实现那些丰富多彩的应用,对图片、CSS或者其他资源的调用是很平常的内容,也就是说,一个程序单元(或者说一个类)除了有一个Js文件以外,还有可能有一系列的图片、CSS片段文件甚至音乐视频等多方面的内容,这些(统称为资源文件)和脚本文件合在一起,才组合成为一个程序单元,如果要考虑代码的重用和独立性的问题,这些资源文件也应该考虑进去。
对于资源的调用,我设计了这样的一个解决方案:
1.开发的时候,假设资源文件和类文件一样,都是分目录存放的, 例如对于命名空间Com.Test.ClassA,类文件的路径为:
workspace/src/Com/Test/ClassA.js
假设类ClassA调用了一个图片test.png,则应该放在:
workspace/resource/Com/Test/ClassA/test.png
这样,每个类都有自己的资源目录,将自己的多个资源放在目录下即可,也不用担心和其他的文件名冲突的问题
2.假如类ClassA之中想要调用自己的资源,当然不能直接写路径(如果直接写路径,基本上不能保证代码部署后或者移动之后还能执行),建议ClassA在定义的时候require Js.Resource类,然后调用该类的静态方法getUrl,例如:
1 Jla.require(["Js.Resource","namespace.ClassB"],2,function(Resource,ClassB)
2 {
3 //code of ClassD start
4 function App()
5 {
6 var img=document.createElement("img");
7 img.src=Resource.getUrl(App,"test.png");
8 }
9 App.prototype.onClick=function()
10 {
11 Jla.require(["namespace.ClassC"],1,function(ClassC)
12 {
13 //调用ClassC.完成点击之后执行的操作
14 })
15 }
16 //code of ClassD End
17 Jla.set("namespace.ClassD",App);
18 })
上面的代码之中,创建了一个图片对象,并将URL设置为资源文件夹之中的test.png,getUrl函数的第一个参数是类或者类的命名空间,一般来讲,我们并不希望一个类太关心自己的命名空间是什么,所以,建议还是使用自己的类,第二个参数就是资源的名称,一般也就是对应的文件名,根据这两个参数,Resource类会计算出这个资源文件的URL地址并返回,这样代码单元不再需要关心自己的资源文件具体部署在什么位置。
在代码最终部署的时候,资源的访问路径和组织形式可能和开发的模式有所不同,这种情况下,只需要在发布的时候通过更改Resource类的配置或者重写getUrl的方法,就可以让每个代码单元正常访问到自己的资源,各个代码单元不需要做任何更改。另外,可以根据代码单元的require关系,完全不需要的代码单元,其对应资源也不用发布到部署环境之中。
以上就是针对资源的解决方案,该解决方案将代码单元和资源之间的关系简单化,带来很多方便。
下面说说带来的一个问题,就是Sprite,因为按照现有的框架,我希望能将不同的代码单元的资源分目录存放,可是现在很多网站上,将多个图片(很有可能是毫无关联)的图片组合到一张图片上,这样,似乎就很难对代码进行有效的拆分,针对这个问题,我提出如下的解决方案:
首先,我认为,Sprite不应该是开发时就需要考虑的内容,而是部署时需要考虑的内容,因此,我不建议各个代码单元在开发的时候就使用合并起来的图片,这样会大幅度的减少代码重用性,最好在开发的时候,图片还是一个个的分离的图片,每个代码单元调用自己的资源图片,哪怕代码单元内部,也不建议使用Sprite技术,因为我说过:Sprite不应该是开发时就需要考虑的内容,而是部署时需要考虑的内容。
现在假设各个程序单元开发的时候都没有考虑过Sprite的问题,怎么实现Sprite呢?我想通过这样一个类似于重写的技术:
1.假设每个代码单元,在显示图片的时候,都使用一个类Js.Html.Image,这个类提供了create(用来根据URL创建一个图片实例),setSrc(用来更改一个图片实例的src),setBackground(用来设置背景图)三个方法,这三个方法包含了所有图片的基本使用
2.Js.Html.Image还提供了一个addSprite方法,用于通知Image类,哪些图片被整合到了另一张图片的什么地方,例如:
1 Image.addSprite("img/image.png",[
2 {rect:[0,256,300,44],path:Resource.getUrl("Test.Class1","test.png")},
3 {rect:[0,212,300,44],path:Resource.getUrl("Test.Class2","test.png")},
4 ],{size:{x:300,y:300}})
这样,Image类记录下这个信息,在创建Test.Class1的test.png图片的时候,最终实际上会去img/image.png的某一块上去获取图片显示,这样就实现了图片的Sprite方案。
这样做,我觉得比一般的Sprite方案有更多优点:
1.和开发过程基本无关,使开发过程不受影响,要知道网上讨论Sprite技术的时候,一致表明缺点是使开发和维护复杂化
2.假如Sprite图片做了调整,只需要改上面的配置即可,不需要到处调整。
3.将完全无关的功能组合在一起也完全不是什么问题
这样就实现了这种模式下的Sprite技术,本来应该将这个关键的Image对象源码展示一下的,但是这个还没有完善,而且,本系列文章主要是介绍新框架的思路,源码并不是特别重要,而且,一般写一个出来也不会太复杂吧。
这一篇将介绍Jla框架的核心,在此之前,先要介绍"Jla"这个名称,全称是"JavaScript Lazy App",这个框架的核心是将一系列代码单元组合起来,在页面上按需加载,也就是通俗提到的"懒载入",这也就是这个名字的由来。
当我们来仔细审视JavaScript相对于其他语言的特点时,我们会发现,安全性和对懒载入的支持是我们不能忽视的特性,安全性是指用户通常会信任JavaScript脚本的运行,因此运行的平台非常广泛,而懒载入则使得我们在开发之中,可以灵活的控制程序单元从服务端流向客户端的过程,这样,就为使用JavaScript进行大型项目的开发提供了可能。
在现在,越来越多的桌面应用程序正在改为BS模式,原来的程序可能是几十兆甚至上百兆,这种情况下,如果通过BS模式一次加载所有的功能到页面上,会给客户端带来非常不好的用户体验,对服务器的压力也挺大,而且其中的大部分功能未必用户真正能用得上,因此我们最好是将程序进行分拆,任何功能都是在需要的时候再加载,例如点击按钮之后执行的操作等。
Jla框架正是为解决这个问题而设计的框架,我们先看看该框架是怎么定义的:
在Jla框架之中,一个程序由众多的代码单元组成,每个代码单元应该是这样的模式:
1 Jla.require(["namespace.ClassA","namespace.ClassB"],2,function(ClassA,ClassB)
2 {
3 //code of ClassD start
4 function App()
5 {
6 }
7 App.prototype.onClick=function()
8 {
9 Jla.require(["namespace.ClassC"],1,function(ClassC)
10 {
11 //调用ClassC.完成点击之后执行的操作
12 })
13 }
14 //code of ClassD End
15 Jla.set("namespace.ClassD",App);
16 })
从上面的代码单元来看,这个代码单元是定义了一个类,这个类的命名空间是namespace.ClassD,通过Jla.set来向框架之中注册这个命名空间,这个类使用到了两个Jla.require方法,代表这个类在运行时会依赖其他的类。
当一个类需要调用其他类的时候,要求使用Jla.require模式,该模式有三个参数:
参数1是一个命名空间数组,代表需要调用哪几个类
参数2是依赖的类型,有三种类型:
值1代表应该立即去获取该类,并在获取完成之后执行回调函数,主要用在需要懒载入的情况,例如点击按钮之后执行的函数等
值2代表的含义和1相同,但是进一步建议在代码发布时应该将这几个类也打包在一起(意味着依赖程度太高,不建议使用动态加载),主要用在一个类必须引用另一个类才能运行的情况下
值0代表不需要主动去加载该类,但假如该类已经加载或者后面被其他的类引用加载,则执行回调函数,用的比较少,主要用在一些事件绑定过程上。
参数3是回调函数,在需要的类都加载完成后,会自动执行回调函数,而且会将需要的类列表作为回调函数的参数传递,这样就可以直接使用那些类,而不需要再次执行
虽然以上的代码非常简单,实际上也就使用了Jla.require和Jla.set两个函数,但这段代码已经完整表达了Jla框架的核心规范,我简单的描述一下这个核心所定义的规范:
1.不要直接调用一个类,对外部类的引用都应该通过Jla.require来引用,虽然你可能直接通过命名空间或者Jla.get(namespace)调用到那个类,因为这样就破坏了规范,而且如果你调用的时候该类还没有加载完成,就会出现异常;
2.每个代码单元都应该明确在实现自己的逻辑的过程中需要依赖哪几个类,依赖程度如何,只有对这些能明确下来,才能保证最后应用程序只会载入需要的逻辑单元;
3.每个类都只应该关心自己实现的逻辑,而不需要关注最终的程序是什么样子的,这样才能保证每一个代码单元的重用性。
这个框架规范比较简单,仅仅拿着这个框架去开发会遇到一些麻烦,但是实际上这已经是一个完整的开发框架,后面我继续描述的内容比较多,但都是依赖这个框架的应用,并不是对这个框架的补充。