在進行有一定規模的項目時,通常希望實現以下目標:1、支援複雜的頁面邏輯(根據商務規則動態展現內容,例如:許可權,資料狀態等);2、堅持前後端分離的基本原則(不分離的時候,可以在後端用模版引擎直接產生好頁面);3、頁面載入時間短(商務邏輯複雜就需要引用第三方的庫,但很可能載入的庫和使用者本次操作沒關係);4,還要代碼好維護(加入新的邏輯時,影響的檔案盡量少)。
想同時實現這些目標,就必須有一套按需載入的機制,頁面上展現的內容和所有需要依賴的檔案,都可以根據商務邏輯需要按需載入。最近都是基於angularjs做開發,所以本文主要圍繞angularjs提供的各種機制,探索全面實現按需載入的套路。
一、一步一步實現
基本思路:1、先開發一個架構頁面,它可以完成一些基本的商務邏輯,並且支援擴充的機制;2、商務邏輯變複雜,需要把部分邏輯拆分到子頁面中,子頁面按需載入;3、子頁面中的展現內容也變了複雜,又需要進行拆分,按需載入;4、子頁面的內容複雜到依賴外部模組,需要按需載入angular模組。
1、架構頁
提到前端的按需載入,就會想到AMD( Asynchronous Module Definition),現在用requirejs的非常多,所以首先考慮引入requires。
index.html
<script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js"></script>
注意:採用手動啟動angular的方式,因此html中沒有ng-app。
spa-loader.js
require.config({ paths: { "domReady": '/static/js/domReady', "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min", "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min", }, shim: { "angular": { exports: "angular" }, "angular-route": { deps: ["angular"] }, }, deps: ['/test/lazyspa/spa.js'], urlArgs: "bust=" + (new Date()).getTime()});
spa.js
define(["require", "angular", "angular-route"], function(require, angular) { var app = angular.module('app', ['ngRoute']); require(['domReady!'], function(document) { angular.bootstrap(document, ["app"]); /*手工啟動angular*/ window.loading.finish(); });});
2、按需載入子頁面
angular的routeProvider+ng-view已經提供完整的子頁面載入的方法,直接用。
注意必須設定html5Mode,否則url變化以後,routeProvider不截獲。
index.html
<div> <a href="/test/lazyspa/page1">page1</a> <a href="/test/lazyspa/page2">page2</a> <a href="/test/lazyspa/">main</a></div><div ng-view></div>
spa.js
app.config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) { /* 必須設定生效,否則下面的設定不生效 */ $locationProvider.html5Mode(true); /* 根據url的變化載入內容 */ $routeProvider.when('/test/lazyspa/page1', { template: '<div>page1</div>', }).when('/test/lazyspa/page2', { template: '<div>page2</div>', }).otherwise({ template: '<div>main</div>', });}]);
3、按需載入子頁面中的內容
用routeProvider的前提是url要發生變化,但是有的時候只是子頁面中的局部要發生變化。如果這些變化主要是和繫結資料相關,不影響頁面配置,或者影響很小,那麼通過ng-if一類的標籤基本就解決了。但是有的時候要根據頁面狀態,完全改變局部的內容,例如:使用者登入前和登入後局部要發生的變化等,這就意味著局部的布局可能也挺複雜,需要作為獨立的單元來對待。
利用ng-include可以解決頁面局部內容載入的問題。但是,我們可以再考慮更複雜一些的情況。這個頁面片段對應的代碼是後端動態產生的,而且不僅僅有html還有js,js中定義了程式碼片段對應的controller。這種情況下,不僅僅要考慮動態載入html的問題,還要考慮動態定義controller的問題。controller是通過angular的controllerProvider的register方法註冊,因此需要獲得controllerProvider的執行個體。
spa.js
app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) { app.providers = { $controllerProvider: $controllerProvider //注意這裡!!! }; /* 必須設定生效,否則下面的設定不生效 */ $locationProvider.html5Mode(true); /* 根據url的變化載入內容 */ $routeProvider.when('/test/lazyspa/page1', { /*!!!頁面中引入動態內容!!!*/ template: '<div>page1</div><div ng-include="\'page1.html\'"></div>', controller: 'ctrlPage1' }).when('/test/lazyspa/page2', { template: '<div>page2</div>', }).otherwise({ template: '<div>main</div>', }); app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) { /* 用這種方式,ng-include配合,根據商務邏輯動態擷取頁面內容 */ /* !!!動態定義controller!!! */ app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) { $scope.openAlert = function() { alert('page1 alert'); }; }]); /* !!!動態定義頁面的內容!!! */ $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>'); }]);}]);
4、動態載入模組
採用上面子頁面片段的載入方式存在一個局限,就是各種邏輯(js)要加入到啟動模組中,這樣還是限制子頁面片段的獨立封裝。特別是,如果子頁面片段需要使用第三方模組,且這個模組在啟動模組中沒有事先載入時,就沒有辦法了。所以,必須要能夠實現模組的動態載入。實現模組的動態載入就是把angular啟動過程中載入模組的方式提取出來,再處理一些特殊情況。
但是,實際跑起來發現文章中的代碼有問題,就是“$injector”到底是什嗎?研究了angular的原始碼injector.js才大概搞明白是怎麼回事。
一個應用有兩個$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用錯了,就會找到需要的服務。
routeProvider中動態載入模組檔案。
template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',resolve: { load: ['$q', function($q) { var defer = $q.defer(); /* 動態載入angular模組 */ require(['/test/lazyspa/module1.js'], function(loader) { loader.onload && loader.onload(function() { defer.resolve(); }); }); return defer.promise; }]}
動態載入angular模組
angular._lazyLoadModule = function(moduleName) { var m = angular.module(moduleName); console.log('register module:' + moduleName); /* 應用的injector,和config中的injector不是同一個,是instanceInject,返回的是通過provider.$get建立的執行個體 */ var $injector = angular.element(document).injector(); /* 遞迴載入依賴的模組 */ angular.forEach(m.requires, function(r) { angular._lazyLoadModule(r); }); /* 用provider的injector運行模組的controller,directive等等 */ angular.forEach(m._invokeQueue, function(invokeArgs) { try { var provider = providers.$injector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } catch (e) { console.error('load module invokeQueue failed:' + e.message, invokeArgs); } }); /* 用provider的injector運行模組的config */ angular.forEach(m._configBlocks, function(invokeArgs) { try { providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]); } catch (e) { console.error('load module configBlocks failed:' + e.message, invokeArgs); } }); /* 用應用的injector運行模組的run */ angular.forEach(m._runBlocks, function(fn) { $injector.invoke(fn); });};
定義模組
module1.js
define(["angular"], function(angular) { var onloads = []; var loadCss = function(url) { var link, head; link = document.createElement('link'); link.href = url; link.rel = 'stylesheet'; head = document.querySelector('head'); head.appendChild(link); }; loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css'); /* !!! 動態定義requirejs !!!*/ require.config({ paths: { 'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min' }, shim: { "ui-bootstrap-tpls": { deps: ['angular'] } } }); /*!!! 模組中需要引用第三方的庫,載入模組依賴的模組 !!!*/ require(['ui-bootstrap-tpls'], function() { var m1 = angular.module('module1', ['ui.bootstrap']); m1.config(['$controllerProvider', function($controllerProvider) { console.log('module1 - config begin'); }]); m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) { console.log('module1 - ctrl begin'); /*!!! 開啟angular ui的對話方塊 !!!*/ var dlg = '<div class="modal-header">'; dlg += '<h3 class="modal-title">I\'m a modal!</h3>'; dlg += '</div>'; dlg += '<div class="modal-body">content</div>'; dlg += '<div class="modal-footer">'; dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>'; dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>'; dlg += '</div>'; $scope.openDialog = function() { $uibModal.open({ template: dlg, controller: ['$scope', '$uibModalInstance', function($scope, $mi) { $scope.cancel = function() { $mi.dismiss(); }; $scope.ok = function() { $mi.close(); }; }], backdrop: 'static' }); }; }]); /* !!!動態載入模組!!! */ angular._lazyLoadModule('module1'); console.log('module1 loaded'); angular.forEach(onloads, function(onload) { angular.isFunction(onload) && onload(); }); }); return { onload: function(callback) { onloads.push(callback); } };});
二、完整的代碼
index.html
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <meta content="width=device-width,user-scalable=no,initial-scale=1.0" name="viewport"> <base href='/'> <title>SPA</title> </head> <body> <div ng-controller='ctrlMain'> <div> <a href="/test/lazyspa/page1">page1</a> <a href="/test/lazyspa/page2">page2</a> <a href="/test/lazyspa/">main</a> </div> <div ng-view></div> </div> <div class="loading"><div class='loading-indicator'><i></i></div></div> <script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js?_=3"></script> </body></html>
spa-loader.js
window.loading = { finish: function() { /* 保留個方法做一些載入完成後的處理,我實際的項目中會在這裡結束載入動畫 */ }, load: function() { require.config({ paths: { "domReady": '/static/js/domReady', "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min", "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min", }, shim: { "angular": { exports: "angular" }, "angular-route": { deps: ["angular"] }, }, deps: ['/test/lazyspa/spa.js'], urlArgs: "bust=" + (new Date()).getTime() }); }};window.loading.load();
spa.js
'use strict';define(["require", "angular", "angular-route"], function(require, angular) { var app = angular.module('app', ['ngRoute']); /* 消極式載入模組 */ angular._lazyLoadModule = function(moduleName) { var m = angular.module(moduleName); console.log('register module:' + moduleName); /* 應用的injector,和config中的injector不是同一個,是instanceInject,返回的是通過provider.$get建立的執行個體 */ var $injector = angular.element(document).injector(); /* 遞迴載入依賴的模組 */ angular.forEach(m.requires, function(r) { angular._lazyLoadModule(r); }); /* 用provider的injector運行模組的controller,directive等等 */ angular.forEach(m._invokeQueue, function(invokeArgs) { try { var provider = providers.$injector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } catch (e) { console.error('load module invokeQueue failed:' + e.message, invokeArgs); } }); /* 用provider的injector運行模組的config */ angular.forEach(m._configBlocks, function(invokeArgs) { try { providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]); } catch (e) { console.error('load module configBlocks failed:' + e.message, invokeArgs); } }); /* 用應用的injector運行模組的run */ angular.forEach(m._runBlocks, function(fn) { $injector.invoke(fn); }); }; app.config(['$injector', '$locationProvider', '$routeProvider', '$controllerProvider', function($injector, $locationProvider, $routeProvider, $controllerProvider) { /** * config中的injector和應用的injector不是同一個,是providerInjector,獲得的是provider,而不是通過provider建立的執行個體 * 這個injector通過angular無法獲得,所以在執行config的時候把它儲存下來 */ app.providers = { $injector: $injector, $controllerProvider: $controllerProvider }; /* 必須設定生效,否則下面的設定不生效 */ $locationProvider.html5Mode(true); /* 根據url的變化載入內容 */ $routeProvider.when('/test/lazyspa/page1', { template: '<div>page1</div><div ng-include="\'page1.html\'"></div>', controller: 'ctrlPage1' }).when('/test/lazyspa/page2', { template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>', resolve: { load: ['$q', function($q) { var defer = $q.defer(); /* 動態載入angular模組 */ require(['/test/lazyspa/module1.js'], function(loader) { loader.onload && loader.onload(function() { defer.resolve(); }); }); return defer.promise; }] } }).otherwise({ template: '<div>main</div>', }); }]); app.controller('ctrlMain', ['$scope', '$location', function($scope, $location) { console.log('main controller'); /* 根據商務邏輯自動到預設的視圖 */ $location.url('/test/lazyspa/page1'); }]); app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) { /* 用這種方式,ng-include配合,根據商務邏輯動態擷取頁面內容 */ /* 動態定義controller */ app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) { $scope.openAlert = function() { alert('page1 alert'); }; }]); /* 動態定義頁面內容 */ $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>'); }]); require(['domReady!'], function(document) { angular.bootstrap(document, ["app"]); });});
module1.js
'use strict';define(["angular"], function(angular) { var onloads = []; var loadCss = function(url) { var link, head; link = document.createElement('link'); link.href = url; link.rel = 'stylesheet'; head = document.querySelector('head'); head.appendChild(link); }; loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css'); require.config({ paths: { 'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min' }, shim: { "ui-bootstrap-tpls": { deps: ['angular'] } } }); require(['ui-bootstrap-tpls'], function() { var m1 = angular.module('module1', ['ui.bootstrap']); m1.config(['$controllerProvider', function($controllerProvider) { console.log('module1 - config begin'); }]); m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) { console.log('module1 - ctrl begin'); var dlg = '<div class="modal-header">'; dlg += '<h3 class="modal-title">I\'m a modal!</h3>'; dlg += '</div>'; dlg += '<div class="modal-body">content</div>'; dlg += '<div class="modal-footer">'; dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>'; dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>'; dlg += '</div>'; $scope.openDialog = function() { $uibModal.open({ template: dlg, controller: ['$scope', '$uibModalInstance', function($scope, $mi) { $scope.cancel = function() { $mi.dismiss(); }; $scope.ok = function() { $mi.close(); }; }], backdrop: 'static' }); }; }]); angular._lazyLoadModule('module1'); console.log('module1 loaded'); angular.forEach(onloads, function(onload) { angular.isFunction(onload) && onload(); }); }); return { onload: function(callback) { onloads.push(callback); } };});
以上就是本文的全部內容,希望對大家的學習有所協助。