一定規模のプロジェクトに取り組む場合、通常は次の目標を達成する必要があります。 1. 複雑なページ ロジックをサポートする (権限、データ ステータスなどのビジネス ルールに基づいたコンテンツの動的な表示)。フロントエンドとバックエンドの分離の基本原則を遵守します (分離されていない場合は、テンプレート エンジンを使用してバックエンドで適切なページを直接生成できます) 3. ページの読み込み時間が短い (ビジネス ロジックが複雑な場合)。 、サードパーティのライブラリを参照する必要がありますが、ロードされたライブラリはユーザーの現在の操作とは何の関係もない可能性が非常に高いです) ) 4. コードは保守しやすくなければなりません (新しいロジックを追加するときは、影響を最小限に抑える必要があります)。可能な限りファイル)。
これらの目標を同時に達成するには、ページに表示されるコンテンツとすべての依存ファイルをビジネス ロジックのニーズに応じてオンデマンドでロードできるメカニズムが必要です。最近はすべての開発が angularjs に基づいているため、この記事では主に angularjs が提供するさまざまなメカニズムに焦点を当て、オンデマンド読み込みを完全に実装する方法を検討します。
1. 段階的な実装
基本的な考え方: 1. まず、いくつかの基本的なビジネス ロジックを完成させ、拡張メカニズムをサポートできるフレームワーク ページを開発します。 、一部のロジックはサブページに分割する必要があり、それらはオンデマンドでロードされます。 3. サブページの表示コンテンツも複雑になり、オンデマンドで分割してロードする必要があります。サブページは複雑です 外部モジュールに依存するには、オンデマンドで Angular モジュールをロードする必要があります。
1. フレームページ
フロントエンドでのオンデマンドロードといえば、最近では AMD (Asynchronous Module Definition) が思い浮かびます。多くの requirejs が使用されるため、最初に要件の導入を検討してください。
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 がその URL をインターセプトしません。
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で定義されます。この場合、HTML を動的にロードするという問題だけでなく、コントローラーを動的に定義するという問題も考慮する必要があります。コントローラーは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 のソースコードを調べたところ、何が起こっているのかがおおよそわかりました。
アプリケーションには、providerInjector と instanceInjector という 2 つの $injector があります。 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); } }; });
2. 完全なコード
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); } }; });
上記は、オンデマンド読み込みルーチン_AngularJS を完全に実装するために angularjs+requirejs を探索する内容です。関連コンテンツについては、PHP 中国語 Web サイト (www.php.cn) に注目してください。