《嘗試通過AngularJS模組按需載入搭建大型應用(上)》 說到目前angular應用的通用構建方式,一種是全量預載入,將所有可能用到的模組在使用者首次訪問時載入;第二種是按需載入業務邏輯,根據路由載入對應的controller和view,兩種處理方式互換優缺點。對比後得出最理想的方式應該是“模組按需載入”,即按需預載入業務功模組程式碼。本文具體聊聊如何實現。
模組劃分
上文說過了模組的載入方式,再來看看常見的模組劃分依據,基本可以分為兩類。
按元件劃分
angular中有很多元件(service、controller、filter等等。angular元件可以理解是一段js程式碼,一段帶有樣式的html,或者兩者兼而有之),其中一種劃分就是把不同類別的元件劃分到一個模組。
1 2 3 4 5 6 7 8 9 |
//主模組 angular.module('app', []); //模組下所有的service angular.module('app.service', []); ... //模組下所有的controller angular.module('app.controller', []); |
當然也有更簡單更粗暴的劃分方式,因為controller包含了業務邏輯,基本無複用可能性,而其他angular元件基本上可以被複用,統統放到一個模組,即變成:
1 2 3 4 5 6 |
//主模組 angular.module('app', []); //service、filter、directive等 angular.module('app.library', []); //controller angular.module('app.controller', []); |
本質上都一樣,都是按照元件劃分。這種劃分其實實際意義並不大,只是看起來顯得解構略微清晰一點,可移植性略微提升了一丟丟,因為把包含業務邏輯的controller分離了出來。
按業務劃分
這是比較推薦的一種劃分方法。示例如下:
1 2 3 4 5 6 7 |
//主模組 angular.module('app', []); //登陸註冊 angular.module('login', []); //首頁 angular.module('home', []); ... |
好處就是A模組的需要引用B模組元件的時候,只需要引入B模組即可,可移植性高。
同時從理論上來說各業務模組的耦合性可以大大降低。本文所提的就是這種劃分模式。
那是不是我們把不同模組放到不同的js檔案,然後按需引用就可以了呢?
是也不是,目標是要達到這個效果,但是有個棘手的問題——路由。
上文提到的兩種方式都有一個前提,就是 載入其它模組之前路由已經配置完成。
就以示例來說,如果我們在app模組配置login模組的路由,就會報錯找不到controller。原因很簡單,controller都在login模組下。那麼是不是先把login模組引入就行了呢?恭喜你,進入了上文說的第一種全量預載入方式。
所以最佳的方式是 每個模組單獨管理自己模組的路由,而不是由一個模組管理。
如果路由跟著業務模組進行配置,那麼問題來了:
- app模組沒有了路由怎麼載入檢視?
- 各個模組之間怎麼實現“頁面跳轉”?
帶著問題接著往下看~
按需載入
先簡單介紹一下懶載入檔案的依賴模組。上文說過第三方外掛和框架的相容性,依賴載入還是考慮比較成熟的第三方angular模組oc.lazyLoad。主要作用就是按照模組名懶載入該模組依賴的檔案,包括js和其它檔案。
可以首先在主模組app中配置模組資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
angular.module('app', ['oc.lazyLoad']).config(function('$ocLazyLoadProvider'){ //配置了兩個模組 // ngAnimate是第三方模組,依賴angular-animate.min.js // home是業務模組, 依賴home.js、home.css、home-html.js檔案 $ocLazyLoadProvider.config({ modules: [{ name: 'ngAnimate', files: ['lib/angular-animate/angular-animate.min.js'] },{ name: 'home', files: ['home/script/home.js', 'home/style/home.css', 'home/view/home-html.js'] }] }); }); |
當然這只是為了舉例,真正寫程式碼的時候不會這麼寫,配置資料肯定要分離出來,不要和業務邏輯混在一起。
要載入模組的時候也非常簡單,利用$ocLazyLoad
服務的load
函式。
1 2 3 4 |
//返回一個promise物件 $ocLazyLoad.load(moduleName).then(function() { //載入完成之後 }); |
路由管理
因為我們將路由都封裝在各個業務模組之中,但是頁面首次載入的時候只有app模組,所以這裡我們需要引用ui.router處理一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 在$rootScope上監聽'$locationChangeSuccess'事件 // 當使用者在瀏覽器中輸入URL地址時觸發 $rootScope.$on('$locationChangeSuccess', function () { if(!$location.path()) $location.path('/'); var mod = $location.path().split('/')[1]||'home'; // 路由路徑按照 "模組/頁面" 的方式配置,有兩個好處: // 1. 避免不同模組的路徑衝突 // 2. 可以通過路徑判斷模組 $ocLazyLoad.load(mod).then(function () { $urlRouter.sync(); }); }); $urlRouter.listen(); |
當然,直接這麼寫不會生效,我們需要阻塞ui.router預設的監聽事件,在app模組的config中呼叫一個函式
1 |
$urlRouterProvider.deferIntercept(); |
這麼一來當使用者直接輸入地址或者從收藏夾訪問頁面的時候可以先載入業務模組,然後由業務模組的路由來處理頁面載入。這樣第一個問題就解決了。還有內部跳轉的情況。
通常ui.router用得最多的跳轉方式有兩種,一種是指令ui-sref
,另一種是函式$state.go
。雖然前者寫在檢視上後者寫在controller中,但研究原始碼後發現都是呼叫了$state.transitionTo
。
現在想要從home模組直接跳到login模組是會報錯的,因為login模組沒有載入找不到對應的路由。那麼需要做的就是在路由跳轉之前載入對應的模組,這裡需要對$state.transitionTo
稍微改造一下。
不得不用到angular中的黑科技decorator
。decorator
在常用開發中用得很少,它最大的作用就是修改第三方模組的服務,而且是在 不修改原始碼 的情況下。直接看程式碼和註釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//處理模組間跳轉 $provide.decorator('$state', function($delegate, $ocLazyLoad) { var state = {}; // angular物件還是有些實用的方法的,深拷貝物件算是一個 // 這裡做深拷貝不做淺拷貝是避免迴圈巢狀呼叫記憶體溢位 angular.copy($delegate, state); $delegate.transitionTo = function (to) { // 跳轉的時候有兩種情況,一種是傳入self物件,另一種是直接把state的id傳進來 if (to.self) { // 當to為物件時,讀取self.url屬性獲取路徑,因為路徑命名遵循"模組/頁面"的方式,所以可以輕鬆判讀取模組名 var mod = to.self.url.replace('main.', '').replace(/\/(.*)\/.*/, '$1'); if (!mod || '/' === mod) { mod = 'home'; } //模組載入完成後再呼叫預設的路由跳轉函式 $ocLazyLoad.load(mod).then(function (){ state.transitionTo.apply(null, arguments); }); } else { var id = to.replace('main.', '').replace(/(([a-z]*)[A-Z]{1})?.*/, '$2'); $ocLazyLoad.load(mnModule[id].name).then(function() {state.transitionTo.apply(null, arguments);}); } } return $delegate; }); |
總結
按照上述方式組織程式碼之後,業務模組一般會包含3個合併後的檔案,一個js業務邏輯,一個css樣式,一個模板檔案。模組間相互引用,路由跳轉傳參,巢狀路由,多重路由等常用功能都可以正常使用。
框架程式碼只是一部分,還需要構建工具配合~
原始碼目錄結構大致如下:
編譯後的程式碼:
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式