【javascript】最简洁的TreeView、“蛮力跨域”、Jsonp协议、局部运算以及仿客户端的Web视频应用例子(含下载)
简单经验小结,性烈老鸟莫入!偶尔眼不见、心不烦啊!
最近弄了个Web应用:有大量的数据需要以树形结构展示,另外数据存储在不同域名的站点。
首先要解决展示的问题。我刚开始引用了 jquery 插件 simpleTreeD&D,易用、好用,推荐给大家;引用个小例子:
1: <li><span class="text">Tree Node 1-1</span>
2: <ul class="ajax">
3: <li>{url:tree_load.php?tree_id=1}</li>
4: </ul>
5: </li>
1: $('#SomeTreeId').simpleTree({
2: afterMove:function(dd, ds, pos)
3: {
4: var msg = "position- "+pos+" \n"
5: +"drag destination ID- "+$(dd).attr("id")+"\n"
6: +" drag source ID- "+$(ds).attr("id");
7: alert(msg);
8: }
9: });
只可惜我引用的 json 实例数据量很大(单文件最大 2 m左右),下载就颇费时间,再用这个树形控件计算、绘图:持续相当一段时间的无响应状态!
网上搜了个极其简单的、没使用任何框架的实现树形视图的例子,遗憾的是现在回头怎么也找不到了,实在不知道当初自己用的什么搜索关键字。好在源码大意还记得,能说明一些问题:
1: <script type="text/javascript">
2: window.nodeClick = function(sender) {
3: sender.title = sender.title == 'none' ? '' : 'none';
4: var v = sender.nextSibling;
5: while (v.nodeName != 'B') {
6: v.style.display = sender.title;
7: v = v.nextSibling;
8: }
9: }
10: </script>
11: <b onclick="nodeClick(this)">a</b><br /><a>1</a><br /><a>2</a><br /><a>3</a><b></b><br />
12: <b onclick="nodeClick(this)">b</b><br /><a>1</a><br /><a>2</a><br /><a>3</a><b></b><br />
13: <b onclick="nodeClick(this)">c</b><br /><a>1</a><br /><a>2</a><br /><a>3</a><b></b><br />
受此启发,在后来多次的实验中,我最终确定了如下的 HTML 格式:
1: <ul>
2: <li><b>+</b><a>A</a>
3: <ul>
4: <li><b>+</b><a onclick="">a</a>
5: <ul>
6: <li>1</li>
7: <li>2</li>
10: </ul>
11: </li>
12: <li><b>+</b><a onclick="">a2</a>
13: <ul>
14: <li>1</li>
15: <li>2</li>
18: </ul>
19: </li>
20: </ul>
21: </li><li><b>+</b><a>B</a>
22: <ul>
23: <li><b>+</b><a onclick="">b</a>
24: <ul>
25: <li>1</li>
26: <li>2</li>
29: </ul>
30: </li>
31: <li><b>+</b><a onclick="">b2</a>
32: <ul>
33: <li>1</li>
34: <li>2</li>
37: </ul>
38: </li>
39: </ul>
40: </li>
41: </ul>
1: window.qv_OnNodeToggle = function(sender) {//触发节点展示切换
2: var v = $(sender.parentNode);
3: var ul = v.children("ul");
4: var b = v.children("b");
5: v = b.html();
6: switch (b.html()) {
7: case ('+'):
8: b.html('-');
9: ul.show();
10: break;
11: case ('-'):
12: b.html('+');
13: ul.hide();
14: break;
32: default:
33: alert('正在加载当前分类, 请稍等...');
34: break;
35: }
36: event.cancelBubble = true;
37: };
这就是“最简洁的TreeView”!该结构简洁明了,比较容易维护,而且只有必要数据(消减了图标、连线等),对于要求性能第一的应用来说,再合适不过。
接下来是脚本跨域的问题。起手时,我运用了一个非常蛮力的方法:
1: window.LoadOnlineCategory = function(categoryId, categoryName) {
2: window.w = window.open("", "newwin", "height=200px, ...,menubar=no");
3: var html = new Array();
4: html.push('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 ....\r\n');
5: html.push('<html xmlns="http://www.w3.org/1999/xhtml">\r\n');
6: html.push('<head>\r\n');
7: html.push('<title>');
8: html.push(categoryName);
9: html.push(' - 正在加载节目列表...</title>\r\n');
10: html.push('</head>\r\n');
11: html.push('<body style="background-color: Black;color:White">\r\n');
12: html.push('<b style="color:Red">请勿关闭本窗口</b>! ...自动关闭</b>...\r\n');
13: html.push(['<script ...src="http://.../Category_', categoryId, '.js"><\/script>\r\n'].join(''));
14: html.push('<script type="text/javascript">\r\n');
15: html.push(' window.timer = 1;\r\n');
16: html.push(' window.done = function() {\r\n');
17: html.push(' if (window.queenVideos != undefined && ...) {\r\n');
18: html.push(' window.opener.CategoryLoadedCallback(window, window.queenVideos[0]);\r\n');
19: html.push(' }\r\n');
20: html.push(' else {\r\n');
21: html.push(' document.title = ["(", window.timer++, ")正在加载节目列表..."].join("");\r\n');
22: html.push(' setTimeout(window.done, 1000);\r\n');
23: html.push(' }\r\n');
24: html.push(' };\r\n');
25: html.push(' window.onload = function() {\r\n');
27: html.push(' setTimeout(window.done, 3000);\r\n');
28: html.push(' };\r\n');
29: html.push(' if(window.queenVideos == undefined){\r\n');
30: html.push(' document.location.reload();\r\n');
31: html.push(' }\r\n');
32: html.push('<\/script>\r\n');
33: html.push('</body>\r\n');
34: html.push('</html>');
35: w.document.write(html.join(''));
36: };
1: window.CategoryLoadedCallback = function(subWindow, category) {
2: var s = document.getElementById(['ul_', category.c].join(''));
3: var html = new Array();
4: var series = category.v;
5: var files;
6: for (var i = 0; i < series.length; i++) {
7: //...
8: }
9: subWindow.close();
10: s.innerHTML += html.join('');
11: };
在 IE8 下,居然能如预期地运行;两天前,使用这种方式加载跨域数据的应用发布了。这就是我所谓的“蛮力跨域”,只是其似乎不支持 Chrome 和 Firefox,让我觉得遗憾。
我给一位负责脚本开发的前同事看,被一顿鄙视,他跟我提了下 Jsonp 协议;在大致弄清了我的“野蛮”做法后,并不忘最后来了句:“SB和NB仅一步之差!”我不以为然,了解了下Jsonp协议,发现涉及到数据提供方的接口形式约束(必须以 callbackFunc({...}) 的形式返回数据),更是觉得实用性差。
我给一位广州的小鸟看(他的技术无论dota还是编程确实都很菜!),这鸟表现的饶有兴趣,我观察他确实有心去用,因此对他的意见多给了点关注;但他提了很多毫不客气的批评牢骚,打着“完全站在客户角度”的旗帜,其实就是技术上小白。终了连 web 播放器和客户端播放器都没整清爽!
他说这个鸟东西弹窗口真烦人!还要装什么鸟加速器,更烦人!
老子试图探讨的海量数据的加载、展示技术细节,它全然不懂、不管!
我一咬牙,还是用 Ajax 体验要好很多,还是试试 Jsonp 吧。
因为数据量很大,所以要求必须减少、甚至不用框架,不然用户会感觉页面加载很慢!我翻出了我早前包装的一段 Ajax 包装代码:
1: window._AjaxBuilder = function() {
2: if (window.ActiveXObject) {
3: var MSXML = new Array('MSXML2.XMLHTTP', 'Microsoft.XMLHTTP', ....); //IE 5,6,7
4: for (var i = 0; i < MSXML.length; i++) {
5: try {
6: var requestor = new ActiveXObject(MSXML[i]);
7: Ajax.prototype._GetRequestor = function() {
8: return new ActiveXObject(MSXML[i]);
9: }
10: break;
11: }
12: catch (e) {
13: }
14: }
15: }
16: else if (window.XMLHttpRequest) { //for Firefox, Opera,IE 7
17: Ajax.prototype._GetRequestor = function() {
18: return new XMLHttpRequest();
19: }
20: }
21: else {
22: throw { name: "ExplorerException", message: "不可识别的浏览器, 创建 _AjaxBuilder 失败." };
23: }
24: };
25:
26: window.Ajax = function() {
27: if (this._ajaxBuilder == null)
28: this._ajaxBuilder = new _AjaxBuilder();
29: var requestor = this._GetRequestor();
30: var _callbackId = null, _requestType = 'GET', _isAsynch = true, ...;
31: function _Callback() {
32: //...
33: }
34:
35: this._SetArgs = function(callbackId, DoneCallback, ..., isAsynch) {
36: //...
37: }
38:
39: this._Send = function(url, datas) {
40: //...
41: }
42: };
43:
44: window.Ajax.prototype = {
45: _ajaxBuilder: null,
46: _GetRequestor: null,
47: Send: function(url, callbackId, DoneCallback, ExceptionCallback, ..., datas) {
48: this._SetArgs(callbackId, DoneCallback, ..., isAsynch);
49: this._Send(url, datas);
50: }
51: };
52: window.ajax = new Ajax();
我将返回数据改造成如下:
1: qv_LoadCategories([{i:15,n:'动作片'},{i:16,n:'喜剧片'},{i:13,n:'综艺娱乐'}]);
本地测试时非常正常(直接打开电脑的一个 htm 文档 d:/index.html,文档内请求 files.cnblogs.com 上的 jsonp 协议数据)。我满怀希望的将脚本插入我的博客(http://www.cnblogs.com/),结果我的 Ajax 在请求数据时返回错误码 0!
实在没时间也懒得再去调试这些底层问题,还是请出 jquery 大神吧。看到它最小大概 50k 左右,反复权衡,决定自己还是从了算了!
1: $.ajax({
2: url: [window.qv_server, 'qv.js'].join(''),
3: dataType: "jsonp"
4: });
寥寥几行,解决了我所有的问题!后来的一两天时间里,我跟着把加载的过程、播放的过程变得更加简便、友好,以及增加类似电视剧剧集的关联播出等。
这就是 jsonp,当运用后才发现,原来脚本跨域可以变得这么优雅、简单!
我把成果赶紧展示给那位脚本同事,最后还不忘加了句:“都SB了,NB还会远吗?”
运用 Ajax 技术,是为了局部刷新,为了提高用户的体验。我在处理大量数据的发觉,在运算的过程中也不妨 Ajax 一下。
当我点击“大陆剧”的时候,意味着要加载接近一万部视频文件的相关信息(名称、播放串等),文件本身就接近 2m;浏览器在解析该庞大 json 数据时甚至还会卡一段时间。那么在我创建树形结构的时候,基本就是无响应状态了,时常浏览器还会显示超时错误,要强行中断脚本执行等;好在我使用了最简洁的TreeView结构,否则这个问题更加严重!
这些视频数据是增量的,据估测,最终这些数据将扩大至现在的 4 倍多,那么我当前的运算模式肯定是不适应的了!我猛然间有了上文提到的想法(在进行 Ado/.Net 数据库操作时,我有过类似做法:为了防止某个连接超时,就在单次连接期间做小的一个分片的活,通过循环多次完成整个任务):
1: window.qv_LoadCategory = function(category) {//加载某个分类节目
2: //...
3: setTimeout(window.qv_LoadCategoryPart, 100);
4: };
5:
6: window.qv_LoadCategoryPart = function() {//局部加载分类节目
9: var count = 0;
11: do {
12: if (count > 100) {
13: //...
14: setTimeout(window.qv_LoadCategoryPart, 1000);
15: return;
16: }
17: series = window._categoryTemp.v.pop();
18: //....
19: } while (window._categoryTemp.v.length > 0);
20: //...
21: $('#qv_listStatus').html(' 节目列表(点击节目播放)');
22: };
事实上,我更愿意把这理解成 javascript 独特的多线程编程模式。即使运用 setTimeout 去运行某个大工作量函数,浏览器照样会明显反应变慢。因此只有做一小段活,然后休息一会(延时1秒执行),这样浏览器的执行时间就被你和你的程序“分时”共享了,并且各自都还显得顺畅,即便程序执行的总时间变长了。我在应用过程中发现,用户其实不需要很快加载完所有数据,而是要能尽快使用上已加载的数据(尽快的响应)!我的这个分片、多次运算的方法还算比较好的满足了这个需求!
这段日子在持续地看一本说 .net 框架的书,每天读一点。初级的程序员要求的不过是“实现功能”,而稍专业些的要多花些精力在性能、扩展性等方面。没有引用 jquery 前,我所有的HTML标签创建都是靠的拼接字符串,引用后,我全用了它了构造器,我相信框架一定是以最优的方法去实现的;这也是框架的价值所在。
本文虽然只是记录了几个实践环节中的小的点,但却是有实用价值的:有强烈性能要求的场合,不妨构造自己的轮子(TreeView);在实在没有服务端支持的前提下,不妨使用“蛮力”哪怕丑陋的解决一个问题(蛮力跨域);当然,最好的办法是得到服务端支持,一起优雅的解决困难(jsonp协议);在庞大的工作量面前,学会分而治之,劳逸结合(局部运算);最后,至少我现在不用下载客户端(安装后经常有隐秘进程干扰),却能在web上像在客户端上一样看看高清电影、电视了吧(解决海量数据加载、分析的策略问题;同时邀请大家试用)。
补充:我在默认的文章签名里插入了该Web视频应用的代码;因博客园默认引用了Jquery库,而我的初始界面创建代码只有几百字节,因此不会对大家的页面加载速度造成任何影响(它只在当前页面其他部分全部加载完成之后才异步创建)。希望有兴趣的网友在试用的过程中多提意见,尤其可指导一下如何使得应用数据加载、展示更优。至于视频内容上的需求,也可联系我,我可以不定期更新博客园视频数据的快照(之所以不公开源,是为了防止恶意的网络攻击行为)。
本地播放页面下载