当前位置:   article > 正文

angularjs+requirejs实现按需加载的全面实践

angular 项目 require

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

  1. require.config({
  2. paths: {
  3. "domReady": '/static/js/domReady',
  4. "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
  5. "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
  6. },
  7. shim: {
  8. "angular": {
  9. exports: "angular"
  10. },
  11. "angular-route": {
  12. deps: ["angular"]
  13. },
  14. },
  15. deps: ['/test/lazyspa/spa.js'],
  16. urlArgs: "bust=" + (new Date()).getTime()
  17. });

spa.js

  1. define(["require", "angular", "angular-route"], function(require, angular) {
  2. var app = angular.module('app', ['ngRoute']);
  3. require(['domReady!'], function(document) {
  4. angular.bootstrap(document, ["app"]); /*手工启动angular*/
  5. window.loading.finish();
  6. });
  7. });

2、按需加载子页面

angular的routeProvider+ng-view已经提供完整的子页面加载的方法,直接用。
注意必须设置html5Mode,否则url变化以后,routeProvider不截获。

index.html

  1. <div>
  2. <a href="/test/lazyspa/page1">page1</a>
  3. <a href="/test/lazyspa/page2">page2</a>
  4. <a href="/test/lazyspa/">main</a>
  5. </div>
  6. <div ng-view></div>

spa.js

  1. app.config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) {
  2. /* 必须设置生效,否则下面的设置不生效 */
  3. $locationProvider.html5Mode(true);
  4. /* 根据url的变化加载内容 */
  5. $routeProvider.when('/test/lazyspa/page1', {
  6. template: '<div>page1</div>',
  7. }).when('/test/lazyspa/page2', {
  8. template: '<div>page2</div>',
  9. }).otherwise({
  10. template: '<div>main</div>',
  11. });
  12. }]);

3、按需加载子页面中的内容

用routeProvider的前提是url要发生变化,但是有的时候只是子页面中的局部要发生变化。如果这些变化主要是和绑定的数据相关,不影响页面布局,或者影响很小,那么通过ng-if一类的标签基本就解决了。但是有的时候要根据页面状态,完全改变局部的内容,例如:用户登录前和登录后局部要发生的变化等,这就意味着局部的布局可能也挺复杂,需要作为独立的单元来对待。

利用ng-include可以解决页面局部内容加载的问题。但是,我们可以再考虑更复杂一些的情况。这个页面片段对应的代码是后端动态生成的,而且不仅仅有html还有js,js中定义了代码片段对应的controller。这种情况下,不仅仅要考虑动态加载html的问题,还要考虑动态定义controller的问题。controller是通过angular的controllerProvider的register方法注册,因此需要获得controllerProvider的实例。

spa.js

  1. app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) {
  2. app.providers = {
  3. $controllerProvider: $controllerProvider //注意这里!!!
  4. };
  5. /* 必须设置生效,否则下面的设置不生效 */
  6. $locationProvider.html5Mode(true);
  7. /* 根据url的变化加载内容 */
  8. $routeProvider.when('/test/lazyspa/page1', {
  9. /*!!!页面中引入动态内容!!!*/
  10. template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
  11. controller: 'ctrlPage1'
  12. }).when('/test/lazyspa/page2', {
  13. template: '<div>page2</div>',
  14. }).otherwise({
  15. template: '<div>main</div>',
  16. });
  17. app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
  18. /* 用这种方式,ng-include配合,根据业务逻辑动态获取页面内容 */
  19. /* !!!动态的定义controller!!! */
  20. app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
  21. $scope.openAlert = function() {
  22. alert('page1 alert');
  23. };
  24. }]);
  25. /* !!!动态定义页面的内容!!! */
  26. $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
  27. }]);
  28. }]);

4、动态加载模块

采用上面子页面片段的加载方式存在一个局限,就是各种逻辑(js)要加入到启动模块中,这样还是限制子页面片段的独立封装。特别是,如果子页面片段需要使用第三方模块,且这个模块在启动模块中没有事先加载时,就没有办法了。所以,必须要能够实现模块的动态加载。实现模块的动态加载就是把angular启动过程中加载模块的方式提取出来,再处理一些特殊情况。

动态加载模块深入分析可以参考这篇文章:
http://www.tuicool.com/articles/jmuymiE

但是,实际跑起来发现文章中的代码有问题,就是“$injector”到底是什么?研究了angular的源代码injector.js才大概搞明白是怎么回事。

一个应用有两个$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用错了,就会找到需要的服务。

  • routeProvider中动态加载模块文件。

  1. template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
  2. resolve: {
  3. load: ['$q', function($q) {
  4. var defer = $q.defer();
  5. /* 动态加载angular模块 */
  6. require(['/test/lazyspa/module1.js'], function(loader) {
  7. loader.onload && loader.onload(function() {
  8. defer.resolve();
  9. });
  10. });
  11. return defer.promise;
  12. }]
  13. }
  • 动态加载angular模块

  1. angular._lazyLoadModule = function(moduleName) {
  2. var m = angular.module(moduleName);
  3. console.log('register module:' + moduleName);
  4. /* 应用的injector,和config中的injector不是同一个,是instanceInject,返回的是通过provider.$get创建的实例 */
  5. var $injector = angular.element(document).injector();
  6. /* 递归加载依赖的模块 */
  7. angular.forEach(m.requires, function(r) {
  8. angular._lazyLoadModule(r);
  9. });
  10. /* 用provider的injector运行模块的controller,directive等等 */
  11. angular.forEach(m._invokeQueue, function(invokeArgs) {
  12. try {
  13. var provider = providers.$injector.get(invokeArgs[0]);
  14. provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
  15. } catch (e) {
  16. console.error('load module invokeQueue failed:' + e.message, invokeArgs);
  17. }
  18. });
  19. /* 用provider的injector运行模块的config */
  20. angular.forEach(m._configBlocks, function(invokeArgs) {
  21. try {
  22. providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
  23. } catch (e) {
  24. console.error('load module configBlocks failed:' + e.message, invokeArgs);
  25. }
  26. });
  27. /* 用应用的injector运行模块的run */
  28. angular.forEach(m._runBlocks, function(fn) {
  29. $injector.invoke(fn);
  30. });
  31. };
  • 定义模块
    module1.js

  1. define(["angular"], function(angular) {
  2. var onloads = [];
  3. var loadCss = function(url) {
  4. var link, head;
  5. link = document.createElement('link');
  6. link.href = url;
  7. link.rel = 'stylesheet';
  8. head = document.querySelector('head');
  9. head.appendChild(link);
  10. };
  11. loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
  12. /* !!! 动态定义requirejs !!!*/
  13. require.config({
  14. paths: {
  15. 'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
  16. },
  17. shim: {
  18. "ui-bootstrap-tpls": {
  19. deps: ['angular']
  20. }
  21. }
  22. });
  23. /*!!! 模块中需要引用第三方的库,加载模块依赖的模块 !!!*/
  24. require(['ui-bootstrap-tpls'], function() {
  25. var m1 = angular.module('module1', ['ui.bootstrap']);
  26. m1.config(['$controllerProvider', function($controllerProvider) {
  27. console.log('module1 - config begin');
  28. }]);
  29. m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
  30. console.log('module1 - ctrl begin');
  31. /*!!! 打开angular ui的对话框 !!!*/
  32. var dlg = '<div class="modal-header">';
  33. dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
  34. dlg += '</div>';
  35. dlg += '<div class="modal-body">content</div>';
  36. dlg += '<div class="modal-footer">';
  37. dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
  38. dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
  39. dlg += '</div>';
  40. $scope.openDialog = function() {
  41. $uibModal.open({
  42. template: dlg,
  43. controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
  44. $scope.cancel = function() {
  45. $mi.dismiss();
  46. };
  47. $scope.ok = function() {
  48. $mi.close();
  49. };
  50. }],
  51. backdrop: 'static'
  52. });
  53. };
  54. }]);
  55. /* !!!动态加载模块!!! */
  56. angular._lazyLoadModule('module1');
  57. console.log('module1 loaded');
  58. angular.forEach(onloads, function(onload) {
  59. angular.isFunction(onload) && onload();
  60. });
  61. });
  62. return {
  63. onload: function(callback) {
  64. onloads.push(callback);
  65. }
  66. };
  67. });

二、完整的代码

  • index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta content="width=device-width,user-scalable=no,initial-scale=1.0" name="viewport">
  6. <base href='/'>
  7. <title>SPA</title>
  8. </head>
  9. <body>
  10. <div ng-controller='ctrlMain'>
  11. <div>
  12. <a href="/test/lazyspa/page1">page1</a>
  13. <a href="/test/lazyspa/page2">page2</a>
  14. <a href="/test/lazyspa/">main</a>
  15. </div>
  16. <div ng-view></div>
  17. </div>
  18. <div class="loading"><div class='loading-indicator'><i></i></div></div>
  19. <script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js?_=3"></script>
  20. </body>
  21. </html>
  • spa-loader.js

  1. window.loading = {
  2. finish: function() {
  3. /* 保留个方法做一些加载完成后的处理,我实际的项目中会在这里结束加载动画 */
  4. },
  5. load: function() {
  6. require.config({
  7. paths: {
  8. "domReady": '/static/js/domReady',
  9. "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
  10. "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
  11. },
  12. shim: {
  13. "angular": {
  14. exports: "angular"
  15. },
  16. "angular-route": {
  17. deps: ["angular"]
  18. },
  19. },
  20. deps: ['/test/lazyspa/spa.js'],
  21. urlArgs: "bust=" + (new Date()).getTime()
  22. });
  23. }
  24. };
  25. window.loading.load();
  • spa.js

  1. 'use strict';
  2. define(["require", "angular", "angular-route"], function(require, angular) {
  3. var app = angular.module('app', ['ngRoute']);
  4. /* 延迟加载模块 */
  5. angular._lazyLoadModule = function(moduleName) {
  6. var m = angular.module(moduleName);
  7. console.log('register module:' + moduleName);
  8. /* 应用的injector,和config中的injector不是同一个,是instanceInject,返回的是通过provider.$get创建的实例 */
  9. var $injector = angular.element(document).injector();
  10. /* 递归加载依赖的模块 */
  11. angular.forEach(m.requires, function(r) {
  12. angular._lazyLoadModule(r);
  13. });
  14. /* 用provider的injector运行模块的controller,directive等等 */
  15. angular.forEach(m._invokeQueue, function(invokeArgs) {
  16. try {
  17. var provider = providers.$injector.get(invokeArgs[0]);
  18. provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
  19. } catch (e) {
  20. console.error('load module invokeQueue failed:' + e.message, invokeArgs);
  21. }
  22. });
  23. /* 用provider的injector运行模块的config */
  24. angular.forEach(m._configBlocks, function(invokeArgs) {
  25. try {
  26. providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
  27. } catch (e) {
  28. console.error('load module configBlocks failed:' + e.message, invokeArgs);
  29. }
  30. });
  31. /* 用应用的injector运行模块的run */
  32. angular.forEach(m._runBlocks, function(fn) {
  33. $injector.invoke(fn);
  34. });
  35. };
  36. app.config(['$injector', '$locationProvider', '$routeProvider', '$controllerProvider', function($injector, $locationProvider, $routeProvider, $controllerProvider) {
  37. /*
  38. * config中的injector和应用的injector不是同一个,是providerInjector,获得的是provider,而不是通过provider创建的实例
  39. * 这个injector通过angular无法获得,所以在执行config的时候把它保存下来
  40. */
  41. app.providers = {
  42. $injector: $injector,
  43. $controllerProvider: $controllerProvider
  44. };
  45. /* 必须设置生效,否则下面的设置不生效 */
  46. $locationProvider.html5Mode(true);
  47. /* 根据url的变化加载内容 */
  48. $routeProvider.when('/test/lazyspa/page1', {
  49. template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
  50. controller: 'ctrlPage1'
  51. }).when('/test/lazyspa/page2', {
  52. template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
  53. resolve: {
  54. load: ['$q', function($q) {
  55. var defer = $q.defer();
  56. /* 动态加载angular模块 */
  57. require(['/test/lazyspa/module1.js'], function(loader) {
  58. loader.onload && loader.onload(function() {
  59. defer.resolve();
  60. });
  61. });
  62. return defer.promise;
  63. }]
  64. }
  65. }).otherwise({
  66. template: '<div>main</div>',
  67. });
  68. }]);
  69. app.controller('ctrlMain', ['$scope', '$location', function($scope, $location) {
  70. console.log('main controller');
  71. /* 根据业务逻辑自动到缺省的视图 */
  72. $location.url('/test/lazyspa/page1');
  73. }]);
  74. app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
  75. /* 用这种方式,ng-include配合,根据业务逻辑动态获取页面内容 */
  76. /* 动态的定义controller */
  77. app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
  78. $scope.openAlert = function() {
  79. alert('page1 alert');
  80. };
  81. }]);
  82. /* 动态定义页面内容 */
  83. $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
  84. }]);
  85. require(['domReady!'], function(document) {
  86. angular.bootstrap(document, ["app"]);
  87. });
  88. });

module1.js

  1. 'use strict';
  2. define(["angular"], function(angular) {
  3. var onloads = [];
  4. var loadCss = function(url) {
  5. var link, head;
  6. link = document.createElement('link');
  7. link.href = url;
  8. link.rel = 'stylesheet';
  9. head = document.querySelector('head');
  10. head.appendChild(link);
  11. };
  12. loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
  13. require.config({
  14. paths: {
  15. 'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
  16. },
  17. shim: {
  18. "ui-bootstrap-tpls": {
  19. deps: ['angular']
  20. }
  21. }
  22. });
  23. require(['ui-bootstrap-tpls'], function() {
  24. var m1 = angular.module('module1', ['ui.bootstrap']);
  25. m1.config(['$controllerProvider', function($controllerProvider) {
  26. console.log('module1 - config begin');
  27. }]);
  28. m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
  29. console.log('module1 - ctrl begin');
  30. var dlg = '<div class="modal-header">';
  31. dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
  32. dlg += '</div>';
  33. dlg += '<div class="modal-body">content</div>';
  34. dlg += '<div class="modal-footer">';
  35. dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
  36. dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
  37. dlg += '</div>';
  38. $scope.openDialog = function() {
  39. $uibModal.open({
  40. template: dlg,
  41. controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
  42. $scope.cancel = function() {
  43. $mi.dismiss();
  44. };
  45. $scope.ok = function() {
  46. $mi.close();
  47. };
  48. }],
  49. backdrop: 'static'
  50. });
  51. };
  52. }]);
  53. angular._lazyLoadModule('module1');
  54. console.log('module1 loaded');
  55. angular.forEach(onloads, function(onload) {
  56. angular.isFunction(onload) && onload();
  57. });
  58. });
  59. return {
  60. onload: function(callback) {
  61. onloads.push(callback);
  62. }
  63. };
  64. });

写后感

年初定下的目标是坚持每周写一篇自己在开发过程碰到的问题总结,本以为是个简单的事情,写起来才发现写文章的时间比写代码的花的时间还要长。因为写代码的时候只要功能实现了就行了,但是,写文章的时候就一定要把代码搞清楚才敢写,实际上就是逼着自己要认真研究源代码,虽然压力很大,但收获更大。另一方面,发现找到一个好题目挺难的,只是简单的贴别人的代码没意思,可是自己想出来有价值,有意思的问题挺难的。因此大家要是觉得有啥有意思,有价值前端问题,分享一下吧,给我的年度写作计划帮帮忙

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/707454
推荐阅读
相关标签
  

闽ICP备14008679号