探索angularjs+requirejs全面實現(xiàn)按需加載的套路
在進行有一定規(guī)模的項目時,通常希望實現(xiàn)以下目標:1、支持復雜的頁面邏輯(根據(jù)業(yè)務規(guī)則動態(tài)展現(xiàn)內(nèi)容,例如:權限,數(shù)據(jù)狀態(tài)等);2、堅持前后端分離的基本原則(不分離的時候,可以在后端用模版引擎直接生成好頁面);3、頁面加載時間短(業(yè)務邏輯復雜就需要引用第三方的庫,但很可能加載的庫和用戶本次操作沒關系);4,還要代碼好維護(加入新的邏輯時,影響的文件盡量少)。
想同時實現(xiàn)這些目標,就必須有一套按需加載的機制,頁面上展現(xiàn)的內(nèi)容和所有需要依賴的文件,都可以根據(jù)業(yè)務邏輯需要按需加載。最近都是基于angularjs做開發(fā),所以本文主要圍繞angularjs提供的各種機制,探索全面實現(xiàn)按需加載的套路。
一、一步一步實現(xiàn)
基本思路:1、先開發(fā)一個框架頁面,它可以完成一些基本的業(yè)務邏輯,并且支持擴展的機制;2、業(yè)務邏輯變復雜,需要把部分邏輯拆分到子頁面中,子頁面按需加載;3、子頁面中的展現(xiàn)內(nèi)容也變了復雜,又需要進行拆分,按需加載;4、子頁面的內(nèi)容復雜到依賴外部模塊,需要按需加載angular模塊。
1、框架頁
提到前端的按需加載,就會想到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>
注意:采用手動啟動angular的方式,因此html中沒有ng-app。
spa-loader.js
require.config({
paths: {
"domReady": '/static/js/domReady',
"angular": "http://cdn.bootcss.com/angular.js/1.4.8/angular.min",
"angular-route": "http://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已經(jīng)提供完整的子頁面加載的方法,直接用。
注意必須設置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);
/* 根據(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、按需加載子頁面中的內(nèi)容
用routeProvider的前提是url要發(fā)生變化,但是有的時候只是子頁面中的局部要發(fā)生變化。如果這些變化主要是和綁定的數(shù)據(jù)相關,不影響頁面布局,或者影響很小,那么通過ng-if一類的標簽基本就解決了。但是有的時候要根據(jù)頁面狀態(tài),完全改變局部的內(nèi)容,例如:用戶登錄前和登錄后局部要發(fā)生的變化等,這就意味著局部的布局可能也挺復雜,需要作為獨立的單元來對待。
利用ng-include可以解決頁面局部內(nèi)容加載的問題。但是,我們可以再考慮更復雜一些的情況。這個頁面片段對應的代碼是后端動態(tài)生成的,而且不僅僅有html還有js,js中定義了代碼片段對應的controller。這種情況下,不僅僅要考慮動態(tài)加載html的問題,還要考慮動態(tài)定義controller的問題。controller是通過angular的controllerProvider的register方法注冊,因此需要獲得controllerProvider的實例。
spa.js
app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) {
app.providers = {
$controllerProvider: $controllerProvider //注意這里?。?!
};
/* 必須設置生效,否則下面的設置不生效 */
$locationProvider.html5Mode(true);
/* 根據(jù)url的變化加載內(nèi)容 */
$routeProvider.when('/test/lazyspa/page1', {
/*!!!頁面中引入動態(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è)務邏輯動態(tài)獲取頁面內(nèi)容 */
/* !!!動態(tài)的定義controller!!! */
app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
$scope.openAlert = function() {
alert('page1 alert');
};
}]);
/* !!!動態(tài)定義頁面的內(nèi)容!!! */
$templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
}]);
}]);
4、動態(tài)加載模塊
采用上面子頁面片段的加載方式存在一個局限,就是各種邏輯(js)要加入到啟動模塊中,這樣還是限制子頁面片段的獨立封裝。特別是,如果子頁面片段需要使用第三方模塊,且這個模塊在啟動模塊中沒有事先加載時,就沒有辦法了。所以,必須要能夠?qū)崿F(xiàn)模塊的動態(tài)加載。實現(xiàn)模塊的動態(tài)加載就是把angular啟動過程中加載模塊的方式提取出來,再處理一些特殊情況。
但是,實際跑起來發(fā)現(xiàn)文章中的代碼有問題,就是“$injector”到底是什么?研究了angular的源代碼injector.js才大概搞明白是怎么回事。
一個應用有兩個$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用錯了,就會找到需要的服務。
routeProvider中動態(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();
/* 動態(tài)加載angular模塊 */
require(['/test/lazyspa/module1.js'], function(loader) {
loader.onload && loader.onload(function() {
defer.resolve();
});
});
return defer.promise;
}]
}
動態(tài)加載angular模塊
angular._lazyLoadModule = function(moduleName) {
var m = angular.module(moduleName);
console.log('register module:' + moduleName);
/* 應用的injector,和config中的injector不是同一個,是instanceInject,返回的是通過provider.$get創(chuàng)建的實例 */
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');
/* !!! 動態(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']
}
}
});
/*!!! 模塊中需要引用第三方的庫,加載模塊依賴的模塊 !!!*/
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'
});
};
}]);
/* !!!動態(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() {
/* 保留個方法做一些加載完成后的處理,我實際的項目中會在這里結束加載動畫 */
},
load: function() {
require.config({
paths: {
"domReady": '/static/js/domReady',
"angular": "http://cdn.bootcss.com/angular.js/1.4.8/angular.min",
"angular-route": "http://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創(chuàng)建的實例 */
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創(chuàng)建的實例
* 這個injector通過angular無法獲得,所以在執(zhí)行config的時候把它保存下來
*/
app.providers = {
$injector: $injector,
$controllerProvider: $controllerProvider
};
/* 必須設置生效,否則下面的設置不生效 */
$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();
/* 動態(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è)務邏輯自動到缺省的視圖 */
$location.url('/test/lazyspa/page1');
}]);
app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
/* 用這種方式,ng-include配合,根據(jù)業(yè)務邏輯動態(tài)獲取頁面內(nèi)容 */
/* 動態(tài)的定義controller */
app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
$scope.openAlert = function() {
alert('page1 alert');
};
}]);
/* 動態(tài)定義頁面內(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);
}
};
});
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助。
相關文章
詳解AngularJs路由之Ui-router-resolve(預加載)
本篇文章主要介紹了詳解AngularJs路由之Ui-router-resolve(預加載),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
淺談angularJs函數(shù)的使用方法(大小寫轉(zhuǎn)換,拷貝,擴充對象)
今天小編就為大家分享一篇淺談angularJs函數(shù)的使用方法(大小寫轉(zhuǎn)換,拷貝,擴充對象),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10
angularJs自定義過濾器實現(xiàn)手機號信息隱藏的方法
今天小編就為大家分享一篇angularJs自定義過濾器實現(xiàn)手機號信息隱藏的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10
整理AngularJS框架使用過程當中的一些性能優(yōu)化要點
這篇文章主要介紹了AngularJS框架使用過程當中的一些性能優(yōu)化要點,Angular通常被用來制作大型單頁應用,因而性能問題也是必須考慮的因素,需要的朋友可以參考下2016-03-03

