【原创】JavaScript 文件响应式异步加载器(单页稳定版)

我在做《i 飞扬》电子杂志 HTML5 在线版的过程中,为了在不改变 Web 前端程序猿的编程思维习惯的前提下,保证整个 WebApp 的好用、可靠,自己开发了一个【JavaScript 文件响应式异步加载器】—— EasyImport.js

开始只是一段放在 HTML <head /> 中的小脚本,没觉得是个多么复杂的东西。但随着应用的深入,要想做到上述自己定下的目标,代码不断地迭代,期间也因为严重的 Bug 而怀疑它的价值,所以有了后来的一次较大的局部重构。

但辛劳总会有收获 —— 个人更深刻地理解了 JavaScriptDOM 的一些细节,它本身也到了足够成熟的地步,作为几个线上实用项目的基础库,运行在很多人的浏览器中~

v0.6(单页稳定版)

这是 EasyImport.js 首个功能完备的稳定版,能给 单页应用 提供有力的异步加载、响应式适配支持,但不支持 <iframe /> 子页面的 JS、CSS 继承(也就是说,它独立运行于每一个加载它的父/子页面中,不考虑关联页面的相互联系)。它将提供长期 Bug 修复的官方维护,后续版本与本版的公共代码也将同步更新,但不会再改变代码的整体结构。而子页面继承将在下个稳定版给予集成~

//
//                >>>  EasyImport.js  <<<
//
//
//      [Version]    v0.6  (2014-11-17)  Stable
//
//      [Usage]      Only for loading JavaScript files in Single-Page Web,
//                   no Inherit support for Frames.
//
//
//              (C)2013-2014    SCU FYclub-RDD
//

(function(BOM, DOM) {
  var UA = navigator.userAgent,
    RootPath,
    JS_List,
    CallBack,
    EIJS = {
      AsyncJS: [],
      DOM_Ready: []
    };
  // ----------- Inner Basic Member ----------- //
  var is_IE = UA.match(/MSIE (\d)|Trident[^\)]+rv:(\d+)/i),
    is_Webkit = UA.match(/Webkit/i);
  var IE_Ver = is_IE ? Number(is_IE[1] || is_IE[2]) : NaN;
  var old_IE = IE_Ver && IE_Ver < 9,
    is_Pad = UA.match(/Tablet|Pad|Book|Android 3/i),
    is_Phone = UA.match(/Phone|Touch|Android 2|Symbian/i);
  var is_Mobile =
    (is_Pad || is_Phone || UA.match(/Mobile/i)) && !UA.match(/ PC /);

  function DOM_Loaded_Event(DOM_E, CB_Func) {
    if (!old_IE) DOM_E.addEventListener('load', CB_Func, false);
    else
      DOM_E.attachEvent('onreadystatechange', function() {
        if (DOM_E.readyState.match(/loaded|complete/)) CB_Func.call(DOM_E);
      });
  }
  function DOM_Ready_Event(_CB) {
    if (!old_IE) DOM.addEventListener('DOMContentLoaded', _CB, false);
    else if (self !== top) DOM_Loaded_Event(DOM, _CB);
    else
      (function() {
        try {
          DOM.documentElement.doScroll('left');
          _CB.call(DOM);
        } catch (Err) {
          setTimeout(arguments.callee, 1);
        }
      })();
  }
  function $_TN(HTML_Elements, TagName) {
    return HTML_Elements.getElementsByTagName(TagName);
  }
  function NewTag(TagName, AttrList) {
    var NE = DOM.createElement(TagName);
    if (AttrList)
      for (var AK in AttrList) {
        if (IE_Ver && AK == 'class') NE.className = AttrList[AK];
        NE.setAttribute(AK, AttrList[AK]);
      }
    return NE;
  }

  var DOM_Head = $_TN(DOM, 'head')[0];
  var DOM_Title = $_TN(DOM_Head, 'title')[0];
  // ----------- Standard Mode Meta Patches ----------- //
  DOM_Head.appendChild(
    NewTag('meta', {
      'http-equiv': 'Window-Target',
      content: '_top'
    })
  );
  if (is_Mobile) {
    if (!old_IE) {
      var is_WeChat = UA.match(/MicroMessenger/i),
        is_UCWeb = UA.match(/UCBrowser|UCWeb/i);
      DOM_Head.insertBefore(
        NewTag('meta', {
          name: 'viewport',
          content: [
            [
              'width',
              is_Phone && (is_WeChat || is_UCWeb) ? 320 : 'device-width'
            ].join('='),
            'initial-scale=1.0',
            'minimum-scale=1.0',
            'target-densitydpi=medium-dpi'
          ].join(',')
        }),
        DOM_Title
      );
    } else
      DOM_Head.insertBefore(
        NewTag('meta', {
          name: 'MobileOptimized',
          content: 320
        }),
        DOM_Title
      );
    DOM_Head.insertBefore(
      NewTag('meta', {
        name: 'HandheldFriendly',
        content: 'true'
      }),
      DOM_Title
    );
  }
  if (IE_Ver)
    DOM_Head.appendChild(
      NewTag('meta', {
        'http-equiv': 'X-UA-Compatible',
        content: 'IE=Edge, Chrome=1'
      })
    );

  // ----------- Inner Logic Module ----------- //
  function xImport(JS_URL, CB_Func) {
    var SE = NewTag('script', {
      type: 'text/javascript',
      charset: 'UTF-8',
      src: JS_URL
    });
    if (CB_Func) DOM_Loaded_Event(SE, CB_Func);
    return $_TN(DOM, 'head')[0].appendChild(SE);
  }
  function LI_Add(LQ, RP, FN) {
    if (!FN.match(/^http(s)?:\/\//)) FN = RP + FN;
    LQ.push({ URL: FN });
  }
  function SL_Set(RP, List0) {
    for (var i = 0; i < List0.length; i++)
      if (!(List0[i] instanceof Array)) List0[i] = [List0[i]];
    var List1 = [];
    for (var i = 0, k = 0; i < List0.length; i++) {
      List1[k] = [];
      for (var j = 0, _URL; j < List0[i].length; j++) {
        if (typeof List0[i][j] == 'string') LI_Add(List1[k], RP, List0[i][j]);
        else {
          var _Rule = {
              old_PC: old_IE,
              Mobile: is_Mobile,
              Phone: is_Phone,
              Pad: is_Pad
            },
            no_Break = true;
          for (RI in _Rule)
            if (_Rule[RI]) {
              if (List0[i][j][RI] === false) no_Break = false;
              else if (List0[i][j][RI]) LI_Add(List1[k], RP, List0[i][j][RI]);
              break;
            }
        }
        if (no_Break && !List1[k][j] && List0[i][j].new_PC)
          LI_Add(List1[k], RP, List0[i][j].new_PC);
      }
      if (List1[k].length) k++;
    }
    return List1;
  }
  function CB_Set(List, Index) {
    var Item = List[Index + 1];
    if (Item[0].CallBack) {
      var AJSQ = EIJS.AsyncJS;
      var AJS_NO = AJSQ.push(0) - 1;
      var AJS_CB = function() {
        if (++AJSQ[AJS_NO] == Item.length) Item[0].CallBack();
      };
    }
    return function TF_Import() {
      for (var n = 0; n < Item.length; n++) xImport(Item[n].URL, AJS_CB);
    };
  }
  function CB_Chain(JS_RC, Final_CB) {
    var DRQ = EIJS.DOM_Ready;
    var DRQ_NO = DRQ.push(0) - 1;
    for (var k = JS_RC.length - 2, l = 0; k > -2; k--, l++) {
      if (!l) {
        if (Final_CB) {
          JS_RC[k + 1][0]['CallBack'] = function(iEvent) {
            if (++DRQ[DRQ_NO] == 2) Final_CB.apply(self);
          };
          DOM_Ready_Event(JS_RC[k + 1][0].CallBack);
        } else if (k > -1) {
          var Last_Script = JS_RC[k + 1][0].URL;
          JS_RC[k][0]['CallBack'] = function(iEvent) {
            if (++DRQ[DRQ_NO] == 2) xImport(Last_Script);
          };
          DOM_Ready_Event(JS_RC[k][0].CallBack);
          continue;
        }
      }
      if (k > -1) JS_RC[k][0]['CallBack'] = CB_Set(JS_RC, k);
    }
  }

  // ----------- Open API ----------- //
  BOM.ImportJS = function() {
    var Func_Args = Array.prototype.slice.call(arguments, 0);

    if (typeof Func_Args[0] == 'string') RootPath = Func_Args.shift();
    else
      RootPath = $_TN(DOM, 'script')[0].src.replace(
        /[^\/]*EasyImport[^\/]*\.js[^\/]*$/i,
        ''
      );
    if (Func_Args[0] instanceof Array) JS_List = Func_Args.shift();
    else throw "Format of Importing List isn't currect !";
    if (typeof Func_Args[0] == 'function') CallBack = Func_Args.shift();
    else CallBack = null;

    var JS_Item = SL_Set(RootPath, JS_List);
    if (!JS_Item[0].length) return;
    CB_Chain(JS_Item, CallBack);
    xImport(JS_Item[0][0].URL, JS_Item[0][0].CallBack);
  };

  BOM.ImportJS.UA = {
    IE: !!old_IE,
    Modern: !old_IE,
    Mobile: !!is_Mobile,
    Pad: !!is_Pad,
    Phone: !!is_Phone
  };
})(window, document);

【平台支持】

  • 主流内核浏览器:IE 7~11、Firefox 3+、Chrome、Safari、Opera
  • 国产马甲浏览器:傲游 2+、枫树、搜狗高速 3+、360 安全/极速、猎豹、QQ 等(还可能有个别奇葩 Bug,恕无力一一测试……)
  • Android、iOS、Windows Phone、Symbian 等智能设备平台上的自带浏览器及其内核控件

【主要特性】

  1. 替代 <script /> 加载 HTML 页面中所有外置、内置 JavaScript 脚本,但不改变原有“顺序即依赖”的前端编程习惯,且书写形式比原来更简洁
  2. 所有外置脚本都可写在 <head /> 中,但却能与整个页面的其它部分并行加载代码结构清晰而又高性能
  3. 最后一段脚本(无论内置回调、外置脚本)自动在 DOM Ready 后加载/执行,既靠谱又方便($(document).ready(function () {}); 之类的大包装可以完全不需要了)
  4. 无依赖的部分外置脚本只需用 [](数组字面量)括起来就可以异步加载
  5. 外置脚本可以根据浏览器平台类型选择性加载,模块化管理 JavaScript
  6. 移动设备浏览器布局模式自适配:添加其适用的各种 <meta /> 标签,让网页源码以标准而通用的简洁思维编写,自然地实施响应式设计
  7. 浏览器级的 <iframe /> 套用防御

【典型案例】

http://mag.fyscu.com/iWB/iBookView.php?name=iFY&index=19

以下示例代码摘抄自上述网页的 HTML 源码 ——

<head>
  ......
  <script type="text/javascript" src="./Libs/EasyImport.js"></script>
  <script>
    ImportJS(
      [
        {
          old_PC: 'jQuery.js',
          new_PC: 'jQuery2.js'
        },
        [
          {
            old_PC: 'Turn.HTML4.js',
            new_PC: 'Turn.js'
          },
          'Smooth_Scroll.js',
          'jPlayer.js',
          'jQuery.Hammer.js',
          {
            old_PC: false,
            new_PC: 'jQuery.PageZoomer.js',
            Mobile: false
          },
          {
            new_PC: 'Hover_Scroll.js',
            Mobile: false
          }
        ],
        'FY_iWeBook.js'
      ],
      function() {
        $('#iWB').iWeBook('#jPlayer_Box');
      }
    );

    var duoshuoQuery = { short_name: 'fyscu' };
    ImportJS(['http://static.duoshuo.com/embed.js']);
  </script>
  ......
</head>

【参考文章】

  1. http://sunnylost.com/article/js.load.scripts.sequence.html
  2. http://bbs.fyscu.com/forum.php?mod=viewthread&tid=4808
  3. http://www.cnblogs.com/pigtail/archive/2013/03/15/2961631.html
  4. http://www.uiej.com/960.html

上一篇
【原创】Web 前端脚本文件响应式异步加载器(框架页兼容) 【原创】Web 前端脚本文件响应式异步加载器(框架页兼容)
我之前开源了【JavaScript 文件响应式异步加载器】v0.6,用于在 Web 前端统一管理单页开发中的 JS 模块加载,方便、优雅、可靠。 但若要用框架页的模式来开发较为复杂的 Web 应用,统一管理父子页面的 JS、CSS 又是一件
2014-10-28
下一篇
【网页重构】CSS 规则清理(浏览器 BookMarkLet、FireBug 插件) 【网页重构】CSS 规则清理(浏览器 BookMarkLet、FireBug 插件)
很多程序猿认为 —— 最蛋疼的工作不在于重写或创新,而是“修改前任程序猿留下的代码”…… 对前端工程师来说,若不进行彻底的网页重构,而只是修补现有网页的代码(多是之前由 后端工程师 乃至 只会点儿 DreamWeaver 的运维网管 写的)
2014-09-30