想同時(shí)實(shí)現(xiàn)這些目標(biāo),就必須有一套按需加載的機(jī)制,頁(yè)面上展現(xiàn)的內(nèi)容和所有需要依賴(lài)的文件,都可以根據(jù)業(yè)務(wù)邏輯需要按需加載。最近都是基于angularjs做開(kāi)發(fā),所以本文主要圍繞angularjs提供的各種機(jī)制,探索全面實(shí)現(xiàn)按需加載的套路。
一、一步一步實(shí)現(xiàn)
基本思路:1、先開(kāi)發(fā)一個(gè)框架頁(yè)面,它可以完成一些基本的業(yè)務(wù)邏輯,并且支持?jǐn)U展的機(jī)制;2、業(yè)務(wù)邏輯變復(fù)雜,需要把部分邏輯拆分到子頁(yè)面中,子頁(yè)面按需加載;3、子頁(yè)面中的展現(xiàn)內(nèi)容也變了復(fù)雜,又需要進(jìn)行拆分,按需加載;4、子頁(yè)面的內(nèi)容復(fù)雜到依賴(lài)外部模塊,需要按需加載angular模塊。
1、框架頁(yè)
提到前端的按需加載,就會(huì)想到AMD( Asynchronous Module Definition),現(xiàn)在用requirejs的非常多,所以首先考慮引入requires。
index.html
<script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js"></script>
注意:采用手動(dòng)啟動(dòng)angular的方式,因此html中沒(méi)有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"]); /*手工啟動(dòng)angular*/ window.loading.finish(); }); });
2、按需加載子頁(yè)面
angular的routeProvider+ng-view已經(jīng)提供完整的子頁(yè)面加載的方法,直接用。
注意必須設(shè)置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) { /* 必須設(shè)置生效,否則下面的設(shè)置不生效 */ $locationProvider.html5Mode(true); /* 根據(jù)url的變化加載內(nèi)容 */ $routeProvider.when('/test/lazyspa/page1', { template: '<div>page1</div>', }).when('/test/lazyspa/page2', { template: '<div>page2</div>', }).otherwise({ template: '<div>main</div>', }); }]);
3、按需加載子頁(yè)面中的內(nèi)容
用routeProvider的前提是url要發(fā)生變化,但是有的時(shí)候只是子頁(yè)面中的局部要發(fā)生變化。如果這些變化主要是和綁定的數(shù)據(jù)相關(guān),不影響頁(yè)面布局,或者影響很小,那么通過(guò)ng-if一類(lèi)的標(biāo)簽基本就解決了。但是有的時(shí)候要根據(jù)頁(yè)面狀態(tài),完全改變局部的內(nèi)容,例如:用戶(hù)登錄前和登錄后局部要發(fā)生的變化等,這就意味著局部的布局可能也挺復(fù)雜,需要作為獨(dú)立的單元來(lái)對(duì)待。
利用ng-include可以解決頁(yè)面局部?jī)?nèi)容加載的問(wèn)題。但是,我們可以再考慮更復(fù)雜一些的情況。這個(gè)頁(yè)面片段對(duì)應(yīng)的代碼是后端動(dòng)態(tài)生成的,而且不僅僅有html還有js,js中定義了代碼片段對(duì)應(yīng)的controller。這種情況下,不僅僅要考慮動(dòng)態(tài)加載html的問(wèn)題,還要考慮動(dòng)態(tài)定義controller的問(wèn)題。controller是通過(guò)angular的controllerProvider的register方法注冊(cè),因此需要獲得controllerProvider的實(shí)例。
spa.js
app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) { app.providers = { $controllerProvider: $controllerProvider //注意這里!??! }; /* 必須設(shè)置生效,否則下面的設(shè)置不生效 */ $locationProvider.html5Mode(true); /* 根據(jù)url的變化加載內(nèi)容 */ $routeProvider.when('/test/lazyspa/page1', { /*!!!頁(yè)面中引入動(dòng)態(tài)內(nèi)容!!!*/ 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配合,根據(jù)業(yè)務(wù)邏輯動(dòng)態(tài)獲取頁(yè)面內(nèi)容 */ /* !!!動(dòng)態(tài)的定義controller!!! */ app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) { $scope.openAlert = function() { alert('page1 alert'); }; }]); /* !!!動(dòng)態(tài)定義頁(yè)面的內(nèi)容!!! */ $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"> <button ng-click="openAlert()">alert</button></div>'); }]); }]);
4、動(dòng)態(tài)加載模塊
采用上面子頁(yè)面片段的加載方式存在一個(gè)局限,就是各種邏輯(js)要加入到啟動(dòng)模塊中,這樣還是限制子頁(yè)面片段的獨(dú)立封裝。特別是,如果子頁(yè)面片段需要使用第三方模塊,且這個(gè)模塊在啟動(dòng)模塊中沒(méi)有事先加載時(shí),就沒(méi)有辦法了。所以,必須要能夠?qū)崿F(xiàn)模塊的動(dòng)態(tài)加載。實(shí)現(xiàn)模塊的動(dòng)態(tài)加載就是把a(bǔ)ngular啟動(dòng)過(guò)程中加載模塊的方式提取出來(lái),再處理一些特殊情況。
但是,實(shí)際跑起來(lái)發(fā)現(xiàn)文章中的代碼有問(wèn)題,就是“$injector”到底是什么?研究了angular的源代碼injector.js才大概搞明白是怎么回事。
一個(gè)應(yīng)用有兩個(gè)$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用錯(cuò)了,就會(huì)找到需要的服務(wù)。
routeProvider中動(dòng)態(tài)加載模塊文件。
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(); /* 動(dòng)態(tài)加載angular模塊 */ require(['/test/lazyspa/module1.js'], function(loader) { loader.onload && loader.onload(function() { defer.resolve(); }); }); return defer.promise; }] }
動(dòng)態(tài)加載angular模塊
angular._lazyLoadModule = function(moduleName) { var m = angular.module(moduleName); console.log('register module:' + moduleName); /* 應(yīng)用的injector,和config中的injector不是同一個(gè),是instanceInject,返回的是通過(guò)provider.$get創(chuàng)建的實(shí)例 */ var $injector = angular.element(document).injector(); /* 遞歸加載依賴(lài)的模塊 */ angular.forEach(m.requires, function(r) { angular._lazyLoadModule(r); }); /* 用provider的injector運(yùn)行模塊的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運(yùn)行模塊的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); } }); /* 用應(yīng)用的injector運(yùn)行模塊的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'); /* !!! 動(dòng)態(tài)定義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'] } } }); /*!!! 模塊中需要引用第三方的庫(kù),加載模塊依賴(lài)的模塊 !!!*/ 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'); /*!!! 打開(kāi)angular ui的對(duì)話(huà)框 !!!*/ 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' }); }; }]); /* !!!動(dòng)態(tài)加載模塊!!! */ 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() { /* 保留個(gè)方法做一些加載完成后的處理,我實(shí)際的項(xiàng)目中會(huì)在這里結(jié)束加載動(dòng)畫(huà) */ }, 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); /* 應(yīng)用的injector,和config中的injector不是同一個(gè),是instanceInject,返回的是通過(guò)provider.$get創(chuàng)建的實(shí)例 */ var $injector = angular.element(document).injector(); /* 遞歸加載依賴(lài)的模塊 */ angular.forEach(m.requires, function(r) { angular._lazyLoadModule(r); }); /* 用provider的injector運(yùn)行模塊的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運(yùn)行模塊的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); } }); /* 用應(yīng)用的injector運(yùn)行模塊的run */ angular.forEach(m._runBlocks, function(fn) { $injector.invoke(fn); }); }; app.config(['$injector', '$locationProvider', '$routeProvider', '$controllerProvider', function($injector, $locationProvider, $routeProvider, $controllerProvider) { /** * config中的injector和應(yīng)用的injector不是同一個(gè),是providerInjector,獲得的是provider, 而不是通過(guò)provider創(chuàng)建的實(shí)例 * 這個(gè)injector通過(guò)angular無(wú)法獲得,所以在執(zhí)行config的時(shí)候把它保存下來(lái) */ app.providers = { $injector: $injector, $controllerProvider: $controllerProvider }; /* 必須設(shè)置生效,否則下面的設(shè)置不生效 */ $locationProvider.html5Mode(true); /* 根據(jù)url的變化加載內(nèi)容 */ $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(); /* 動(dòng)態(tài)加載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'); /* 根據(jù)業(yè)務(wù)邏輯自動(dòng)到缺省的視圖 */ $location.url('/test/lazyspa/page1'); }]); app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) { /* 用這種方式,ng-include配合,根據(jù)業(yè)務(wù)邏輯動(dòng)態(tài)獲取頁(yè)面內(nèi)容 */ /* 動(dòng)態(tài)的定義controller */ app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) { $scope.openAlert = function() { alert('page1 alert'); }; }]); /* 動(dòng)態(tài)定義頁(yè)面內(nèi)容 */ $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); } }; });
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com