初次使用angularjs做項目,但是發(fā)現(xiàn)angularjs在路由配置后會一次性加載所有依賴文件,這對于大一點的項目來說是不可接受的,使用requirejs也不能阻止路由配置處angularjs自己去加載文件。
然后我找到了angularAMD,在angularjs和angular-ui-router的環(huán)境下是可以實現(xiàn)template、controller的動態(tài)加載(也就是進入哪個頁面就加載改頁面相關(guān)內(nèi)容)。
但是由于不想自己折騰太多的UI,我使用了很火的框架ionic,他對路由模塊可能坐了自己的封裝,使用了<ion-nav-view>而不是<ui-view>,然后導(dǎo)致控制器能按需加載,模板卻一次性加載。
我谷歌了這些內(nèi)容,發(fā)現(xiàn)相關(guān)話題非常少,難道是我理解有誤?angular不需要動態(tài)加載模塊嗎,但是對于有很多個頁面的項目,這肯定是不合理的,譬如我們要做的是hybird app,頁面量很大。不知道大家在使用angular時都是怎么對待這個需求的
占位 謝
@張治中邀請
樓上的幾個答案我都看了一下
1、把單頁面應(yīng)用做成應(yīng)用中所有頁面都加載并且重新初始化Angularjs框架的行為在我個人的角度是無法接受的,不論是從交互體驗的角度上還是從技術(shù)追求的角度。當(dāng)然,如果從公司的角度,加上其他因素的影響,最后根據(jù)各方面實際情況(開發(fā)周期、團隊技術(shù)儲備、產(chǎn)品經(jīng)理交互需求等等)的妥協(xié)與折衷,這樣做其實也是可以的。
2、使用oclazyload的方案我調(diào)研了一下,是可行的,但是想說這個方案有些缺點,比如每次動態(tài)加載需要的腳本、模版資源會有很多不必要的網(wǎng)絡(luò)開銷,路由定義比較復(fù)雜(多了一些配置項,其實不能算復(fù)雜,而是繁瑣),對于大型復(fù)雜業(yè)務(wù)應(yīng)用,路由眾多,耗費的精力不可忽視。在實際做對外開放的產(chǎn)品時,我們一般會把使用requirejs管理依賴關(guān)系的代碼打包壓縮,加版本號,同時根據(jù)項目情況決定要不要按照業(yè)務(wù)模塊做拆分打包&異步按需加載。
3、不知道樓主開發(fā)的產(chǎn)品是不是移動端的單頁面應(yīng)用,不知道樓主的應(yīng)用是否業(yè)務(wù)復(fù)雜以及腳本文件的大小在什么量級,所以我下面講解的技術(shù)方案可能在某些地方并不適合樓主的場景,但是原理是相通的,樓主可以參考一下然后看看是否可以解決你的項目中面臨的問題。如果需要,可以隨時找我一起討論你所面臨的問題以及采用哪種方式解決最好。
4、占位,晚上有空更新。
===========2015-06-08更新============
最近一直比較忙,今天抽空更新一部分。
看了樓主的評論回復(fù),移動端和PC瀏覽器端,對于Angular本身而言沒有區(qū)別,所以我說的這些應(yīng)該也適用于移動端Angularjs應(yīng)用。
首先說一下Angularjs的啟動原理,就知道為什么很少有人做Angular代碼的異步加載了。
好像知乎不支持markdown格式,寫起來好難受,只能拿 “等號”玩了
========================Angular框架啟動原理分析==============================
我們現(xiàn)在一般配合Requirejs 做代碼依賴管理的情況下,都是在RequireJs的入口文件中,執(zhí)行以下代碼來啟動Angular框架:
angular.element(document).ready(function() { angular.bootstrap(document, ['vpcConsole']); });
這種情況下,我們來扒代碼看看 angular.bootstrap都干了什么(無關(guān)緊要的代碼用省略號表示了):
function bootstrap(element, modules) { var doBootstrap = function() { //獲取Jquery的Dom對象 element = jqLite(element); …… //這句話是最關(guān)鍵的點,我們會繼續(xù)深挖createInjector這個函數(shù) var injector = createInjector(modules); injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', function(scope, element, compile, injector, animate) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }] ); return injector; }; //以下代碼應(yīng)該是為了支持測試用的,不用關(guān)心,感興趣可以看文檔說明:http://docs.angularjs.cn/guide/bootstrap var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); angular.resumeBootstrap = function(extraModules) { forEach(extraModules, function(module) { modules.push(module); }); doBootstrap(); };}
在上面的bootstrap方法里面,我們找到了一行非常關(guān)鍵的代碼,調(diào)用了createInjector方法,接下來我們看下createInjector方法里面干了什么事情(不重要的代碼統(tǒng)統(tǒng)省略號)。
function createInjector(modulesToLoad) { /* 開始部分申明了一堆變量,無視掉 */ ……//重要的代碼,loadModules方法需要深挖 forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); return instanceInjector;/*源碼里面這里寫了一堆函數(shù)定義,由于function 聲明會在js解析引擎里面被提前,所以前面有return語句也沒關(guān)系*/……}
然后再來看 loadModules里面的關(guān)鍵語句(loadModules函數(shù)其實就定義在了 createInjector函數(shù)里面)
function loadModules(modulesToLoad){ var runBlocks = [], moduleFn, invokeQueue, i, ii; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); try { if (isString(module)) { //得到angular模塊 moduleFn = angularModule(module); runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); //這里使用for循環(huán)讀取了modelFn的_invokeQueue,然后做遍歷執(zhí)行,這個_invokeQueue 是很關(guān)鍵的東西,等你知道了它的由來,就知道為什么Angularjs天然不支持異步加載了。 for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { var invokeArgs = invokeQueue[i], provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { runBlocks.push(providerInjector.invoke(module)); } else { assertArgFn(module, 'module'); } } catch (e) { /* 異常處理,不關(guān)心 */ …… } }); return runBlocks; }
上面的代碼中已經(jīng)發(fā)現(xiàn)_invokeQueue 是個很重要的東西,那么我們來看下它在什么地方生成的(這個地方代碼比較長,但是非常非常關(guān)鍵,無用代碼直接省略號)。
function setupModuleLoader(window) { …… function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } //給window對象添加一個 angular 屬性 var angular = ensure(window, 'angular', Object); …… return ensure(angular, 'module', function() { var modules = {}; return function module(name, requires, configFn) { …… return ensure(modules, name, function() { //這個就是我們上面提到的_invokeQueue; var invokeQueue = []; /** @type {!Array.<Function>} */ var runBlocks = []; var config = invokeLater('$injector', 'invoke'); //這個就是我們調(diào)用 angular.module()以后得到的實例,注意它對外暴露的接口都是用什么實現(xiàn)的(invokeLater做的實現(xiàn)) var moduleInstance = { //這個就是很重要的module的一個屬性,_invokeQueue,前面的代碼很清楚,它是一個數(shù)組,而且這個數(shù)組里面的東西,只有在angular.bootstrap的時候才會被執(zhí)行,明白這一點非常關(guān)鍵非常重要。 _invokeQueue: invokeQueue, _runBlocks: runBlocks, requires: requires, name: name, /* 下面這些就是我們經(jīng)常調(diào)用的各種接口,注意它的實現(xiàn),都是使用invokeLater來做了一次封裝,下面的代碼注釋里面說明了invokeLater的作用。 */ provider: invokeLater('$provide', 'provider'), factory: invokeLater('$provide', 'factory'), service: invokeLater('$provide', 'service'), value: invokeLater('$provide', 'value'), constant: invokeLater('$provide', 'constant', 'unshift'), animation: invokeLater('$animateProvider', 'register'), filter: invokeLater('$filterProvider', 'register'), controller: invokeLater('$controllerProvider', 'register'), directive: invokeLater('$compileProvider', 'directive'), config: config, run: function(block) { runBlocks.push(block); return this; } }; if (configFn) { config(configFn); } return moduleInstance; /*這里是InvokeLater的實現(xiàn),我們調(diào)用 Angular的module實例所注冊的factory、filter、controller、directive 等等都是通過這個方法放在了 invokeQueue里面,也就是說,當(dāng)我們在代碼里面執(zhí)行 xxxModule.directive('mydirective',['xxx',function(xxx){}])的時候,其實這個directive并沒有真正被注冊,而是放在了一個invokeQueue里面,知道這一點很重要。同理,我們定義一個controller的時候,也沒有真正注冊執(zhí)行這個controller,而是這個controller被放在了一個數(shù)組里面,等著angular.bootstrap真正去執(zhí)行并實例化這個函數(shù) */ function invokeLater(provider, method, insertMethod) { return function() { invokeQueue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } }); }; });}
代碼扒到這里就差不多了。如果你看明白了整個angular.bootstrap的時候的來龍去脈,就會發(fā)現(xiàn),假設(shè)我當(dāng)前頁面已經(jīng)加載了相關(guān)資源,Angular框架已經(jīng)運行起來(執(zhí)行了angualr.bootstrap方法),那么我后續(xù)通過按需加載引入的javascript腳本文件中的那些
xxxModule.controller("xxx",['yyy',function(yyy){}])、
xxxModule.directive("zzz",['www',function(www){}])
代碼都雖然會在requirejs引入的時候被執(zhí)行一遍,但是執(zhí)行的結(jié)果僅僅是把這些controller和directive和factory等等函數(shù)放在了一個invokeLater的數(shù)組里面,我們的前端路由激活的時候,去通過angular尋找對應(yīng)路由(視圖)的controller的時候,發(fā)現(xiàn)根本沒有這個controller,原因就是在這里:Angular框架啟動以后(執(zhí)行了bootstrap方法之后),它讀取controller構(gòu)造函數(shù)、directive構(gòu)造函數(shù)等的地方和我們執(zhí)行 controller方法、directive方法所注冊進去的代碼不是一個地方(或者說不是緩存在一個變量里面),所以,在按需加載的情況下,雖然我們的代碼看起來執(zhí)行了,但是真正Angular的部分卻并沒有真正的執(zhí)行,而是僅僅被放在一個地方等待著“invokeLater”(其實再等多久也沒用,因為不能再執(zhí)行angular.bootstrap方法了)。
=======================異步按需加載方案分析======================
AngularJs框架啟動原理分析完以后,就可以分析異步按需加載方案了。首先我們來看一下我們做異步按需加載方案需要解決哪些問題:
1、angularjs框架啟動后,調(diào)用 各種api無法真正注冊相關(guān)構(gòu)造函數(shù)的問題
2、路由激活時,加載當(dāng)前路由需要的腳本資源(考慮防止重復(fù)加載、考慮最大化利用客戶端緩存等等問題)
3、當(dāng)前單頁面應(yīng)用模塊劃分和打包(當(dāng)上面兩個技術(shù)問題解決以后,就要考慮這個偏向業(yè)務(wù)的問題了)
我們先來看第一個問題怎么解決:
1、解決異步按需加載代碼后Angular原生代碼無法真正注冊各種構(gòu)造函數(shù)的問題
經(jīng)過上面的加載原理分析以后,我們就知道該怎么辦了:把angular.module("xxx")的實例的factory、controller、directive、value、filter 等等方法都“變換”掉,讓我們的代碼執(zhí)行這些方法的時候,直接把我們的函數(shù)放在運行期的對應(yīng)的緩存的對象里面,這樣一來異步加載的代碼就會在執(zhí)行的時候真正被注冊到Angular運行時可以讀取的地方(Angular運行時具體緩存各個構(gòu)造函數(shù)的地方自己扒源碼吧,懶得貼出來了),這樣在路由激活的時候,就可以找到對應(yīng)的controller,然后執(zhí)行。
這個辦法可以參考下面的代碼(可以參考前面同學(xué)回答的oclazyload這個框架里面的代碼,但是這個框架作了很多其他的事情,導(dǎo)致最核心的思想沒有很清晰的體現(xiàn),不過也可以讀一讀,挺有意思):
(function(){ var app = angular.module('app', []); app.config(function($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) { app.controllerProvider = $controllerProvider; app.compileProvider = $compileProvider; app.routeProvider = $routeProvider; app.filterProvider = $filterProvider; app.provide = $provide; // Register routes with the $routeProvider });})();
解決這個問題以后,我們來看第2個按路由加載代碼的問題如何解決:(精力有限,暫時更新到這里,預(yù)計本周還會更新一次,先給個提示:按照路由去加載所需的代碼方案有很多種,常見的是就在定義路由的時候定義一個 resolve,在里面做資源的加載,但是這種方法需要在路由定義的敵方寫比較多的東西,不是很喜歡這種方式。題主你的手機端應(yīng)用建議你做好合理的模塊或者說頁面的劃分,我初步的建議是你每個頁面內(nèi)部的資源全部都打包成一個腳本包括視圖模板也打包進去,最終看文件的大小是不是在移動端單次web請求允許的范圍之內(nèi),然后angularjs相關(guān)的和公共代碼打包成一個,應(yīng)用啟動時加載這個公共的腳本,切換到其他頁面或者路由的時候公共的資源腳本已經(jīng)運行起來,不需要再加載,而只需加載對應(yīng)頁面或者路由的腳本就可以了。初步可以這樣,后續(xù)再詳細(xì)更新說明具體的方案和原因。)
===========2015-06-21更新============
抱歉最近非常忙,更新時間略長
之前的內(nèi)容講解了Angularjs支持動態(tài)加載腳本相關(guān)的局限性(動態(tài)引入到瀏覽器里面的代碼中,沒有經(jīng)過處理的話,所有的 xxxModule.directive()、xxxModule.controller() 執(zhí)行過后并沒有真正立刻被實例化,而是放在了一個緩存中等待app bootstrap的時候去執(zhí)行,可是由于這些代碼是動態(tài)引入的,在它們引入之前,app 已經(jīng)bootstrap過了。),現(xiàn)在開始講如何配合 requirejs 做相關(guān)的處理來支持動態(tài)加載:
先看下有幾個問題需要解決:
1、確定按需加載的靜態(tài)資源引入頁面的時機。
2、按需加載的靜態(tài)資源打包方式。
3、其他一些問題的整體考慮。
依次回答上面三個問題,加上之前更新的內(nèi)容,然后關(guān)于AngularJs動態(tài)加載腳本的問題和相關(guān)的原理基本上可以說明白了。
1: 毫無疑問,在用戶瀏覽某個“頁面”的時候,應(yīng)用會激活對應(yīng)的路由,然后去加載對應(yīng)的資源腳本。在我們實際開發(fā)過程中,一個業(yè)務(wù)模塊下面會有n個相關(guān)的子頁面來共同服務(wù)于某個業(yè)務(wù)需求,這種情況下,我們會給這n個子頁面定義一個最基本的父路由,在進入該父路由的時候,去判斷當(dāng)前路由相關(guān)的資源腳本是否加載,如果沒有加載的話,則利用requirejs去 獲取資源腳本文件,等拿到并執(zhí)行以后,再執(zhí)行相關(guān)的業(yè)務(wù)邏輯(比如激活某個頁面的路由、實例化對應(yīng)的controller函數(shù));如果已經(jīng)加載過了,則和正常的路由解析過程一致,直接依次執(zhí)行其子路由直到用戶想要訪問的頁面對應(yīng)的路由激活。所以說,在不引入其他第三方庫的情況下(或者已經(jīng)引入了某個庫,不再適合引入其他庫的情況下),我們可能需要在路由定義的地方做手腳,加入自己的靜態(tài)資源加載服務(wù)。
2:由于每個業(yè)務(wù)模塊有n個頁面,所以一定有大量的其他代碼一起配合完成相關(guān)的業(yè)務(wù)邏輯,比如有很多directive,有很多filter,這些腳本有的是我們單頁面應(yīng)用里面其他業(yè)務(wù)模塊用不到的,有的是其他業(yè)務(wù)模塊可以共用的,所以我們在開發(fā)過程中,除了以業(yè)務(wù)模塊為維度統(tǒng)一劃分開發(fā)文件結(jié)構(gòu)以外,還會多出一個 common 或者 base 這樣類型的文件夾,專門負(fù)責(zé)放置應(yīng)用內(nèi)部各個業(yè)務(wù)模塊都使用的公共代碼或者公共組件等等。這種情況下,我們的項目中,app文件夾下面的一級文件夾就是 common + n 個業(yè)務(wù)模塊文件夾。在這種項目文件結(jié)構(gòu)下,我們會把 common 和所有用到的第三方組件資源腳本打包、壓縮在一個文件中,在用戶打開瀏覽器加載整個單頁面應(yīng)用的時候最先加載并執(zhí)行,其他n個業(yè)務(wù)模塊,每一個文件夾下的腳本資源單獨打包壓縮成一個js文件,每個業(yè)務(wù)模塊文件夾下需要一個種子文件去引用依賴當(dāng)前項目文件夾中的其他資源腳本,這樣在使用r.js做打包的時候,就可以以這個文件為種子文件將其依賴的腳本打包到一個文件里面。在打包業(yè)務(wù)模塊的腳本的時候,一定會有某些腳本引用了 common 里的文件或者第三方組件中的文件,所以需要在打包配置項中,做一些配置,告訴打包工具不把聲明的這些文件打包到當(dāng)前文件中。具體的配置項可以參考 r.js 中的 path配置項,其中某個子項如果使用 xxx:“empty:” 這樣的寫法的話,則該聲明意為不把 xxx 打包到當(dāng)前打包的文件中??梢詤⒖紃.js的示例配置文件:
https://github.com/jrburke/r.js/blob/master/build/example.build.js#L39這樣,打包部分的方式和方法也說明了,具體相關(guān)配置不太熟悉的同學(xué)可以看requirejs的官方文檔或者直接看 r.js 的github項目
r.js/example.build.js at master · jrburke/r.js · GitHub大部分同學(xué)應(yīng)該是使用 grunt-requirejs做打包的,其實和r.js是一樣的,所以看上面的鏈接地址的代碼和注釋就可以了。
3:在做這樣的技術(shù)改造或者技術(shù)升級的時候,需要考慮很多很多因素,而不是僅僅去找?guī)讉€相同的例子然后把自己的代碼改成那樣就好了,而是需要考慮已有代碼做升級的時候,如何才能改動最小,對已有代碼侵入最少,還要考慮到對開發(fā)人員的友好性,考慮到測試和線上調(diào)試的方便程度,還要考慮到開發(fā)團隊內(nèi)部其他成員的接受程度等等。以題主目前的處境來看,其實你的業(yè)務(wù)代碼的按需加載已經(jīng)由你采用的框架幫你實現(xiàn),現(xiàn)在你的困惑主要集中在模板部分不能按需加載,所以,我們可以把目光直接聚焦到如何解決 模板不能按需加載的問題上,而不用再去考慮如何做業(yè)務(wù)腳本的按需加載的整體實現(xiàn)。所以,針對題主你的問題,我建議如下(假設(shè)你的HTML模板都已經(jīng)打包到了一個文件中):1、使用 grunt-html2js 打包轉(zhuǎn)換HTML模板為js文件 2、打包后的模板緩存文件和業(yè)務(wù)模塊代碼打包到一起 3、業(yè)務(wù)腳本動態(tài)引入的時候,自動引入HTML模板的js文件,這樣就實現(xiàn)了HTML模板的動態(tài)引入。原理:Angularjs 的指令、State 等等在處理 templateUrl 的時候,會去服務(wù)端按照templateUrl的地址發(fā)起請求,但是在請求真正發(fā)起之前會使用這個Url作為key去 $templateCache 中查找有沒有該key對應(yīng)的緩存,如果有的話,則不會真的發(fā)起請求,而是直接采用緩存中的HTML字符串。所以我們的 grunt-html2js會生成js文件,這個文件里面,每個HTML模板都會以某個Url(這個Url取決于自己的配置)作為自己的key,然后把模板內(nèi)容轉(zhuǎn)義為字符串,存入$templateCache中。所以,題主,你可以按照這個思路重新調(diào)整一下模板相關(guān)的部分,多做debug,觀察每個視圖激活的過程,看下templateUrl的處理,就知道該怎么做了。
http://www.zhihu.com/question/30624377