When working on a project of a certain scale, you usually want to achieve the following goals: 1. Support complex page logic (dynamic display of content based on business rules, such as permissions, data status, etc.); 2. Adhere to the basic principle of separation of front and back ends Principle (when not separated, you can use the template engine to directly generate a good page on the backend); 3. The page loading time is short (complex business logic requires the reference of a third-party library, but it is very likely that the loaded library has nothing to do with the user's current operation) ); 4. The code must be easy to maintain (when adding new logic, affect as few files as possible).
To achieve these goals at the same time, there must be an on-demand loading mechanism. The content displayed on the page and all dependent files can be loaded on demand according to business logic needs. Recently, all development is based on angularjs, so this article mainly focuses on the various mechanisms provided by angularjs to explore ways to fully implement on-demand loading.
1. Step by step implementation
Basic ideas: 1. First develop a framework page, which can complete some basic business logic and support expansion mechanisms; 2. Business logic becomes complex, and some logic needs to be split into sub-pages, which are loaded on demand; 3. The display content in the sub-pages has also become complex, and needs to be split and loaded on demand; 4. The content of the sub-pages is complex To rely on external modules, you need to load the angular module on demand.
1. Frame page
When it comes to on-demand loading of the front end, AMD (Asynchronous Module Definition) comes to mind. Nowadays, many requirejs is used. So first consider introducing requirements.
index.html
<script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js"></script>
Note: Angular is started manually, so there is no ng-app in the html.
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. Load subpages on demand
Angular’s routeProvider+ng-view already provides a complete sub-page loading method, which can be used directly.
Note that html5Mode must be set, otherwise routeProvider will not intercept the URL after it changes.
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. Load content in sub-pages on demand
use The premise of routeProvider is that the URL needs to change, but sometimes only parts of the subpage need to change. If these changes are mainly related to the bound data and do not affect the page layout, or the impact is very small, then tags such as ng-if can basically solve the problem. However, sometimes the local content needs to be completely changed based on the page status, such as local changes before and after the user logs in, etc. This means that the local layout may be quite complex and needs to be treated as an independent unit.
Use ng-include to solve the problem of loading partial content on the page. However, we can consider a more complex situation. The code corresponding to this page fragment is dynamically generated by the backend, and it is not only html but also js. The controller corresponding to the code fragment is defined in js. In this case, not only the issue of dynamically loading HTML must be considered, but also the issue of dynamically defining the controller must be considered. The controller is registered through the register method of angular's controllerProvider, so it is necessary to obtain an instance of the 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. Dynamic loading module
adopts the above sub-page fragment There is a limitation in the loading method, that is, various logic (js) must be added to the startup module, which still limits the independent encapsulation of sub-page fragments. In particular, if the subpage fragment requires the use of a third-party module, and this module is not loaded in advance in the startup module, there is no solution. Therefore, it is necessary to be able to dynamically load modules. To implement dynamic loading of modules is to extract the method of loading modules during Angular startup and then handle some special situations.
However, when I actually ran it, I found that there was a problem with the code in the article. What exactly is "$injector"? After studying angular's source code injector.js, I roughly figured out what was going on.
An application has two $injectors, providerInjector and instanceInjector. invokeQueue uses providerInjector, and runBlocks uses instanceProvider. If $injector is used incorrectly, the required service will be found.
Dynamic loading of module files in 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; }] }
Dynamic loading of angular modules
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); }); };
Define module
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); } }; });
2. Complete code
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); } }; });
The above is the content of exploring angularjs+requirejs to fully implement on-demand loading routine_AngularJS, and more For related content, please pay attention to the PHP Chinese website (www.php.cn)!