基於ui-router的非侵入式angular按需加載方案

原文:https://github.com/kuitos/kuitos.github.io/issues/31
所有文章:https://github.com/kuitos/kuitos.github.io/issues/html

用過angular1.x(後面提到的angular均指代的angular1.x框架)的同窗應該都知道,angular自身的模塊系統是不具有按需加載的能力的,筆者也贊同angular的模塊系統是真正稱得上設計上的敗筆的觀點的。2015年被黑的最慘的前端主流框架莫過於angular了,但實際上angular真正設計上的硬傷只有兩個:雞肋的模塊系統以及相比其餘MVVM框架略顯醜陋的髒值檢測機制。關於其餘各類所謂致命缺陷的立論其實都是站不住腳的,這些觀點的提出我能夠歸結於使用者對angular的不熟悉,不服的同窗歡迎來辯?前端

angular模塊系統的問題

扯遠了,說回正題。因爲angular自身模塊系統的限制,module不支持運行時添加依賴,也就是咱們在定義入口模塊時必須聲明全部依賴項。當咱們面臨多項目整合的場景時(每每這類場景有按需加載的需求),這個就很噁心了,咱們總不能在入口頁寫好全部可能會嵌入系統的項目的依賴項吧,並且要確保入口模塊能找到全部依賴項對應的模塊,相應的js還必須在入口處就加載好。。 更多關於angular模塊化的問題,具體能夠參見民工叔的這篇文章Angular的模塊機制git

市面上angular實現按需加載的一般方案

目前市面上流行的解決方案大概是這樣的:基於requirejs等模塊加載器,咱們子模塊的代碼包裹在requirejs的模塊定義語法下(define),而後在具體須要的時候在require回調裏invoke咱們子模塊的controller或service等,能夠參見這個seed項目angular-requirejs-seedgithub

可是這種方式也有一些明顯的問題:ajax

  1. requirejs配合angular實現的那一套按需加載的方案實在是太挫了,真的是有礙觀瞻啊!?它是一套徹底侵入式的方式,我我的是沒法接受的。並且我認爲在中小型規模的系統中,基於angular框架,咱們本身須要寫的代碼量其實不會太大,即便在首頁所有引入,在通過簡單的合併壓縮再配合gzip,文件體積徹底在可控範圍內,按需加載在這樣的場景下價值有限。這也是我一直拒絕在angular體系中引入requirejs的緣由。gulp

  2. 若是咱們採用angular的純module的方式開發,那麼咱們天然會有包含各類controller、service、directive的不一樣模塊,相似angular.module('directives',[]).directive('grid',function(){})
    的寫法,而這些子模塊必須在入口模塊定義時聲明其爲依賴項,像這樣angular.module('app',['directives'])即使你採用requirejs作按需加載。瀏覽器

  3. 咱們不採用子單元純module的方式開發,而是將全部的子單元都掛載在入口模塊上,子模塊寫法相似angular.module('app').directive('grid',function(){}),這種作法反作用會相對少點,可是若是碰到多個項目在各個系統之間做嵌入時,很難作到不用修改代碼便可完成嵌入,除非你能確保全部的系統入口模塊命名同樣。安全

基於ui-router的解決方案

恰好最近公司在作整個系統的去iframe化(沒錯以前各個產品嵌入主系統的作法是經過iframe。。不要笑!!?),由於各個產品之間的切換是經過tab完成的,tab的切換又是經過ui-router控制去定位到各個產品的入口html,因此基於ui-router,個人思路是這樣的:app

  1. 首先要理清ui-router的工做方式:tab切換時觸發ui-router的路由,ui-router會經過配置好的路由規則找尋相應的模板配置(這裏假設咱們路由配置的都是templateUrl的方式),獲得url後會去發起ajax請求拿模板,拿到模板再會填充到ui-view內容區,最後作compile、link處理(省去其餘細節),這時候ui-view區域顯示的就是編譯好的模板內容了。框架

  2. 基於此,咱們能夠在模板作編譯以前,分析並拿到模板中的script標籤,而後經過簡單的腳步加載器將模板中定義的js加載到瀏覽器內存裏,在全部的js資源加載完畢以後再去調用編譯流程,一切OK!這裏要順帶解釋一個事情,由於ui-router裏採用element.html(tpl)的方式將模板填充到ui-view中的,因此模板中的script標籤並不會被瀏覽器按正常方式解析,而link、style標籤不會受到影響(出於安全考慮?具體緣由沒查到知道的同窗請不吝指教)。

可是咱們要作的固然不能是直接去找到ui-router這一塊的代碼而後修改源碼,這種作法是有違開閉原則的也是我一直批判的方式,不到萬不得已毫不要去修改第三方插件的源碼!ui-router處理路由模板的主邏輯在uiView指令裏,而後angular裏面又提供了強大的decorator機制。開碼!

angular
    .module('ui.router.requirePolyfill', ['ng', 'ui.router', 'oc.lazyLoad'])
    .decorator('uiViewDirective', DecoratorConstructor);

  /**
   * 裝飾uiView指令,給其加入按需加載的能力
   */
  DecoratorConstructor.$inject = ['$delegate', '$log', '$q', '$compile', '$controller', '$interpolate', '$state', '$ocLazyLoad'];
  function DecoratorConstructor($delegate, $log, $q, $compile, $controller, $interpolate, $state, $ocLazyLoad) {

    // 移除原始指令邏輯
    $delegate.pop();
    // 在原始ui-router的模版加載邏輯中加入腳本請求代碼,實現按需加載需求
    $delegate.push({

      restrict: 'ECA',
      priority: -400,
      compile : function (tElement) {
        var initial = tElement.html();
        return function (scope, $element, attrs) {

          var current = $state.$current,
            name = getUiViewName(scope, attrs, $element, $interpolate),
            locals = current && current.locals[name];

          if (!locals) {
            return;
          }

          $element.data('$uiView', {name: name, state: locals.$$state});

          var template = locals.$template ? locals.$template : initial,
            processResult = processTpl(template);

          var compileTemplate = function () {
            $element.html(processResult.tpl);

            var link = $compile($element.contents());

            if (locals.$$controller) {
              locals.$scope = scope;
              locals.$element = $element;
              var controller = $controller(locals.$$controller, locals);
              if (locals.$$controllerAs) {
                scope[locals.$$controllerAs] = controller;
              }
              $element.data('$ngControllerController', controller);
              $element.children().data('$ngControllerController', controller);
            }

            link(scope);
          };

          // 主要實現
          // 模版中不含腳本則直接編譯,不然在獲取完腳本以後再作編譯
          if (processResult.scripts.length) {
            loadScripts(processResult.scripts).then(compileTemplate);
          } else {
            compileTemplate();
          }

        };
      }

    });

    return $delegate;

最先期我本身實現了一個簡單的script-loader用來作基本的動態腳本加載,可是後來發現一個問題:angular框架下咱們單單的只是加載腳本是沒用的,咱們必須把腳本定義的module注入到主app的module下才有意義。儘管在下仔細讀過大部分angular的核心部件代碼,可是動態註冊模塊這個事情難度仍是很大的,改造工做一度停滯不前。。直到我發現了這個庫ocLazyLoad,這以後事情就好辦了。
附上完整的實現代碼:ui-router-require-polyfill文檔。這裏面爲了解決腳本加載的時序問題,我在loadScript方法里加入了提取script seq屬性的機制用於肯定腳本順序,同時爲了解決gulp腳本合併時的問題,我的簡單改造了下gulp-usemin插件,改造後的插件在這裏,要作發佈的腳本合併時請配合使用這個改造過的插件。

寫在最後

這一套方案目前是我能想到的最接近完美的方案,最主要的是它是非侵入式並且基本不須要對原有angular體系下的代碼作任何改造,便可實現按需加載&模塊移植的需求的方式。若是有同窗有改進建議或者更好的方案,歡迎一塊兒探討。

相關文章
相關標籤/搜索