【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 >\r\n');
  12:      html.push('<b >请勿关闭本窗口</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('&nbsp;&nbsp;&nbsp;&nbsp;节目列表(点击节目播放)');
  22:      };

事实上,我更愿意把这理解成 javascript 独特的多线程编程模式。即使运用 setTimeout 去运行某个大工作量函数,浏览器照样会明显反应变慢。因此只有做一小段活,然后休息一会(延时1秒执行),这样浏览器的执行时间就被你和你的程序“分时”共享了,并且各自都还显得顺畅,即便程序执行的总时间变长了。我在应用过程中发现,用户其实不需要很快加载完所有数据,而是要能尽快使用上已加载的数据(尽快的响应)!我的这个分片、多次运算的方法还算比较好的满足了这个需求!


这段日子在持续地看一本说 .net 框架的书,每天读一点。初级的程序员要求的不过是“实现功能”,而稍专业些的要多花些精力在性能、扩展性等方面。没有引用 jquery 前,我所有的HTML标签创建都是靠的拼接字符串,引用后,我全用了它了构造器,我相信框架一定是以最优的方法去实现的;这也是框架的价值所在。

本文虽然只是记录了几个实践环节中的小的点,但却是有实用价值的:有强烈性能要求的场合,不妨构造自己的轮子(TreeView);在实在没有服务端支持的前提下,不妨使用“蛮力”哪怕丑陋的解决一个问题(蛮力跨域);当然,最好的办法是得到服务端支持,一起优雅的解决困难(jsonp协议);在庞大的工作量面前,学会分而治之,劳逸结合(局部运算);最后,至少我现在不用下载客户端(安装后经常有隐秘进程干扰),却能在web上像在客户端上一样看看高清电影、电视了吧(解决海量数据加载、分析的策略问题;同时邀请大家试用)。

补充:我在默认的文章签名里插入了该Web视频应用的代码;因博客园默认引用了Jquery库,而我的初始界面创建代码只有几百字节,因此不会对大家的页面加载速度造成任何影响(它只在当前页面其他部分全部加载完成之后才异步创建)。希望有兴趣的网友在试用的过程中多提意见,尤其可指导一下如何使得应用数据加载、展示更优。至于视频内容上的需求,也可联系我,我可以不定期更新博客园视频数据的快照(之所以不公开源,是为了防止恶意的网络攻击行为)。

本地播放页面下载