A5下载文章资讯

分类分类

探索angularjs+requirejs全面实现按需加载的套路

2016-02-27 11:03作者:fang

在进行有一定规模的项目时,通常希望实现以下目标: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);

}

};

});

以上就是本文的全部内容,希望对大家的学习有所帮助。

展开全部

相关

说两句网友评论
    我要跟贴
    取消