探索angularjs+requirejs全面实现按需加载的套路
在进行有一定规模的项目时,通常希望实现以下目标:1、支持复杂的页面逻辑(根据业务规则动态展现内容,例如:权限,数据状态等);2、坚持前后端分离的基本原则(不分离的时候,可以在后端用模版引擎直接生成好页面);3、页面加载时间短(业务逻辑复杂就需要引用第三方的库,但很可能加载的库和用户本次操作没关系);4,还要代码好维护(加入新的逻辑时,影响的文件尽量少)。
想同时实现这些目标,就必须有一套按需加载的机制,页面上展现的内容和所有需要依赖的文件,都可以根据业务逻辑需要按需加载。最近都是基于angularjs做开发,所以本文主要围绕angularjs提供的各种机制,探索全面实现按需加载的套路。
一、一步一步实现
基本思路:1、先开发一个框架页面,它可以完成一些基本的业务逻辑,并且支持扩展的机制;2、业务逻辑变复杂,需要把部分逻辑拆分到子页面中,子页面按需加载;3、子页面中的展现内容也变了复杂,又需要进行拆分,按需加载;4、子页面的内容复杂到依赖外部模块,需要按需加载angular模块。
1、框架页
提到前端的按需加载,就会想到AMD(AsynchronousModuleDefinition),现在用requirejs的非常多,所以首先考虑引入requires。
index.html
<scriptsrc="static/js/require.js"deferasyncdata-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="+(newDate()).getTime() });
spa.js
define(["require","angular","angular-route"],function(require,angular){ varapp=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> <ahref="/test/lazyspa/page1">page1</a> <ahref="/test/lazyspa/page2">page2</a> <ahref="/test/lazyspa/">main</a> </div> <divng-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><divng-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('page1alert'); }; }]); /*!!!动态定义页面的内容!!!*/ $templateCache.put('page1.html','<divng-controller="ctrlPage1Dyna"><buttonng-click="openAlert()">alert</button></div>'); }]); }]);
4、动态加载模块
采用上面子页面片段的加载方式存在一个局限,就是各种逻辑(js)要加入到启动模块中,这样还是限制子页面片段的独立封装。特别是,如果子页面片段需要使用第三方模块,且这个模块在启动模块中没有事先加载时,就没有办法了。所以,必须要能够实现模块的动态加载。实现模块的动态加载就是把angular启动过程中加载模块的方式提取出来,再处理一些特殊情况。
但是,实际跑起来发现文章中的代码有问题,就是“$injector”到底是什么?研究了angular的源代码injector.js才大概搞明白是怎么回事。
一个应用有两个$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用错了,就会找到需要的服务。
routeProvider中动态加载模块文件。
template:'<divng-controller="ctrlModule1"><div>page2</div><div><buttonng-click="openDialog()">opendialog</button></div></div>', resolve:{ load:['$q',function($q){ vardefer=$q.defer(); /*动态加载angular模块*/ require(['/test/lazyspa/module1.js'],function(loader){ loader.onload&&loader.onload(function(){ defer.resolve(); }); }); returndefer.promise; }] }
动态加载angular模块
angular._lazyLoadModule=function(moduleName){ varm=angular.module(moduleName); console.log('registermodule:'+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{ varprovider=providers.$injector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider,invokeArgs[2]); }catch(e){ console.error('loadmoduleinvokeQueuefailed:'+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('loadmoduleconfigBlocksfailed:'+e.message,invokeArgs); } }); /*用应用的injector运行模块的run*/ angular.forEach(m._runBlocks,function(fn){ $injector.invoke(fn); }); };
定义模块
module1.js
define(["angular"],function(angular){ varonloads=[]; varloadCss=function(url){ varlink,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(){ varm1=angular.module('module1',['ui.bootstrap']); m1.config(['$controllerProvider',function($controllerProvider){ console.log('module1-configbegin'); }]); m1.controller('ctrlModule1',['$scope','$uibModal',function($scope,$uibModal){ console.log('module1-ctrlbegin'); /*!!!打开angularui的对话框!!!*/ vardlg='<divclass="modal-header">'; dlg+='<h3class="modal-title">I\'mamodal!</h3>'; dlg+='</div>'; dlg+='<divclass="modal-body">content</div>'; dlg+='<divclass="modal-footer">'; dlg+='<buttonclass="btnbtn-primary"type="button"ng-click="ok()">OK</button>'; dlg+='<buttonclass="btnbtn-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('module1loaded'); angular.forEach(onloads,function(onload){ angular.isFunction(onload)&&onload(); }); }); return{ onload:function(callback){ onloads.push(callback); } }; });
二、完整的代码
index.html
<!DOCTYPEhtml> <html> <head> <metacharset="utf-8"> <metacontent="width=device-width,user-scalable=no,initial-scale=1.0"name="viewport"> <basehref='/'> <title>SPA</title> </head> <body> <divng-controller='ctrlMain'> <div> <ahref="/test/lazyspa/page1">page1</a> <ahref="/test/lazyspa/page2">page2</a> <ahref="/test/lazyspa/">main</a> </div> <divng-view></div> </div> <divclass="loading"><divclass='loading-indicator'><i></i></div></div> <scriptsrc="static/js/require.js"deferasyncdata-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="+(newDate()).getTime() }); } }; window.loading.load();
spa.js
'usestrict'; define(["require","angular","angular-route"],function(require,angular){ varapp=angular.module('app',['ngRoute']); /*延迟加载模块*/ angular._lazyLoadModule=function(moduleName){ varm=angular.module(moduleName); console.log('registermodule:'+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{ varprovider=providers.$injector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider,invokeArgs[2]); }catch(e){ console.error('loadmoduleinvokeQueuefailed:'+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('loadmoduleconfigBlocksfailed:'+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><divng-include="\'page1.html\'"></div>', controller:'ctrlPage1' }).when('/test/lazyspa/page2',{ template:'<divng-controller="ctrlModule1"><div>page2</div><div><buttonng-click="openDialog()">opendialog</button></div></div>', resolve:{ load:['$q',function($q){ vardefer=$q.defer(); /*动态加载angular模块*/ require(['/test/lazyspa/module1.js'],function(loader){ loader.onload&&loader.onload(function(){ defer.resolve(); }); }); returndefer.promise; }] } }).otherwise({ template:'<div>main</div>', }); }]); app.controller('ctrlMain',['$scope','$location',function($scope,$location){ console.log('maincontroller'); /*根据业务逻辑自动到缺省的视图*/ $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('page1alert'); }; }]); /*动态定义页面内容*/ $templateCache.put('page1.html','<divng-controller="ctrlPage1Dyna"><buttonng-click="openAlert()">alert</button></div>'); }]); require(['domReady!'],function(document){ angular.bootstrap(document,["app"]); }); });
module1.js
'usestrict'; define(["angular"],function(angular){ varonloads=[]; varloadCss=function(url){ varlink,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(){ varm1=angular.module('module1',['ui.bootstrap']); m1.config(['$controllerProvider',function($controllerProvider){ console.log('module1-configbegin'); }]); m1.controller('ctrlModule1',['$scope','$uibModal',function($scope,$uibModal){ console.log('module1-ctrlbegin'); vardlg='<divclass="modal-header">'; dlg+='<h3class="modal-title">I\'mamodal!</h3>'; dlg+='</div>'; dlg+='<divclass="modal-body">content</div>'; dlg+='<divclass="modal-footer">'; dlg+='<buttonclass="btnbtn-primary"type="button"ng-click="ok()">OK</button>'; dlg+='<buttonclass="btnbtn-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('module1loaded'); angular.forEach(onloads,function(onload){ angular.isFunction(onload)&&onload(); }); }); return{ onload:function(callback){ onloads.push(callback); } }; });
以上就是本文的全部内容,希望对大家的学习有所帮助。