在举行有肯定范围的项目时,一般愿望完成以下目的: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启动历程当中加载模块的体式格局提取出来,再处置惩罚一些特殊状况。
动态加载模块深入分析可以参考这篇文章:
http://www.tuicool.com/articles/jmuymiE
然则,现实跑起来发明文章中的代码有问题,就是“$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);
}
};
});
写后感
年终定下的目的是对峙每周写一篇本身在开辟历程遇到的问题总结,本以为是个简朴的事变,写起来才发明写文章的时候比写代码的花的时候还要长。由于写代码的时刻只需功用完成了就好了,然则,写文章的时刻就肯定要把代码搞清楚才敢写,现实上就是逼着本身要仔细研讨源代码,虽然压力很大,但收成更大。另一方面,发明找到一个好问题挺难的,只是简朴的贴他人的代码没意思,但是本身想出来有价值,有意思的问题挺难的。因而人人如果以为有啥有意思,有价值前端问题,分享一下吧,给我的年度写作设计帮帮忙