如何构建大型web app?
在web pages时代,用脚本来做的事情少的可怜,校验用户输入,表单提交,简单的滚动 等。
后来ajax的出现,允许异步的去获取服务器数据,局部改变html文档内容或样式。于是以前要用两个或多个页面跳转才能表达的内容或交互逐渐被放到一个web page里面来。但由于网络速度,服务器性能,客户端性能,web技术瓶颈等原因。脚本能干的事情依旧有很大的局限。
随着近几年web技术的发展,特别是前端技术的发展,用户在浏览器里面浏览html文档时,不仅仅停留在“浏览”的层面了,开始要求更好的体验。 于是各大浏览器厂商也争先推出更快的脚本引擎,更快的渲染速度。脚本能做的事情越来越多,也不得不去做更多的事。
越来越多的交互开始被放在同一个page里面进行,当这种交互的浓缩到一定程度时,web 已经不再是一个个单纯的page 而已了。当一个独立的page拥有了一整套完整的用户交互与反馈时,这个web page其实就是一个application了。
时至今日,我相信web app时代已经来临,web os时代都离我们不远了。
--------------------------------
开发一个web app和开发web pages 其实有很多地方不同。可以这样说,web page 是文档驱动的,主体还是文档,以浏览形态为主。而web app 是脚本驱动的。html文档只是作为一个最开始的“骨架”,或者说作为脚本程序的载体,以后的内容都是经过 数据的模版化,动态填充,更新,反馈用户操作而成。
当做一个web app时,更像是做一个软件工程,是需要“程序化”,“数据可视化”,处理各种人机交互的。交互和数据的复杂度要求开发者不得不分模块,分步骤进行代码编写,在编写大型 web app时(“大型”的定义暂定为:除去基础类库,javascript代码在优秀架构下仍大于2000行的app)。 模块的划分很重要, 跨模块的通信模式也很重要,直接决定代码的复杂度和可维护性。
软件工程中很多优秀的设计法则和设计模式都开始被引用进来。通常来说,app大,及时分模块,每个模块也不会小。为了保持模块的独立性,模块间的解耦就变得尤为重要,否则不仅后期维护困难,哪怕自己开发到后期,由于耦合而造成的“牵一发而动全身”的情况会让代码的编写越来越困难,自己都会被自己绕晕。
迪米特法则
迪米特法则是为了降低模块之间耦合性的一条很重要的法则:(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
具体解释可以参照百度百科http://baike.baidu.com/view/823220.html?fromTaglist 或者维基百科。从迪米特法则衍生出了(门面模式,中介模式,观察者模式等)众多设计模式,初衷都是为了解耦。
导演模式
前几天跟朋友聊天的时候,说到现在的导演各种权力大,各种威逼利诱,各种片场老大,我突然想到这种一人独大的情况用在开发中还真是个不错的注意。
以下我简单阐述所谓导演模式的工作流(有点像是中介模式和观察者模式的综合):
前提:
一台剧只有一个导演,所有演员都必须听他的。
演员之间没有沟通权利,所有需要和另一个演员进行交互都必须经过导演。
导演醒来 >> 叫醒所有属于他管的演员 >> 给第一幕的演员分配最开始的一次任务
演员之间木有交互,他有事情发生只能通知导演,导演接到通知后做出判断,下出下一道指令反馈给他或者其他的演员。
导演观察着所有的演员,清楚每个演员的一举一动
每个演员他只能知道发生在自己身上的事,并不能直接知道别的演员身上所发生的事。
甚至他自己都没有任何的主见,没有权利控制自己的行为,他要做的只是通知导演,再完成导演传下的命令。
一个导演醒来的时候,他必须清楚两件事:
1.这场戏的第一幕会有哪些演员参与,他们分别需要做什么事。
2.要做一个好导演,他必须预先知道所有演员可能会发生的事,并且都有相对应的策略。才能导演好这场戏。
一个演员被导演叫醒的时候, 他需要知道:
1.他最开始该是什么样子
2.如果自己发生了事情(比如被click之类)的话,立刻通知导演。
3.拿到导演反馈回来的命令,接着演。
4.演员一定是没有主见的,遇到事情的时候不能私自决定该怎么做。
这种模式在很大程度上符合迪米特法则,演员间没有任何直接交互,每个演员都只与导演有交互,而且都只有简单的通知和被通知的交互。
相当于导演是所有演员的中介,也是所有演员的观察者。
优点:由于源自迪米特法则,所以每个模块基本完全独立,模块之间没有依赖性,自然不会相互影响。开发者在开发的时候只要专注于当前开发的模块,而不必担心会对其他模块造成影响。同时这在团队分工合作中也是很方便的。
另外,由于降低了模块间的耦合性,代码可维护性大大增强。对开发者来说,由于少了模块间的交互,所以交互逻辑会少很多,同时,所有的交互接口都集中在导演那一块,在跟踪调试的时候也很方便。不会为了一个功能在模块间绕来绕去,最后自己被自己绕晕。
缺点:和所有设计模式一样,在一定程度上会增加代码的复杂度
Director Pattern -- 最简易的一种实现
/**
* Person
* Director & Actor 的基类
* 所需的一个人的基本属性【名字,是否醒着,醒来要干的事】
*/
var Person = Class.extend({
init: function (name) {
this.name = name;
this.isWaking = false;
this.$WAKETODO = [];
},
$wake: function () {
this.isWaking = true;
},
$sleep: function () {
this.isWaking = false;
}
});
导演类继承于Person:
它有额外的几个权利,
他醒来后要叫醒所有属于他管的演员开始工作。
同时他观察着所有的演员,
于是:
/**
* Director 继承自Person
*/
var Director = Person.extend({
init: function (name, fn) {
this._super(name);
!!fn && fn.call && fn.apply && extend(this, new fn);
this.actors = [];
this._observes = {};
},
// 自己醒来会叫醒所有演员
$wake: function (wakeFn) {
this._super();
wakeFn != undefined && this.$WAKETODO.push(wakeFn);
this.$firstAct != undefined && this.$WAKETODO.push(this.$firstAct);
for (var i = 0; i < this.actors.length; i ++) {
var actor = this.actors[i];
if (actor instanceof Actor) {
actor.$wake();
}
}
for (var j = 0; j < this.$WAKETODO.length; j ++) {
var fn = this.$WAKETODO[j];
if (typeof fn == 'string') {
fn = this[fn];
}
!!fn.call && !!fn.apply && fn.call(this);
}
},
$sleep: function (sleepFn) {
this._super();
!!sleepFn && sleepFn();
},
// 有观察的权利,观察着所有属于他的演员
$observe: function (actor, type, listener) {
if (this._observes[actor.name] == undefined) {
this._observes[actor.name] = {};
}
if (this._observes[actor.name][type] == undefined) {
this._observes[actor.name][type] = [];
}
this._observes[actor.name][type].push(listener);
}
})
演员比较悲惨,没有叫醒别人的权利,甚至没有控制自己的权利,他只能通知导演,然后被导演一手把控:
/**
* Actor
*/
var Actor = Person.extend({
init: function (opt, fn) {
this._super(opt.name);
!!fn && fn.call && fn.apply && extend(this, new fn);
this.director = opt.director;
if (this.director instanceof Director) {
this.director.actors.push(this);
}
},
$wake: function (wakeFn) {
this._super();
wakeFn != undefined && this.$WAKETODO.push(wakeFn);
for (var i = 0; i < this.$WAKETODO.length; i ++) {
var fn = this.$WAKETODO[i];
if (typeof fn == 'string') {
fn = this[fn];
}
!!fn.call && !!fn.apply && fn.call(this);
}
},
$sleep: function (sleepFn) {
this._super();
!!sleepFn && sleepFn();
},
// 演员能做的,只是通知导演而已
$notifyDirector: function (type, arg) {
var listeners = this.director._observes[this.name][type];
if (!!listeners) {
for (var i = 0; i < listeners.length; i ++) {
listeners[i].apply(this, Array.prototype.slice.call(arguments, 1));
}
}
}
})
=====================================
ok,有了上面百来行代码,基本的导演模式就出来了。我们按照上面说的思路用它写一个简单的demo作为实践。
一个简单的基于Director Pattern 的 Tab
(由于一个tab过于简单,不能体验这种设计思路的优势,反而在某些地方会觉得有些累赘...)这里仅作思路展示之用。
// Director pattern
// 声明一个名为 tab的导演,并且让导演知道第一幕要做什么
var Dr = new Director('tab', function () {
// 第一幕的安排,会在导演 wake的时候执行
this.$firstAct = function () {
// 显示第一个tab
tabH.setCurrent(0);
tabC.setCurrent(0);
};
});
// tab haeder
// 模块之一,声明一个名为 tabHeader 的演员,所属导演为 Dr
var tabH = new Actor({
name: 'tabHeader',
director: Dr
}, function () {
this.EL_UL = $('tab-header');
this.EL_BTNS = this.EL_UL.getElementsByTagName('li');
this._current = -1;
// 这个演员醒来后要做什么
this.$WAKETODO = ['bindEvent'];
// 用于设置自己当前状态的方法,不过自己内部不会去调,导演会告诉他什么时候调用
this.setCurrent = function (ind) {
if (ind < this.EL_BTNS.length) {
this.EL_BTNS[this._current] && removeClass(this.EL_BTNS[this._current], 'current');
addClass(this.EL_BTNS[ind], 'current');
this._current = ind;
}
};
this.bindEvent = function () {
var self = this;
addEvent(this.EL_UL, 'click', function (e) {
e = e || event;
var tar = e.target || e.srcElement;
if (tar.nodeName.toLowerCase() == 'a') {
self.$notifyDirector('clicked', tar.getAttribute('data-index'));
}
})
}
});
// tab content
// 模块之二, 声明一个名为 tabContent 的演员,同样属于导演 Dr
var tabC = new Actor({
name: 'tabContent',
director: Dr
}, function () {
var self = this;
this.EL_UL = $('tab-content');
this.EL_CONS = this.EL_UL.getElementsByTagName('li');
this._current = -1;
// 自己的一个方法
this.setCurrent = function (ind) {
if (ind < this.EL_CONS.length) {
this.EL_CONS[this._current] && removeClass(this.EL_CONS[this._current], 'current');
addClass(this.EL_CONS[ind], 'current');
this._current = ind;
}
}
});
// Director observe Actors
// 导演观察者所有演员,由于这里只有tabHeader与外界有 接触,所以添加对tabHeader的观察者。
// 并确认接到通知后的反馈。
Dr.$observe(tabH, 'clicked', function (ind) {
tabH.setCurrent(ind);
tabC.setCurrent(ind);
})
Dr.$wake(); // 导演醒来,所有工作开始。