资料:cab与mvp模式
http://www.agilelabs.cn/blogs/wind_tower/archive/2006/01/26/626.aspx Fowler MVP模式
http://www.martinfowler.com/eaaDev/ModelViewPresenter.html 之前一篇文章中用dojo的事件订阅、发布机制实现了声文同步效果,相关信息可以参考
http://www.cnblogs.com/sharplife/archive/2006/08/22/483476.aspx 由于项目需要,改用了轻量级的客户端库prototype,抛弃dojo的事件机制,因此重写声文同步的实现,顺面利用昨天晚上时间总结一下,我下面的介绍的实现中用到些prototype库中元素,但其实可以很方便的改成完全native的js实现。
对于实现中涉及的传输数据的json模型可以参见之前的文章,这次实现采用mvp模式,说的明了些与mvc模式的不同处就是完全解除了model、view之间的耦合,也即model和view可以相互不知道对方的存在,用presenter控制器实现view、model的交互、更新(和之前的实现功能上是一致肯定会有许多相似处)。
以上类图其实是之前在之前用c#在winform实现时留下的,图中只显示了各接口的关键方法、属性及事件定义(当然在下面的js实现中我就没定义相应的接口了,因为脚本中接口概念很模糊),IAudioUtil适用于包装播放控件的接口,这样在不同的播放控件间切换就方便了,其关键事件有PositionChanged和StatusChanged,在之前的c#实现中PositionChanged是用用一个后台线程单独模拟的(这样在播放状态变化的时候就需要适时的调整后台线程的状态,如Suspend、Abort等,在涉及与view交互时还要考虑
非主线程对UI的更新),下面js的实现中,我们用prototype中的PeriodicalExecuter模拟PositionChanged事件的实现(因为我们的控件未提供、或是提供的PositionChanged事件不符合要求),相对来讲更方便,当然完全可以用js的setTimeout或setInterval native实现。
下面介绍一个正常播放时各对象的交互,播放控件的PositionChanged事件主要由IPModel中的每个子IsentenceModel订阅,当检查到当前播放的position位于自身(一个sentence)的开始、结束时间之间,则触发自身的ItemInvoke事件,而各子model的此事件被IPresenter订阅,继而做更新IPModel、更新IPView的操作,并最终触发由Ipresenter所拥有的唯一的事件displaySentence,由其他订阅者订阅以做个性化的显示,交互时序如下:
另一个典型的交互便是由用户单击、或者双击view(我们的具体实现中体现为web页面的容器元素),首先view会更新自身的状态(更新当前sentence),因为这没必要经由控制器通知view再去更新自身,然后触发view本身所拥有的click、dbclick事件(这两个事件是在view类中模拟出来的,并非web页面的单、双击事件,不过view中各sentence所处容器元素span的单、双击会分别触发此两事件),之后订阅此二事件的Presnter控制器会进行设置播放器信息(如暂停,当前播放位置)、更新model的操作,之后同上一个交互过程,触发displaySentence事件,如果是双击除了同样执行上述操作外会还有最后一个播放操作(因为双击事件会首先触发单击事件,所以我们的实现就简单了,同样也是值得注意的地方)。
主要实现代码如下:
![](/Images/OutliningIndicators/ContractedBlock.gif)
播放控件对象
1
//AudioUtil class
2
var AudioUtil=Class.create();
3![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
AudioUtil.prototype=
{
4![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
initialize:function(nct)
{
5
this.audio=nct;
6
this.curPos=0;
7
},
8![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
setPos:function(pos)
{
9
//will cause nct_audio stop, invoke stop event
10
//and invoke this.stop method
11
this.audio.currentPosition=parseInt(pos*1000);
12
},
13![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
canPlay:function()
{
14
return this.audio.CanPlay;
15
},
16![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
getDuration:function()
{
17
return this.audio.Duration/1000.0;
18
},
19![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
record:function()
{
20
this.audio.Record();
21
},
22![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
play:function()
{
23![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(this.canPlay())
{
24
this.audio.Play();
25
if(this.pe)this.pe.stop();
26![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this.pe=new PeriodicalExecuter(function()
{
27![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(Math.abs(this.curPos-this.getPos())>.1)
{
28
this.curPos=this.getPos();
29![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this.onPositionChanged(
{CurrentPosition:this.curPos});
30
}
31
}.bind(this),0.1);
32
}
33
},
34![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
open:function(url)
{
35
if(this.pe)this.pe.stop();
36
this.curPos=0;
37
this.audio.Open(url);
38
},
39![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
pause:function()
{
40![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(this.canPlay())
{
41
if(this.pe)this.pe.stop();//insure you could select specified sen
42
this.audio.Pause();
43
}
44
},
45![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
stop:function()
{
46![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
try
{//prevent some err when refresh page
47
if(this.pe)this.pe.stop();//prevent after read over to goback above sen
48
this.audio.Stop();
49
this.curPos=0;
50
//this.onPositionChanged({CurrentPosition:0});
51![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
}catch(exc)
{}
52
},
53![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
close:function()
{
54
this.curPos=0;
55
this.audio.Close();
56
if(this.pe)this.pe.stop();
57
},
58![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
select:function(start,end,flag)
{
59
this.audio.Select(start,end,flag);
60
},
61![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
scale:function(mode,selected)
{
62
this.audio.Scale(mode,selected);
63
},
64![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
clearSelection:function()
{
65
this.audio.ClearSelection();
66
},
67![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
getPos:function()
{
68
if(this.audio)
69
return this.audio.currentPosition / 1000.0;
70
else
71
return .0;
72
},
73![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
onPositionChanged:function(e)
{
74![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
$H(this.positionChanged).each(function(paire)
{
75![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(typeof(paire.value)=='function')
{
76![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
try
{
77
(paire.value)(e);
78![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
}catch(exc)
{}
79
}
80
});
81
},
82![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
positionChanged:
{}
83
}; 注意AudioUtil类中positionChanged事件的实现,如上面所提在c#中需控制后台线程的状态,我们同样要适当控制PeriodicalExecuter的执行(如在pause、stop等状态),另外由于positionChanged事件会被各子model订阅,所以我们将其订阅者的处理方法放在js对象中,在触发时用protype的$H结合each遍历并执行(传输对象参数e)所有处理方法。
![](/Images/OutliningIndicators/ContractedBlock.gif)
Model对象
//ParagraphModel class
var ParagraphModel=Class.create();
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
ParagraphModel.prototype=
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
initialize : function(jsonData,duration,audio)
{
var jsonObj=eval('('+jsonData+')');
var sentences=[];
this.audioCtrl=audio;
this._url=jsonObj.MediaUrl.replace(/\/\/localhost/i,'//'+location.hostname);
this.audioCtrl.open(this._url);
duration=this.audioCtrl.getDuration();
if(duration)
this._flag=true;
else
this._flag=false;
this._percentMis=1;
var i=0;
var latestSen=null;
var _this=this;
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
jsonObj.Sentences.each(function(sen)
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(_this._flag)
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(latestSen)
{
sen.StartTime=latestSen.EndTime;
}
sen.EndTime=sen.StartTime+sen.DurPercent*duration;
latestSen=sen;
}
_this._percentMis = _this._percentMis-sen.DurPercent;
sen.SentenceText=sen.SentenceText.replace(/(.+)\|\|。$/,'$1');
var sentenceObj=new SentenceModel(i,sen,_this.audioCtrl);
sentences.push(sentenceObj);
i++;
});
this._sentences=sentences;
this.setDuration(duration);
this._selectedSentence = sentences[0];
},
setDuration : function(duration)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
this._flag = true;
var mis = this._percentMis * duration;
var latestSen=null;
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this._sentences.each(function(sen)
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(latestSen)
{
sen.StartTime=latestSen.EndTime;
}
sen.EndTime=sen.StartTime+sen.DurPercent*(duration+mis);
latestSen=sen;
});
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
getSentences : function ()
{
return [].concat(this._sentences);
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
addItem : function (sentence)
{
this._sentences.push(sentence);
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
setSelected : function (sentence)
{
this._selectedSentence = sentence;
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
isContains : function(sentence)
{
return this._sentences.include(sentence);
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
reset : function ()
{
this._selectedSentence = this._sentences[0];
},
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
uninit: function()
{
delete this._sentences;
delete this._url;
delete this._selectedSentence;
this._sentences = null;
this._url = null;
this._selectedSentence = null;
}
};
![](/Images/OutliningIndicators/None.gif)
//SentenceModel class
var SentenceModel=Class.create();
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
SentenceModel.prototype=
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
initialize:function(i,sen,audio)
{
this.id='iFlyItem'+i;
this._jsonSentence=sen;
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
audio.positionChanged[this.id]=function(e)
{
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if (this._jsonSentence.StartTime <= e.CurrentPosition && this._jsonSentence.EndTime > e.CurrentPosition)
{
this.itemInvoked(this);}
}.bind(this);
},
itemInvoked:Prototype.emptyFunction
}; ParagraphModel主要完成分析从服务端获取的json串、计算各sentence的开始、结束时间、构造出各子SentenceModel(自身完成对播放器positionChanged事件的订阅)
![](/Images/OutliningIndicators/ContractedBlock.gif)
Presenter对象
1
//ParagraphPresenter class
2
var ParagraphPresenter=Class.create();
3![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
ParagraphPresenter.prototype=
{
4![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
initialize:function(model,view)
{
5
this._model=model;
6
this._view=view;
7
8
var _this=this;
9![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this._model._sentences.each(function(sen)
{
10![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
sen.itemInvoked=function(e)
{
11
this._model.setSelected(e);
12
this._view.select(e);
13
this.displaySentence(e);
14
}.bind(_this);
15
});
16
17![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this._view.click=function(e)
{
18
_this.commonHandler(e);
19
};
20![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
this._view.dbClick=function(e)
{
21
_this._model.audioCtrl.play();
22
};
23
},
24![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
commonHandler:function(sen)
{
25
this._model.audioCtrl.pause();
26
this._model.audioCtrl.setPos(sen._jsonSentence.StartTime);
27![](/Images/OutliningIndicators/InBlock.gif)
28
this._model.setSelected(sen);
29
this._model.audioCtrl.clearSelection();
30
this._model.audioCtrl.select(sen._jsonSentence.StartTime,sen._jsonSentence.EndTime,true);
31
this.displaySentence(sen);
32
},
33
displaySentence:Prototype.emptyFunction
34
}; ParagraphPresenter拥有model及view的实例,订阅了view的click及dbclick事件,同时订阅了model中各子model的itemInvoke事件,完成model、view之间通讯。
![](/Images/OutliningIndicators/ContractedBlock.gif)
View对象
1
//ParagraphView class
2
var ParagraphView=Class.create();
3![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
ParagraphView.prototype=
{
4![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
initialize:function(sens,container)
{
5![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
$A(container.childNodes).each(function(node)
{
6
container.removeChild(node);
7
});
8
container.innerHTML='';
9
container.innerText='';
10
11
var span = document.createElement("span");
12
var br=document.createElement("br");
13
var _this=this;
14![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
sens.each(function(sen)
{
15![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
if(!(/^\|\|。$/.test(sen._jsonSentence.SentenceText)))
{
16
var sp=span.cloneNode(false);
17
sp.id=sen.id;
18
sp.appendChild(document.createTextNode(sen._jsonSentence.SentenceText));
19
sp.appendChild(br.cloneNode(false));
20
container.appendChild(sp);
21
22
sp.onclick=_this.onCick.bind(_this,sen);
23
sp.ondblclick=_this.onDbClick.bind(_this,sen);
24
}
25
});
26
this.select(sens[0]);
27
},
28![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
select:function(sen)
{
29
var cur=$$('span.current')[0];
30
if(cur)
31
Element.removeClassName(cur,'current');
32
var ele=$(sen.id);
33
if(ele)
34
Element.addClassName(ele,'current');
35
},
36![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
onCick:function(e)
{
37
this.select(e);
38
if(this.click!=Prototype.emptyFunction)
39
this.click(e);
40
},
41
click:Prototype.emptyFunction,
42![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
onDbClick:function(e)
{
43
if(this.dbClick!=Prototype.emptyFunction)
44
this.dbClick(e);
45
},
46
dbClick:Prototype.emptyFunction
47
};
ParagraphView的首要任务是根据传入的sentence数据及html元素将model展现在web页面上,其中完成了各sentence在web页面上的单双击触发ParagraphView的click、dbclick事件(将各自本身sentence对象做参数传入)的操作。
demo效果截图就不放了,实现一般的声文同步(如歌词秀应该不成问题),总结一下,本次实现并没有过多考虑js在ie上的memory leak问题,本实现用了n多js closure,想是循环引用是免不了的了,优化应该有许多可以做吧,就先说到这了(没做较大段落文本的测试)。
[回应下之前的文章,现在js的开发、调试环境越来越好了,而RIA的解决方案也增多了]