一个简单的、面向对象的javascript基础框架
如果以后公司再能让我独立做一套新的完整系统,那么我肯定会为这个系统再写一个前端框架,那么我到底该如何写这个框架呢?
在我以前的博客里我给大家展示了一个我自己写的框架,由于当时时间很紧张,做之前几乎没有完整的思考过我到底该如何去写这个框架,所以事后对于这个框架我有很多遗憾之处,当我重构过一次代码后我就没再做过任何重构操作的工作,因为我根本不想再去给它修修补补了,之所以有这个想法,就是我对我写的那个框架的基础架构不满意。
为什么不满意这个基础架构了?我们先来看看我当时封装框架的方式:
(function(window,document){ var o1 ={},o2={}; var outerObj = { o1:o1, o2:o2 } window.outerObj = outerObj; })(window,document,undefined)
这段代码是我参考jQuery的编写方式,现在回头看看,这个编写方式无非就是用匿名函数把内部代码保护起来,除此之外真的没有用到什么javascript语言其他特性。不过当时这么写还是有我自己的考虑的,主要是从下面这三个角度思考:
- 要提高网站性能就得尽量减少页面加载时候的请求个数,因此外部的javascript文件越少越好;
- 单个请求的大小也要尽量最少,因此页面里javascript代码要尽量精简;
- 要向jQuery学习,在页面里编写的代码应该有固定的套路,只要是两个页面都会使用javascript代码都应该迁移到自己编写的javascript库里,其次,页面的javascript开发里容易出错的代码都应该由javascript库来完成
因此我开发javascript库的时候将大量代码都迁移到一个外部文件里,这些代码有的是基础性的代码,例如一些工具类,grid的构建代码,遮罩功能等,还有些代码是业务代码例如:加密解密等,这些都集中在了一个外部文件,由于自己原框架的结构只是将这些代码通过匿名函数包装起来,而没有让基础性代码和业务代码解耦的方式,所以当时编写的javascript库就是一个大杂烩,很多东西交织在一起,这使得自己维护代码的时候不是很科学了,经常变成硬编码。
总而言之,我之前的javascript库的基础结构没有很好的扩展性和伸缩性,它没有将不同类型代码隔离出来的能力,所以我需要一个新的javascript基础框架,这样我以后再去开发一个javascript库,这个基础架构使得库更加的健壮。
我最近做了一些前端的项目,这个项目里单个页面都是非常复杂的,功能非常多,单个页面的javascript业务代码少则几百行,多则上千行,因此我们前端应用里有一个很大的公共库,但是到了每个页面又得单独写一个外部javascript文件做相应的业务处理,下面是这个项目javascript代码书写的基本结构:
var opts = { version:"1.0.0", name:"sharpxiajun" }; (function(opts){ function Clazz(){ return this.init(arguments); } Clazz.fn = Clazz.prototype = { init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接打印对象 console.log(this.settings); // 遍历对象输出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } } window.$ = new Clazz(opts) })(opts); // 测试 $.testInit();
运行结果如下所示:
不管我们使用外部公共类库,还是每个页面对应的javascript外部文件,都采用这个结构,仔细分析下这个代码,它和我之前写的javascript库的结构并没有高明之处,只不过我之前的javascript库是直接使用javascript的对象,而这个结构无非换成了面向对象的写法,而这个面向对象其实是不好使用继承的面向对象,是个孤立的对象,因此实际开发里我们要区别不同的功能模块,只得新建多个不同的功能对象,最后整个应用里会有N多个相互独立的功能对象,这其实和我原来库的写法有着同样的不易扩展的问题。而且这个代码有个让我开发时候很不习惯的问题,就是在具体页面开发里,我必须先要构建一个参数对象,并把对象传到外部文件的接口里,如果你没有提前构造参数对象,那么外部javascript代码就会出错。
不过这两种写法的差异让我对编写javascript库有了新的想法,这个想法具体内容如下:
一个web前端应用里,排除一些公用的库例如我必用的jQuery,可能还有时间控件的库(我以后做web前端估计都会尽量让我的外部库最少,像jQuery,requireJs,seajs这样的库我基本是毫无保留的使用,像什么eaysui,jqgrid,extjs这样的库我会想尽办法舍弃),其他的javascript代码都是程序员要自己编写的,程序员自己写的代码不管它是业务代码还是通用代码都应该是一个整体,这个整体的表现就是它们都可以用一个对象进行输出,看看我上面讲的两种写库的基础结构,它们的共同问题要么就是通用代码和业务代码交织,耦合,要么就是相互独立,关系僵硬。
此外,生产上能把javascript合并成少量文件也是非常重要的,上面的第二种库的写法(把页面的业务代码抽取到外部文件)目的之一就是让文件合并比较容易,但是这种堆砌式的合并文件总让我感觉有点不是很舒服的味道。我觉得对于复杂页面单独一个javascript外部文件有很多好处,这个做法还是不能舍弃的,但是这个外部文件最好和超大的公共库有一个逻辑关系,这个关系最好像jQuery原始库和它的插件之间的关系,如果有这样的关系我们再合并文件,这个做法感觉就会好多了。
最后,我自己写库的做法没有使用面向对象编程,使用的是javascript对象本身的特点,这个如果换到面向对象编程里就是类的静态变量方案,而另外一种写法则是实例化对象的实现方案,我觉得这两种写法都有可取之处,也有不足地方,最好我们设计的库应该兼容这两个机制。
下面就是我根据上面思考写的新的基础javascript基础框架模型,代码如下:
(function(window,document){ function Clazz(){ return this.init(arguments); } Clazz.fn = Clazz.prototype = { init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接打印对象 console.log(this.settings); // 遍历对象输出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } }; Clazz.addStaticMethod = function(nmSpace,obj,ftn){ if (!Clazz[nmSpace]){Clazz[nmSpace] = {}} for (var i in obj){ Clazz[nmSpace][i] = obj[i]; } if (ftn) {ftn()} } Clazz.addObjectMethod = function(nmSpace,obj,ftn){ if (!Clazz.fn[nmSpace]){Clazz.fn[nmSpace] = {}} for (var i in obj){ Clazz.fn[nmSpace][i] = obj[i]; } if (ftn) {ftn()} } window.Clazz = Clazz; })(window,document,undefined) var opts = { version:"1.0.0", name:"sharpxiajun" }; Clazz.addStaticMethod("myStatic",{ sClz:"static", staticFtn:function(){ console.log(Clazz["myStatic"].sClz); } },function(){ console.log("Add Static Method End!!!!!!!"); }) Clazz.addObjectMethod("myFirst",{ sParam:"sharp", ftn01:function(s){ this.sParam = s; return this; }, ftn02:function(){ console.log("sParam:" + this.sParam); return this; } },function(){ console.log("Add Object Method End!!!!!!!"); }) var $ = new Clazz(opts); // 测试一 $.testInit(); // 测试二 console.log($.myFirst.sParam); $.myFirst.ftn01("My God!!!").ftn02(); // 测试三 console.log(Clazz.myStatic.sClz); Clazz.myStatic.staticFtn();
页面运行结果如下所示:
这个代码里我在匿名函数里返回的是类(javascript里其实没有类的概念,但是有构造函数,所以javascript里的构造函数承担了类的作用,所以这里提javascript里的类应该是没问题的),而不是已经实例化好的对象。这样返回的东西既可以有静态的方法和属性又有属于对象的方法和属性了。
该结构里有一个Clazz.addStaticMethod方法,它的作用是给定义的类添加静态的方法,这个方法我设计了三个参数,第一个参数是个字符串,业务函数就是静态方法的作用域,看下面实例代码:
console.log(Clazz.myStatic.sClz);
Clazz.myStatic.staticFtn();
这样就等于给静态变量一个保护,有人会问如果我们不传第一个参数怎么办?认真的童鞋注意,现在我给出的代码只是想表达我的想法,到了生产实现时候该方法会更加丰满点,那时如果用户不传作用域字段,那么添加的静态方法和属性就是直接属于类本身的。
第二个参数就是具体要添加的静态属性和静态方法,这里我用的是对象类型。
第三个参数是个回调函数,当静态方式添加成功后调用,当然用户也可以不传,加个回调函数参数,是我觉得在设计javascript方法时候都应该给一个回调函数,这就是在运用事件驱动编程的思想,这其实为自己留有余地,这么做常常会在你意想不到的时候发挥重要作用。
方法Clazz.addObjectMethod是给对象添加方法和属性的,这些方法和属性石赋予给原型对象的,参数类型和addStaticMethod相同,这里就不在累述了。
用户在使用addObjectMethod方法时候要注意this指针的运用,在每个要添加到对象的方法最后加上一个return this,这么做好处就是每个对象的返回值都是对象本身,这样我们就可以让我们的库拥有和jQuery一样的连缀写法,例如下面的代码:
console.log($.myFirst.sParam);
$.myFirst.ftn01("My God!!!").ftn02();
在生产开发里,我们可以把公共的javascript代码直接写到库里,页面里的业务代码则直接通过扩展返回类的静态方法和属性以及对象方法和属性进行扩充。
上面这个结构基本达到我的需要了,如果我以后用javascriptMVC思想开发前端我估计这个基本结构已经够用,当然还要做些代码健壮性的处理,如果是传统前端页面开发估计还会有一定修改,传统网站的页面开发都是服务端和html混搭的,例如jsp,velocity文件,因此有很多重要数据都是服务端变量生成的,我们时常要把这些变量作为参数传给外部javascript文件,每个页面里的参数是不一样的,所以必须有个对象专门接收这个对象。这个好做,因此这里就不累述了。
上面这个结构使用了面向对象继承机制,原始库是父对象,而业务javascript文件则是子类了,不过这个子类是用命名空间来区分的。
不过有时候我们可能想冒险替换整个父对象的内容,这个需求我想在平时开发里并不常见,不过我还是写了一个这样的方法,下面是我改进的代码,具体如下:
(function(window,document){ function Clazz(){ return this._init(arguments); } Clazz.prototype = { _init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接打印对象 console.log(this.settings); // 遍历对象输出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } }; Clazz.addStaticMethod = function(nmSpace,obj,ftn){ if (!Clazz[nmSpace]){Clazz[nmSpace] = {}} if (Object.prototype.toString.apply(obj) == "[object Object]"){ for (var i in obj){ Clazz[nmSpace][i] = obj[i]; } window.Clazz = Clazz; } if (ftn) {ftn()} } Clazz.addObjectMethod = function(nmSpace,obj,ftn){ if (!Clazz.prototype[nmSpace]){Clazz.prototype[nmSpace] = {}} if (Object.prototype.toString.apply(obj) == "[object Object]"){ for (var i in obj){ Clazz.prototype[nmSpace][i] = obj[i]; } window.Clazz = Clazz; } if (ftn) {ftn()} } /*Clazz.newStaticMethod(){//todo..........}*/ Clazz.newObjectMethod = function(obj,ftn){ if (Object.prototype.toString.apply(obj) == "[object Object]"){ var tmpInit = Clazz.prototype._init; Clazz.prototype = obj; Clazz.prototype._init = tmpInit; window.Clazz = Clazz; } if (ftn) {ftn()} } window.Clazz = Clazz; })(window,document,undefined) var opts = { version:"1.0.0", name:"sharpxiajun" }; Clazz.addStaticMethod("myStatic",{ sClz:"static", staticFtn:function(){ console.log(Clazz["myStatic"].sClz); } },function(){ console.log("Add Static Method End!!!!!!!"); }) Clazz.myStatic.staticFtn(); Clazz.newObjectMethod({ newver:"1.0.3", testNewFtn:function(){ console.log(this.newver); return this; } },function(){ console.log("New Create Prototype Object End !!!!!!"); }); Clazz.addObjectMethod("myFirst",{ sParam:"sharp", ftn01:function(s){ this.sParam = s; return this; }, ftn02:function(){ console.log("sParam:" + this.sParam); return this; } },function(){ console.log("Add Object Method End!!!!!!!"); }) var $ = new Clazz(); console.log("================================"); console.log($.newver); $.testNewFtn().myFirst.ftn01("XXXX").ftn02(); console.log("================================");
运行结果如下所示:
上面的代码做了一定优化,代码里我只给出了替换原型的方法,没有提供替换静态的方法,这是因为替换原型还是有点意义的,替换静态变量其实就是替换整个类了,如果这么干,我这个结构还有啥意义哦。
文章写毕,今年写文章,文章里代码都很少,很多童鞋不习惯,今天补上一篇代码比较多的文章了。