原文: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自身模塊系統的限制,module不支持運行時添加依賴,也就是咱們在定義入口模塊時必須聲明全部依賴項。當咱們面臨多項目整合的場景時(每每這類場景有按需加載的需求),這個就很噁心了,咱們總不能在入口頁寫好全部可能會嵌入系統的項目的依賴項吧,並且要確保入口模塊能找到全部依賴項對應的模塊,相應的js還必須在入口處就加載好。。 更多關於angular模塊化的問題,具體能夠參見民工叔的這篇文章Angular的模塊機制git
目前市面上流行的解決方案大概是這樣的:基於requirejs等模塊加載器,咱們子模塊的代碼包裹在requirejs的模塊定義語法下(define),而後在具體須要的時候在require回調裏invoke咱們子模塊的controller或service等,能夠參見這個seed項目angular-requirejs-seedgithub
可是這種方式也有一些明顯的問題:ajax
requirejs配合angular實現的那一套按需加載的方案實在是太挫了,真的是有礙觀瞻啊!?它是一套徹底侵入式的方式,我我的是沒法接受的。並且我認爲在中小型規模的系統中,基於angular框架,咱們本身須要寫的代碼量其實不會太大,即便在首頁所有引入,在通過簡單的合併壓縮再配合gzip,文件體積徹底在可控範圍內,按需加載在這樣的場景下價值有限。這也是我一直拒絕在angular體系中引入requirejs的緣由。gulp
若是咱們採用angular的純module的方式開發,那麼咱們天然會有包含各類controller、service、directive的不一樣模塊,相似angular.module('directives',[]).directive('grid',function(){})
的寫法,而這些子模塊必須在入口模塊定義時聲明其爲依賴項,像這樣angular.module('app',['directives'])
即使你採用requirejs作按需加載。瀏覽器
咱們不採用子單元純module的方式開發,而是將全部的子單元都掛載在入口模塊上,子模塊寫法相似angular.module('app').directive('grid',function(){})
,這種作法反作用會相對少點,可是若是碰到多個項目在各個系統之間做嵌入時,很難作到不用修改代碼便可完成嵌入,除非你能確保全部的系統入口模塊命名同樣。安全
恰好最近公司在作整個系統的去iframe化(沒錯以前各個產品嵌入主系統的作法是經過iframe。。不要笑!!?),由於各個產品之間的切換是經過tab完成的,tab的切換又是經過ui-router控制去定位到各個產品的入口html,因此基於ui-router,個人思路是這樣的:app
首先要理清ui-router的工做方式:tab切換時觸發ui-router的路由,ui-router會經過配置好的路由規則找尋相應的模板配置(這裏假設咱們路由配置的都是templateUrl的方式),獲得url後會去發起ajax請求拿模板,拿到模板再會填充到ui-view內容區,最後作compile、link處理(省去其餘細節),這時候ui-view區域顯示的就是編譯好的模板內容了。框架
基於此,咱們能夠在模板作編譯以前,分析並拿到模板中的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體系下的代碼作任何改造,便可實現按需加載&模塊移植的需求的方式。若是有同窗有改進建議或者更好的方案,歡迎一塊兒探討。