AngularJSでのオンデマンドモジュールの読み込み

急いでいる場合:はい、 AngularJS



でのモジュールの遅延ロードが可能です。これに必要なコードを以下に示します。



AngularJSは遅延読み込みをサポートしていませんか?



AngularJSはフロントエンド開発に最適なテンプレートの1つですが、まだ若く、いくつかの重要な機能が欠けています(誰が良いルーターと言ったのですか?)。

これらの機能のほとんどは、Googleまたは専用のWebサイトにあるモジュールの形で追加できますが、この方法では追加できない機能がいくつかあります。

現在、多くの人々はモジュールの非同期ロードを必要とし、Googleはフレームワークの第2バージョンでそれを実装しようとしているようですが、誰がそれがいつになるか知っています...



アプリケーションを最適化し、ロード時間を短縮したいので、今これを行う方法を探しています。

2つの非常に興味深い記事(英語)が見つかりました。AngularJSでの読み込みの 遅延と、 RequireJSを使用してアプリケーションを起動した後のAngularJSコンポーネントの読み込みです

ただし、どちらもコントローラー、サービス、フィルター、ディレクティブの遅延ロードについて説明していますが、モジュールの遅延ロードについては説明していません。

モジュールの読み込み専用のソースコードを調べることにしました(こちらで確認できます)。モジュールの登録は初期化後に行われますが、新しい読み込まれたモジュールとその依存関係はアプリケーションに接続せず、初期化されません。



待って、あなたは自分でできませんか?



はい、もちろんできます! 既存のコードを書き換える必要がないため、追加するモジュールが以前にアプリケーションに追加されていないことを確認するだけです。 以前にダウンロードして構成したサービスが必要だと想像してください。サービスを書き換えると、すぐに動作しなくなります。

したがって、以前にロードされたモジュールのリストが必要です。これは簡単に見つけられるはずです。

まあ...はい...実際には...いいえ。

ソースコードを見ると、そこにはmodules



という内部変数がありmodules



。 この変数は、ロードされたすべてのモジュールのリストを保存するために使用され、外部からはアクセスできません。



モジュールのリストを取得できませんか?



いいえ、できません。 ただし、再度作成することができます。

既存のモジュールを取得するためにいつでもangular.module('moduleName')



を使用できます。 結果をログに出力すると、プロパティ_invokeQueue



気付くでしょう。 これは依存関係のリストです。

モジュールは起動時にのみロードでき、アプリケーションはng-app



ディレクティブを使用してのみ起動できるため、アプリケーションモジュールが見つかった場合は、ロードされたモジュールとその依存関係のリスト全体を取得できます。

翻訳者注:実際には、ng-appディレクティブなしでアプリケーションを起動できますが、この場合は重要ではありません。

これは、次のコードで実行できます。

 function init(element) { var elements = [element], appElement, module, names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; function append(elm) { return (elm && elements.push(elm)); } angular.forEach(names, function(name) { names[name] = true; append(document.getElementById(name)); name = name.replace(':', '\\:'); if(element.querySelectorAll) { angular.forEach(element.querySelectorAll('.' + name), append); angular.forEach(element.querySelectorAll('.' + name + '\\:'), append); angular.forEach(element.querySelectorAll('[' + name + ']'), append); } }); angular.forEach(elements, function(elm) { if(!appElement) { var className = ' ' + element.className + ' '; var match = NG_APP_CLASS_REGEXP.exec(className); if(match) { appElement = elm; module = (match[2] || '').replace(/\s+/g, ','); } else { angular.forEach(elm.attributes, function(attr) { if(!appElement && names[attr.name]) { appElement = elm; module = attr.value; } }); } } }); if(appElement) { (function addReg(module) { if(regModules.indexOf(module) === -1) { regModules.push(module); var mainModule = angular.module(module); angular.forEach(mainModule.requires, addReg); } })(module); } }
      
      





新しいモジュールの登録



以前にロードされたモジュールのリストができたので、新しいモジュールを追加できます(以前にダウンロードされていないモジュールのみ)。

これを行うには、Angularでモジュールと依存関係がどのように呼び出されるかを理解する必要があります。

モジュールが登録されるとすぐに、すべての依存関係が(「初期化」フェーズで)ロードされ、初期化の結果としてモジュールに追加されます。 メモリに依存関係が既に存在する場合、初期化フェーズはスキップされ、既存の依存関係へのリンクがモジュールに追加されます。

モジュールは、構成フェーズと実行フェーズを通過できます。

構成フェーズは、依存関係をロードする前に実行されます。

実行フェーズは、構成フェーズとすべての必要な依存関係の完全なロードの後に​​のみ開始されます。 _runBlocks



パラメーターで実行フェーズコードを_runBlocks



できます。

実行フェーズを開始するには、 $injector



サービスのinvoke関数を使用します。

そのため、最後に、すべての依存関係のリストを作成し、構成フェーズと実行フェーズを追跡し、それらを正しい順序で呼び出します。

実際には、 ソースモジュールから学習できるモジュールの角度の読み込み方法を再現します

結果は次の関数に表示されます。

 function register(providers, registerModules, $log) { var i, ii, k, invokeQueue, moduleName, moduleFn, invokeArgs, provider; if(registerModules) { var runBlocks = []; for(k = registerModules.length - 1; k >= 0; k--) { moduleName = registerModules[k]; regModules.push(moduleName); moduleFn = angular.module(moduleName); runBlocks = runBlocks.concat(moduleFn._runBlocks); try { for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { invokeArgs = invokeQueue[i]; if(providers.hasOwnProperty(invokeArgs[0])) { provider = providers[invokeArgs[0]]; } else { return $log.error("unsupported provider " + invokeArgs[0]); } provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } } catch(e) { if(e.message) { e.message += ' from ' + moduleName; } $log.error(e.message); throw e; } registerModules.pop(); } angular.forEach(runBlocks, function(fn) { providers.$injector.invoke(fn); }); } return null; }
      
      





ただし、依存関係を登録するときは、正しいプロバイダーを使用する必要があることに注意してください。 ディレクティブを登録するときにサービスプロバイダーを使用する必要はありません。



サービスを書きましょう



各プロバイダーとインジェクターは、構成フェーズと初期化フェーズで使用できます。 後で使用する場合は、それらへのリンクを保持する必要があります。

また、リロードされないように、ロードされたモジュールを追跡する必要があります。

市場には多くの非同期ブートローダー(requireJS、script.js ...)があり、独自の自転車を発明するのは愚かなことなので、独自の非同期ブートローダーを作成したくありません。

次の構文をサポートするブートローダーを使用できます。

 loader([urls], function callback() {});
      
      





このようにして、必要なリソース(jsファイル、cssなどのみ)をロードできます。

この例では、 script.jsを使用します

 var modules = {}, asyncLoader, providers = { $controllerProvider: $controllerProvider, $compileProvider: $compileProvider, $filterProvider: $filterProvider, $provide: $provide, // other things $injector: $injector };
      
      





これは設定のかなり簡単な部分です。ディレクティブを使用してロードできる非同期ローダーとモジュールのリストを定義する必要があります。

また、以前に定義した初期化スクリプトを実行して、初期初期化用のモジュールのリストを作成します。

 this.config = function(config) { if(typeof config.asyncLoader === 'undefined') { throw('You need to define an async loader such as requireJS or script.js'); } asyncLoader = config.asyncLoader; init(angular.element(window.document)); if(typeof config.modules !== 'undefined') { if(angular.isArray(config.modules)) { angular.forEach(config.modules, function(moduleConfig) { modules[moduleConfig.name] = moduleConfig; }); } else { modules[config.modules.name] = config.modules; } } };
      
      





プロバイダーを構成するには、次のコードをアプリケーションに追加するだけです。

 angular.module('app').config(['$ocLazyLoadProvider', function($ocLazyLoadProvider) { $ocLazyLoadProvider.config({ modules: [ { name: 'TestModule', files: ['js/testModule.js'], template: 'partials/testLazyLoad.html' } ], asyncLoader: $script }); }]);
      
      





プロバイダーを記述しているので、コンポーネントインジェクションを使用できる唯一の場所は$get



プロパティです。 このプロパティを返し、サービスで利用できるもの。

モジュールを設定するためのゲッターとセッター、およびそれらをロードするための関数を定義します。

誰もがプラグインを書きたい場合に利用できるはずなので、モジュールのリストのゲッターも追加します。

ゲッターとセッターは簡単に定義できます:

 getModuleConfig: function(name) { if(!modules[name]) { return null; } return modules[name]; }, setModuleConfig: function(module) { modules[module.name] = module; return module; }, getModules: function() { return regModules; }
      
      





それでは、ダウンロード機能を詳しく見てみましょう。 ロードモジュールを名前または構成で実装する必要があります。

構成オブジェクトには、モジュール名、ファイルのリスト(スクリプト、css ...)、およびオプションのテンプレートが含まれます。

ディレクティブを使用してモジュールをロードすると、テンプレートがそのコードを置き換えるために使用されます。

また、モジュールの依存関係のリストを保持して、登録できるようにします。

ロード関数はプロミスを返します。これにより、開発が簡単になります。

 load: function(name, callback) { var self = this, config, moduleCache = [], deferred = $q.defer(); if(typeof name === 'string') { config = self.getModuleConfig(name); } else if(typeof name === 'object' && typeof name.name !== 'undefined') { config = self.setModuleConfig(name); name = name.name; } moduleCache.push = function(value) { if(this.indexOf(value) === -1) { Array.prototype.push.apply(this, arguments); } }; if(!config) { var errorText = 'Module "' + name + '" not configured'; $log.error(errorText); throw errorText; } }
      
      





モジュールの依存関係を取得する関数が必要です。

 function getRequires(module) { var requires = []; angular.forEach(module.requires, function(requireModule) { if(regModules.indexOf(requireModule) === -1) { requires.push(requireModule); } }); return requires; }
      
      





また、初期化の瞬間から現在の瞬間まで何かを見逃した場合に備えて、特定のモジュールが以前にロードされたかどうかをチェックする機能も必要です。 これを行う「クリーンな」方法はないため、「ダーティ」な方法を使用する必要があります。

 function moduleExists(moduleName) { try { angular.module(moduleName); } catch(e) { if(/No module/.test(e) || (e.message.indexOf('$injector:nomod') > -1)) { return false; } } return true; }
      
      





これで、新しいモジュールの依存関係をロードする関数を作成できます。 モジュールが以前にロードされている場合はすぐに制御を返します。または、 moduleCache



変数に値を入力して、登録のための新しいモジュールとその依存関係のリストを取得します。

 function loadDependencies(moduleName, allDependencyLoad) { if(regModules.indexOf(moduleName) > -1) { return allDependencyLoad(); } var loadedModule = angular.module(moduleName), requires = getRequires(loadedModule); function onModuleLoad(moduleLoaded) { if(moduleLoaded) { var index = requires.indexOf(moduleLoaded); if(index > -1) { requires.splice(index, 1); } } if(requires.length === 0) { $timeout(function() { allDependencyLoad(moduleName); }); } } var requireNeeded = getRequires(loadedModule); angular.forEach(requireNeeded, function(requireModule) { moduleCache.push(requireModule); if(moduleExists(requireModule)) { return onModuleLoad(requireModule); } var requireModuleConfig = self.getConfig(requireModule); if(requireModuleConfig && (typeof requireModuleConfig.files !== 'undefined')) { asyncLoader(requireModuleConfig.files, function() { loadDependencies(requireModule, function requireModuleLoaded(name) { onModuleLoad(name); }); }); } else { $log.warn('module "' + requireModule + "' not loaded and not configured"); onModuleLoad(requireModule); } return null; }); if(requireNeeded.length === 0) { onModuleLoad(); } return null; }
      
      





最後に、依存関係をロードして登録する非同期ローダーを呼び出す必要があります。

 asyncLoader(config.files, function() { moduleCache.push(name); loadDependencies(name, function() { register(providers, moduleCache, $log); $timeout(function() { deferred.resolve(config); }); }); });
      
      





これを行ったので、オンデマンドでモジュールをロードできます!!!

 $ocLazyLoad.load({ name: 'TestModule', files: ['js/testModule.js'] }).then(function() { console.log('done!'); });
      
      





ディレクティブを使用する



ディレクティブを使用してモジュールをロードできるはずです。 これを行うには、前述のtemplate



パラメーターを使用します。 このテンプレートはディレクティブを置き換えます。

$templateCache



サービスを使用して、アプリケーションのキャッシュに既に存在するテンプレートの読み込みを防ぎます。

ディレクティブは次の方法で呼び出されます。

 <div oc-lazy-load="{name: 'TestModule', files: ['js/testModule.js'], template: 'partials/testLazyLoad.html'}"></div>
      
      





プロバイダー設定でTestModule



モジュールの構成を定義した場合、次のようにディレクティブを呼び出すことができます。

 <div oc-lazy-load="'TestModule'"></div>
      
      





ディレクティブを書くことはこの記事の目的ではないので、その説明はスキップします。 ディレクティブの重要な部分は、URLによって、または以前にロードされた場合はキャッシュから、新しいテンプレートをロードすることです。

 ocLazyLoad.directive('ocLazyLoad', ['$http', '$log', '$ocLazyLoad', '$compile', '$timeout', '$templateCache', function($http, $log, $ocLazyLoad, $compile, $timeout, $templateCache) { return { link: function(scope, element, attr) { var childScope; /** * Destroy the current scope of this element and empty the html */ function clearContent() { if(childScope) { childScope.$destroy(); childScope = null; } element.html(''); } /** * Load a template from cache or url * @param url * @param callback */ function loadTemplate(url, callback) { scope.$apply(function() { var view; if(typeof(view = $templateCache.get(url)) !== 'undefined') { scope.$evalAsync(function() { callback(view); }); } else { $http.get(url) .success(function(data) { $templateCache.put('view:' + url, data); scope.$evalAsync(function() { callback(data); }); }) .error(function(data) { $log.error('Error load template "' + url + "': " + data); }); } }); } scope.$watch(attr.ocLazyLoad, function(moduleName) { if(moduleName) { $ocLazyLoad.load(moduleName).then(function(moduleConfig) { if(!moduleConfig.template) { return; } loadTemplate(moduleConfig.template, function(template) { childScope = scope.$new(); element.html(template); var content = element.contents(); var linkFn = $compile(content); $timeout(function() { linkFn(childScope); }); }); }); } else { clearContent(); } }); } }; }]);
      
      





サービスとui-routerの統合



モジュールの遅延ロードは、通常、新しいルートをロードするときに発生します。 ui-routerでこれを行う方法を見てみましょう(しかし、これはng-route



)。

サービスまたはディレクティブを使用してモジュールをロードできるため、2つのオプションを使用できますresolve



オブジェクトを使用するか、テンプレートを使用します。

サービスを使用するには、 resolve



オブジェクトを使用する必要があります。 resolve



オブジェクトを使用すると、ルートにいくつかのパラメーターを定義でき、テンプレートをロードする前に呼び出されます。 これは重要です。テンプレートは、遅延読み込みを実行するコントローラーを使用できます。

resolve



関数の各パラメーターにより、promiseはどのように解決するかを決定できます。 ロード関数はプロミスを返すので、そのまま使用できます。 ここでは、 views



部分が必要です。これはこの例のためだけです。

 $stateProvider.state('index', { url: "/", // root route views: { "lazyLoadView": { templateUrl: 'partials/testLazyLoad.html' } }, resolve: { test: ['$ocLazyLoad', function($ocLazyLoad) { return $ocLazyLoad.load({ name: 'TestModule', files: ['js/testModule.js'] }); }] } });
      
      





ディレクティブの使用も簡単です。

 $stateProvider.state('index', { url: "/", views: { "lazyLoadView": { template: '<div oc-lazy-load="{name: \'TestModule\', files: [\'js/testModule.js\'], template: \'partials/testLazyLoad.html\'}"></div>' } } });
      
      





複雑なレイヤーを追加したresolve



、これはresolve



機能を使用するよりも少し最適ではないと思いますが、場合によっては非常に便利です。

だから私たちはすべてをやった この遅延ブートローダーがお役に立てば幸いです!

完全に機能する例として、 Plunkrをご覧ください

また、すべてのコードとgithubの例を見ることができます。

この角度モジュールをプロジェクトのベースとして使用しましたが、必要な新しい機能を追加することで大幅に改善しました。



All Articles